Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ http.ListenAndServe(":8080", api.NewRouter(pets{}))
| [Server](docs/server.md) | the service interface, the HTTP adapter, 14 routers, starter files |
| [Client](docs/client.md) | one method per operation, envelopes, streams |
| [MCP](docs/mcp.md) | MCP tools over the client |
| [Observability](docs/observability.md) | tracing and metrics with OpenTelemetry, on server and client |
| [Templates](docs/templates.md) | replacing template blocks, extra files from your own templates |
| [Migration](docs/migration.md) | moving from another generator |

Expand Down
37 changes: 28 additions & 9 deletions docs/frameworks.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,19 +114,38 @@ The `framework` package holds what the rules share:

```go
type Handler struct {
Signature string // after the name: "(w http.ResponseWriter, r *http.Request)" or "(c echo.Context) error"
Prologue string // binds w and r when the parameters are others: "w, r := c.Response(), c.Request()"
Return string // leaves the handler early: "return" or "return nil"
Epilogue string // ends the handler: "" or "return nil"
Signature string // after the name: "(w http.ResponseWriter, r *http.Request)" or "(c echo.Context) error"
Writer string // the handler's response writer: "w" or "c.Response()"
Request string // the handler's request: "r" or "c.Request()"
Epilogue string // ends the handler: "" or "return nil"
ServeSignature string // the serve method's shape: "(w http.ResponseWriter, r *http.Request)"
Context string // the framework's context: "" or "c"
ContextServeSignature string // the same, with Context first: "(c echo.Context, w http.ResponseWriter, ...)"
}
```

The adapter template writes every handler as `func (a *HTTPAdapter) Op<Signature> {`, then the
prologue, then the decoding of the request, with `Return` on every early exit.
The adapter template writes two methods per operation:

```go
func (a *HTTPAdapter) GetPet(c echo.Context) error {
a.operations.Serve(c.Response(), c.Request(), "GetPet", func(w http.ResponseWriter, r *http.Request) {
a.serveGetPet(c, w, r)
})
return nil
}

func (a *HTTPAdapter) serveGetPet(c echo.Context, w http.ResponseWriter, r *http.Request) {
// decodes the request, calls the service, writes the response
}
```

