# Handler Handlers in SigNoz are responsible for exposing module functionality over HTTP. They are thin adapters that: - Decode incoming HTTP requests - Call the appropriate module layer - Return structured responses (or errors) in a consistent format - Describe themselves for OpenAPI generation They are **not** the place for complex business logic; that belongs in modules (for example, `pkg/modules/user`, `pkg/modules/session`, etc). ## How are handlers structured? At a high level, a typical flow looks like this: 1. A `Handler` interface is defined in the module (for example, `user.Handler`, `session.Handler`, `organization.Handler`). 2. The `apiserver` provider wires those handlers into HTTP routes using Gorilla `mux.Router`. Each route wraps a module handler method with the following: - Authorization middleware (from `pkg/http/middleware`) - A generic HTTP `handler.Handler` (from `pkg/http/handler`) - An `OpenAPIDef` that describes the operation for OpenAPI generation For example, in `pkg/apiserver/signozapiserver`: ```go if err := router.Handle("/api/v1/invite", handler.New( provider.authZ.AdminAccess(provider.userHandler.CreateInvite), handler.OpenAPIDef{ ID: "CreateInvite", Tags: []string{"users"}, Summary: "Create invite", Description: "This endpoint creates an invite for a user", Request: new(types.PostableInvite), RequestContentType: "application/json", Response: new(types.Invite), ResponseContentType: "application/json", SuccessStatusCode: http.StatusCreated, ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict}, Deprecated: false, SecuritySchemes: newSecuritySchemes(types.RoleAdmin), }, )).Methods(http.MethodPost).GetError(); err != nil { return err } ``` In this pattern: - `provider.userHandler.CreateInvite` is a handler method. - `provider.authZ.AdminAccess(...)` wraps that method with authorization checks and context setup. - `handler.New` converts it into an HTTP handler and wires it to OpenAPI via the `OpenAPIDef`. ## How to write a new handler method? When adding a new endpoint: 1. Add a method to the appropriate module `Handler` interface. 2. Implement that method in the module. 3. Register the method in `signozapiserver` with the correct route, HTTP method, auth, and `OpenAPIDef`. ### 1. Extend an existing `Handler` interface or create a new one Find the module in `pkg/modules/` and extend its `Handler` interface with a new method that receives an `http.ResponseWriter` and `*http.Request`. For example: ```go type Handler interface { // existing methods... CreateThing(rw http.ResponseWriter, req *http.Request) } ``` Keep the method focused on HTTP concerns and delegate business logic to the module. ### 2. Implement the handler method In the module implementation, implement the new method. A typical implementation: - Extracts authentication and organization context from `req.Context()` - Decodes the request body into a `types.*` struct using the `binding` package - Calls module functions - Uses the `render` package to write responses or errors ```go func (h *handler) CreateThing(rw http.ResponseWriter, req *http.Request) { // Extract authentication and organization context from req.Context() claims, err := authtypes.ClaimsFromContext(req.Context()) if err != nil { render.Error(rw, err) return } // Decode the request body into a `types.*` struct using the `binding` package var in types.PostableThing if err := binding.JSON.BindBody(req.Body, &in); err != nil { render.Error(rw, err) return } // Call module functions out, err := h.module.CreateThing(req.Context(), claims.OrgID, &in) if err != nil { render.Error(rw, err) return } // Use the `render` package to write responses or errors render.Success(rw, http.StatusCreated, out) } ``` ### 3. Register the handler in `signozapiserver` In `pkg/apiserver/signozapiserver`, add a route in the appropriate `add*Routes` function (`addUserRoutes`, `addSessionRoutes`, `addOrgRoutes`, etc.). The pattern is: ```go if err := router.Handle("/api/v1/things", handler.New( provider.authZ.AdminAccess(provider.thingHandler.CreateThing), handler.OpenAPIDef{ ID: "CreateThing", Tags: []string{"things"}, Summary: "Create thing", Description: "This endpoint creates a thing", Request: new(types.PostableThing), RequestContentType: "application/json", Response: new(types.GettableThing), ResponseContentType: "application/json", SuccessStatusCode: http.StatusCreated, ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict}, Deprecated: false, SecuritySchemes: newSecuritySchemes(types.RoleAdmin), }, )).Methods(http.MethodPost).GetError(); err != nil { return err } ``` ### 4. Update the OpenAPI spec Run the following command to update the OpenAPI spec: ```bash go run cmd/enterprise/*.go generate openapi ``` This will update the OpenAPI spec in `docs/api/openapi.yml` to reflect the new endpoint. ## How does OpenAPI integration work? The `handler.New` function ties the HTTP handler to OpenAPI metadata via `OpenAPIDef`. This drives the generated OpenAPI document. - **ID**: A unique identifier for the operation (used as the `operationId`). - **Tags**: Logical grouping for the operation (for example, `"users"`, `"sessions"`, `"orgs"`). - **Summary / Description**: Human-friendly documentation. - **Request / RequestContentType**: - `Request` is a Go type that describes the request body or form. - `RequestContentType` is usually `"application/json"` or `"application/x-www-form-urlencoded"` (for callbacks like SAML). - **RequestExamples**: An array of `handler.OpenAPIExample` that provide concrete request payloads in the generated spec. See [Adding request examples](#adding-request-examples) below. - **Response / ResponseContentType**: - `Response` is the Go type for the successful response payload. - `ResponseContentType` is usually `"application/json"`; use `""` for responses without a body. - **SuccessStatusCode**: The HTTP status for successful responses (for example, `http.StatusOK`, `http.StatusCreated`, `http.StatusNoContent`). - **ErrorStatusCodes**: Additional error status codes beyond the standard ones automatically added by `handler.New`. - **SecuritySchemes**: Auth mechanisms and scopes required by the operation. The generic handler: - Automatically appends `401`, `403`, and `500` to `ErrorStatusCodes` when appropriate. - Registers request and response schemas with the OpenAPI reflector so they appear in `docs/api/openapi.yml`. See existing examples in: - `addUserRoutes` (for typical JSON request/response) - `addSessionRoutes` (for form-encoded and redirect flows) ## OpenAPI schema details for request/response types The OpenAPI spec is generated from the Go types you pass as `Request` and `Response` in `OpenAPIDef`. The following struct tags and interfaces control how those types appear in the generated schema. ### Adding request examples Use the `RequestExamples` field in `OpenAPIDef` to provide concrete request payloads. Each example is a `handler.OpenAPIExample`: ```go type OpenAPIExample struct { Name string // unique key for the example (e.g. "traces_time_series") Summary string // short description shown in docs (e.g. "Time series: count spans grouped by service") Description string // optional longer description Value any // the example payload, typically map[string]any } ``` For reference, see `pkg/apiserver/signozapiserver/querier.go` which defines examples inline for the `/api/v5/query_range` endpoint: ```go if err := router.Handle("/api/v5/query_range", handler.New(provider.authZ.ViewAccess(provider.querierHandler.QueryRange), handler.OpenAPIDef{ ID: "QueryRangeV5", Tags: []string{"querier"}, Summary: "Query range", Description: "Execute a composite query over a time range.", Request: new(qbtypes.QueryRangeRequest), RequestContentType: "application/json", RequestExamples: []handler.OpenAPIExample{ { Name: "traces_time_series", Summary: "Time series: count spans grouped by service", Value: map[string]any{ "schemaVersion": "v1", "start": 1640995200000, "end": 1640998800000, "requestType": "time_series", "compositeQuery": map[string]any{ "queries": []any{ map[string]any{ "type": "builder_query", "spec": map[string]any{ "name": "A", "signal": "traces", // ... }, }, }, }, }, }, // ... more examples }, // ... })).Methods(http.MethodPost).GetError(); err != nil { return err } ``` ### `required` tag Use `required:"true"` on struct fields where the property is expected to be **present** in the JSON payload. This is different from the zero value, a field can have its zero value (e.g. `0`, `""`, `false`) and still be required. The `required` tag means the key itself must exist in the JSON object. ```go type ListItem struct { ... } type ListResponse struct { List []ListItem `json:"list" required:"true" nullable:"true"` Total uint64 `json:"total" required:"true"` } ``` In this example, a response like `{"list": null, "total": 0}` is valid. Both keys are present (satisfying `required`), `total` has its zero value, and `list` is null (allowed by `nullable`). But `{"total": 0}` would violate the schema because the `list` key is missing. ### `nullable` tag Use `nullable:"true"` on struct fields that can be `null` in the JSON payload. This is especially important for **slice and map fields** because in Go, the zero value for these types is `nil`, which serializes to `null` in JSON (not `[]` or `{}`). Be explicit about the distinction: - **Nullable list** (`nullable:"true"`): the field can be `null`. Use this when the Go code may return `nil` for the slice. - **Non-nullable list** (no `nullable` tag): the field is always an array, never `null`. Ensure the Go code initializes it to an empty slice (e.g. `make([]T, 0)`) before serializing. ```go // Non-nullable: Go code must ensure this is always an initialized slice. type NonNullableExample struct { Items []Item `json:"items" required:"true"` } ``` When defining your types, ask yourself: "Can this field be `null` in the JSON response, or is it always an array/object?" If the Go code ever returns a `nil` slice or map, mark it `nullable:"true"`. ### `Enum()` method For types that have a fixed set of acceptable values, implement the `Enum() []any` method. This generates an `enum` constraint in the JSON schema so the OpenAPI spec accurately restricts the values. ```go type Signal struct { valuer.String } var ( SignalTraces = Signal{valuer.NewString("traces")} SignalLogs = Signal{valuer.NewString("logs")} SignalMetrics = Signal{valuer.NewString("metrics")} ) func (Signal) Enum() []any { return []any{ SignalTraces, SignalLogs, SignalMetrics, } } ``` This produces the following in the generated OpenAPI spec: ```yaml Signal: enum: - traces - logs - metrics type: string ``` Every type with a known set of values **must** implement `Enum()`. Without it, the JSON schema will only show the base type (e.g. `string`) with no value constraints. ### `JSONSchema()` method (custom schema) For types that need a completely custom JSON schema (for example, a field that accepts either a string or a number), implement the `jsonschema.Exposer` interface: ```go var _ jsonschema.Exposer = Step{} func (Step) JSONSchema() (jsonschema.Schema, error) { s := jsonschema.Schema{} s.WithDescription("Step interval. Accepts a duration string or seconds.") strSchema := jsonschema.Schema{} strSchema.WithType(jsonschema.String.Type()) strSchema.WithExamples("60s", "5m", "1h") numSchema := jsonschema.Schema{} numSchema.WithType(jsonschema.Number.Type()) numSchema.WithExamples(60, 300, 3600) s.OneOf = []jsonschema.SchemaOrBool{ strSchema.ToSchemaOrBool(), numSchema.ToSchemaOrBool(), } return s, nil } ``` ## What should I remember? - **Keep handlers thin**: focus on HTTP concerns and delegate logic to modules/services. - **Always register routes through `signozapiserver`** using `handler.New` and a complete `OpenAPIDef`. - **Choose accurate request/response types** from the `types` packages so OpenAPI schemas are correct. - **Add `required:"true"`** on fields where the key must be present in the JSON (this is about key presence, not about the zero value). - **Add `nullable:"true"`** on fields that can be `null`. Pay special attention to slices and maps -- in Go these default to `nil` which serializes to `null`. If the field should always be an array, initialize it and do not mark it nullable. - **Implement `Enum()`** on every type that has a fixed set of acceptable values so the JSON schema generates proper `enum` constraints. - **Add request examples** via `RequestExamples` in `OpenAPIDef` for any non-trivial endpoint. See `pkg/apiserver/signozapiserver/querier.go` for reference.