`Serve` runs the serve method inside the [operation middleware](server.md#operation-middleware).
An operation without path parameters does not need the context, so its serve method takes
`ServeSignature` and the handler passes `a.serveListPets` as it is.

### Names in a handler

The body of a handler always names the response writer `w`, the request `r` and the framework's
The serve method always names the response writer `w`, the request `r` and the framework's
context `c`. So `PathParam` may use them.

`PathParam` has to give the value unescaped once, whatever the router hands over. For a router that
Expand All @@ -135,8 +154,8 @@ cuts values from the raw path, use `runtime.UnescapePath`.
### Two shapes

- A framework whose handlers are `http.HandlerFunc`s returns `framework.HTTPHandler(s)`.
- A framework whose handlers take its context returns `framework.ContextHandler`, with the
context's type spelled through `s.Import`.
- A framework whose handlers take its context returns `framework.ContextHandler(s, ctxType)`,
with the context's type spelled through `s.Import`.

Handlers of the second kind read their errors from the error handler. They write every failed
request themselves and return nil, so an operation answers the same under every framework.
Expand Down
6 changes: 4 additions & 2 deletions docs/migration/ogen.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ are built and tested.
`WithJSONEncoder` on the server, take another library such as sonic.
- A `uuid`, `uri` or `ipv4` format is a `string` checked by `Validate`, unless
`models.format-types` names a type for it.
- There is no security handler and no OpenTelemetry. A middleware checks credentials, and a
wrapped `http.Client` traces the client.
- There is no security handler, and the generated code does not import OpenTelemetry. A
middleware checks credentials. `otelhttp` traces the server through `WithOperationMiddleware`,
with spans named after the operation as in ogen, and the client through `WithHTTPClient`
([observability](../observability.md)).
- The routes go on the router of `server.framework`. `std-http` is the standard library's
`ServeMux`.

Expand Down
73 changes: 73 additions & 0 deletions docs/observability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Observability

The generated code imports no tracing or metrics library. It gives you two hooks, and you bring the
library. The recipes below use OpenTelemetry's `otelhttp`.

| Side | Hook |
|---|---|
| server | `WithOperationMiddleware`, see [operation middleware](server.md#operation-middleware) |
| client | `WithHTTPClient`, see [HTTP client](client.md#http-client) |

## Operation names

Server and client put the name of the operation, `GetPet`, on the request's context.
`runtime.OperationID(ctx)` reads it. Use it as a span name, a metric label or a log field.

```go
func operationName(_ string, r *http.Request) string {
return runtime.OperationID(r.Context())
}
```

## Server

```go
import (
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel/attribute"

"github.com/mockzilla/mockzilla-codegen/pkg/runtime"
)

func labelOperation(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
labeler, _ := otelhttp.LabelerFromContext(r.Context())
labeler.Add(attribute.String("operation", runtime.OperationID(r.Context())))
next.ServeHTTP(w, r)
})
}

tracing := otelhttp.NewMiddleware("", otelhttp.WithSpanNameFormatter(operationName))
router := NewRouter(svc, WithOperationMiddleware(tracing, labelOperation))
```

- Each request gets a server span named `GetPet`.
- The span covers reading the request, the service and writing the response. A 400 for a bad
parameter is in it too.
- The service's `ctx` holds the span, so the spans the service starts are its children.
- `labelOperation` adds `operation="GetPet"` to otelhttp's request metrics, such as
`http.server.request.duration`. Leave it out if you only want spans.

`otelhttp.NewMiddleware` also fits `WithMiddleware`. There it runs before the route is picked, so
its spans cannot be named after the operation.

## Client

```go
httpClient := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport, otelhttp.WithSpanNameFormatter(operationName)),
}
c, err := NewPetClient(baseURL, WithHTTPClient(httpClient))
```

Each call gets a client span named `GetPet`.

## One trace across services

Set a propagator once, in the client's program and in the server's:

```go
otel.SetTextMapPropagator(propagation.TraceContext{})
```

The client then sends a `traceparent` header, and the server span joins the caller's trace.
37 changes: 36 additions & 1 deletion docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,8 @@ A property of a JSON, form or multipart body gets its default the same way, see
A handler first puts the name of its operation, `ListPets`, on the request's context. The service,
a wrapper of it and the error handler read it with `runtime.OperationID(ctx)`.

The router's middleware runs before the handler, so it does not see the name.
The router's middleware runs before the handler, so it does not see the name. Operation
middleware does, see [operation middleware](#operation-middleware).

### Reading the body

Expand Down Expand Up @@ -362,6 +363,7 @@ Options are set with `ServerOption` functions on the adapter and on the router a
| Option | Sets |
|---|---|
| `WithMiddleware(mw...)` | `func(http.Handler) http.Handler` wrappers, outermost first |
| `WithOperationMiddleware(mw...)` | the same wrappers around each operation, see [operation middleware](#operation-middleware) |
| `WithErrorHandler(h)` | what writes failed requests, `DefaultErrorHandler{}` by default |
| `WithJSONDecoder(fn)` | what reads JSON request bodies, `json.Unmarshal` by default |
| `WithJSONEncoder(fn)` | what writes JSON response bodies and stream frames, `json.Marshal` by default |
Expand Down Expand Up @@ -630,6 +632,39 @@ wraps those routes only:
A router can make a redirect on its own, for a trailing slash or a path it cleans. On gin,
gorilla-mux, kratos, iris, fasthttp and hertz, that redirect is answered before the middleware.

### Operation middleware

`WithOperationMiddleware` takes the same `func(http.Handler) http.Handler` as `WithMiddleware`.
It wraps each operation, once the operation's name is on the context:

```go
func logOperation(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.InfoContext(r.Context(), "served", "operation", runtime.OperationID(r.Context()), "took", time.Since(start))
})
}

router := NewRouter(svc, WithOperationMiddleware(logOperation))
```

| | `WithMiddleware` | `WithOperationMiddleware` |
|---|---|---|
| Runs | before the router picks a route | after it, around one operation |
| Sees the operation name | no | yes |
| Wraps unknown paths | on a new router | no |
| Wraps a 400 for a bad parameter or body | yes | yes |

- Each middleware is called once, when the adapter is made. So it can hold a tracer or a counter.
- It passes on the request it got, or one from `r.WithContext` with a context made from
`r.Context()`. A request with a context of its own panics with `httpserver.ErrContextLost`.
- A middleware that wraps the writer keeps its `Flush`, or gives an `Unwrap` method. Otherwise the
frames of a stream are not flushed as they go.
- On echo and kratos too, the operation gets the writer and the request the middleware passes on.

For tracing and metrics, see [observability](observability.md).

### Handlers

Most routers take the handlers as `http.HandlerFunc`s: some directly, the others through a small
Expand Down
40 changes: 28 additions & 12 deletions examples/bodies/checked/gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 24 additions & 11 deletions examples/bodies/defaults/gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading