signoz Documentation

SigNoz is an open-source, OpenTelemetry-native observability platform for your team and their AI agents. Get logs, metrics, and traces in one tool with...

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy signoz on any server, homelab, or cloud container platform.

File: README.md

SigNoz is an open-source observability platform built on OpenTelemetry. We’re building an enterprise-grade alternative to fragmented monitoring stacks, with logs, metrics, traces, alerts, and dashboards in one place.

Choose how to run SigNoz

Fully managed SigNoz with a 30-day free trial, no credit card required, usage-based pricing that starts at $49, and regional data hosting.

Start free β†’

Enterprise

Enterprise Cloud, BYOC, or Enterprise Self-Hosted with compliance, support, custom retention, RBAC, ingestion controls, data residency, and region selection.

Explore Enterprise β†’

Community

Free open-source SigNoz that runs in your own infrastructure. Deploy with Docker, Kubernetes, or Linux and keep full control of your data plane.

Install SigNoz β†’

What can you monitor?

SigNoz helps teams debug production issues faster by connecting logs, metrics, traces, alerts, dashboards, exceptions, and agent-native workflows in one place.

APM Overview

Monitor service latency, error rate, throughput, Apdex, top endpoints, database calls, and external calls.

Learn more: APM documentation

Log Management

Ingest, search, aggregate, and correlate logs with traces and metrics using a visual query builder.

Learn more: Log management documentation

Metrics and Dashboards

Build dashboards for application, infrastructure, and custom metrics using Query Builder, PromQL, or ClickHouse SQL.

Learn more: Metrics documentation

Infrastructure Monitoring

Monitor Kubernetes clusters, pods, nodes, workloads, and host-level CPU, memory, disk, network, logs, and traces.

Learn more: Infrastructure monitoring documentation

LLM and AI Observability

Trace LLM apps, RAG pipelines, prompts, tool calls, tokens, latency, and costs alongside application and infrastructure telemetry.

Learn more: LLM observability documentation

Agent-Native Observability and MCP

Use the SigNoz MCP server to bring telemetry into coding agents, or use Noz inside SigNoz to investigate incidents, tune alerts, and build dashboards with production context. Noz is available only on SigNoz Cloud.

Learn more: SigNoz MCP server docs Β· Agent skills docs

Distributed Tracing

Follow requests across services with flamegraphs, waterfalls, span events, filters, and trace analytics.

Learn more: Distributed tracing documentation

Trace Funnels

Create funnels from traces to understand request-flow drop-offs, failed transitions, and systemic workflow issues.

Learn more: Trace funnels documentation

Also monitor: exceptions, alerts, external APIs, and integrations for OpenTelemetry, Prometheus, Kubernetes, cloud providers, language SDKs, application frameworks, databases, and LLM tools.

Why teams use SigNoz

  1. OpenTelemetry-native
    Instrument once with open standards and keep ownership of your telemetry.
  2. Correlated signals
    Move from service charts to traces, logs, infra metrics, and exceptions without switching tools.
  3. Single columnar database
    Built for high-cardinality, high-volume observability workloads.
  4. Predictable pricing
    No per-host pricing, no user-seat pricing, and no special pricing for custom metrics.
  5. Enterprise ready
    SOC 2 Type II and HIPAA compliance, RBAC, ingestion controls, custom retention, support, BYOC, and self-hosting.

Getting started

Start on Cloud

Create a managed SigNoz workspace and get your first dashboard without running observability infrastructure.

Start free on SigNoz Cloud

Self-host SigNoz

Run SigNoz in your own infrastructure with Foundry, Docker, Kubernetes, or Linux.

Foundry Β· Docker Β· Kubernetes Β· Linux

Send data

Instrument applications and infrastructure with OpenTelemetry, Prometheus, language SDKs, and integrations.

Instrumentation Β· Integrations

Comparisons to familiar tools

SigNoz is often adopted by teams moving from a stack of single-purpose tools or commercial platforms with unpredictable pricing.

Prometheus
Good if you just need metrics. SigNoz keeps metrics, logs, traces, dashboards, and alerts together so teams can debug with correlated context.

Jaeger
Jaeger only does distributed tracing. SigNoz adds metrics, logs, trace analytics, dashboards, alerts, exceptions, and trace-to-log workflows.

Elastic
SigNoz uses columnar database for efficient observability analytics and high-cardinality log workloads, with 50% lower resource requirement compared to Elastic during ingestion. Check the detailed study.

Loki
In the linked benchmark, SigNoz indexed all keys in the test setup, while Loki hit max stream errors when more labels were added. Check the detailed study.

Contributing

We ❀️ contributions big or small. Please read CONTRIBUTING.md to get started with making contributions to SigNoz.

Not sure how to get started? Just ping us on #contributing in our slack community.

As always, thanks to our amazing contributors!


File: deploy/README.md

Deploy

Check that you have cloned signoz/signoz
and currently are in signoz/deploy folder.

Installation

Note: The install.sh script and the docker-compose manifests have been deprecated.

SigNoz now installs and runs through Foundry.

Already running SigNoz via Docker Compose? See the Migration Guide to transition your existing deployment to Foundry.

Please follow the latest installation instructions at signoz.io/docs/install/docker.
Foundry has support for different platforms and architectures, please review the project documentation for more details.

Uninstall/Troubleshoot?

Go to our official documentation site signoz.io/docs for more.


File: docs/contributing/go/endpoint.md

Endpoint

This guide outlines the recommended approach for designing endpoints, with a focus on entity relationships, RESTful structure, and examples from the codebase.

How do we design an endpoint?

Understand the core entities and their relationships

Start with understanding the core entities and their relationships. For example:

  • Organization: an organization can have multiple users

Structure Endpoints RESTfully

Endpoints should reflect the resource hierarchy and follow RESTful conventions. Use clear, pluralized resource names and versioning. For example:

  • POST /v1/organizations β€” Create an organization
  • GET /v1/organizations/:id β€” Get an organization by id
  • DELETE /v1/organizations/:id β€” Delete an organization by id
  • PUT /v1/organizations/:id β€” Update an organization by id
  • GET /v1/organizations/:id/users β€” Get all users in an organization
  • GET /v1/organizations/me/users β€” Get all users in my organization

Think in terms of resource navigation in a file system. For example, to find your organization, you would navigate to the root of the file system and then to the organizations directory. To find a user in an organization, you would navigate to the organizations directory and then to the id directory.

bash
v1/
β”œβ”€β”€ organizations/
β”‚   └── 123/
β”‚       └── users/

me endpoints are special. They are used to determine the actual id via some auth/external mechanism. For me endpoints, think of the me directory being symlinked to your organization directory. For example, if you are a part of the organization 123, the me directory will be symlinked to /v1/organizations/123:

bash
v1/
β”œβ”€β”€ organizations/
β”‚   └── me/ -> symlink to /v1/organizations/123
β”‚       └── users/
β”‚   └── 123/
β”‚       └── users/

πŸ’‘ Note: There are various ways to structure endpoints. Some prefer to use singular resource names instead of me. Others prefer to use singular resource names for all endpoints. We have, however, chosen to standardize our endpoints in the manner described above.

What should I remember?

  • Use clear, plural resource names
  • Use me endpoints for determining the actual id via some auth mechanism

πŸ’‘ Note: When in doubt, diagram the relationships and walk through the user flows as if navigating a file system. This will help you design endpoints that are both logical and user-friendly.


File: docs/contributing/go/errors.md

Errors

SigNoz includes its own structured errors package. It's built on top of Go's error interface, extending it to add additional context that helps provide more meaningful error messages throughout the application.

How to use it?

To use the SigNoz structured errors package, use these functions instead of the standard library alternatives:

go
// Instead of errors.New()
errors.New(typ, code, message)

// Instead of fmt.Errorf()
errors.Newf(typ, code, message, args...)

Typ

The Typ (read as Type, defined as typ) is used to categorize errors across the codebase and is loosely coupled with HTTP/GRPC status codes. All predefined types can be found in pkg/errors/type.go. For example:

  • TypeInvalidInput - Indicates invalid input was provided
  • TypeNotFound - Indicates a resource was not found

By design, typ is unexported and cannot be declared outside of errors package. This ensures that it is consistent across the codebase and is used in a way that is meaningful.

Code

Codes are used to provide more granular categorization within types. For instance, a type of TypeInvalidInput might have codes like CodeInvalidEmail or CodeInvalidPassword.

To create new error codes, use the errors.MustNewCode function:

go
var (
    CodeThingAlreadyExists = errors.MustNewCode("thing_already_exists")
    CodeThingNotFound = errors.MustNewCode("thing_not_found")
)

πŸ’‘ Note: Error codes must match the regex ^[a-z_]+$ otherwise the code will panic.

Message

The primary, human-readable summary of what went wrong, set when the error is created via errors.New / errors.Newf. Note there are two distinct message fields in the response: this top-level one states the overall failure, while each entry under Additional carries its own message explaining one specific facet of it.

Url

An optional link to documentation that explains the error in more depth, set with WithUrl. It is left empty when the error has no associated doc.

go
return errors.New(errors.TypeInvalidInput, CodeBadThing, "bad thing").
    WithUrl("https://signoz.io/docs/...")

Additional

errors is a list of supplementary details that explain the top-level message. Each entry has its own message and suggestions, so a single error can surface several distinct problems individually. Attach details with WithAdditional (message only) or WithSuggestiveAdditional (message plus the suggestions that belong to it):

Message

A single, self-contained sentence describing one specific facet of the error (e.g. field `filed` not found), distinct from the top-level Message. Prefer one detail per distinct problem over concatenating several into one message.

Suggestions

The suggestions tied to that specific detail β€” typically a did you mean: `x` correction for the value the detail is about. These are distinct from the error-wide Suggestions below: detail-scoped suggestions never leak into the top-level list.

go
return errors.NewInvalidInputf(errors.CodeInvalidInput, "unknown field %q", field).
    WithAdditional("field `field` not found")
return errors.NewInvalidInputf(errors.CodeInvalidInput, "unknown field %q", field).
    WithSuggestiveAdditional("field `filed` not found", "did you mean: `field`")

Retry

Carries the delay the client should wait before retrying, set with WithRetryAfter. It is null when the error is not retryable.

go
return errors.NewTimeoutf(CodeSlow, "upstream timed out").
    WithRetryAfter(5 * time.Second)

Suggestions

WithSuggestions sets the error-wide suggestions list β€” hints about the error as a whole (e.g. "narrow the time range window"), as opposed to suggestions tied to a single detail. Prefer the builders in pkg/errors/suggestions.go over hand-writing the strings so the phrasing stays consistent:

  • NewSuggestionsOnLevenshteinDistance(invalidInput, noun, validInputs) β€” returns a did you mean: `x` correction (when a close typo match exists) followed by the valid-references list.
  • NewValidReferences(noun, values...) β€” formats a capped list as valid <noun> are `a`, `b` (e.g. "valid fields are", "valid keys are"). Returns "" for an empty set.
  • NewSuggestionsFromFunc(produce) β€” wraps a caller-computed correction string as a one-element did you mean: `x` slice (or nil when it returns ""), for callers with their own matching strategy.

noun names the kind of value being suggested. Use one of the exported Noun* constants (errors.NounFields, errors.NounKeys, errors.NounServices, …) so the wording stays uniform across the codebase.

go
return errors.NewInvalidInputf(errors.CodeInvalidInput, "unknown field %q", field).
    WithSuggestions(errors.NewSuggestionsOnLevenshteinDistance(field, errors.NounFields, validFields)...)

Show me some examples

Using the error

A basic example of using the error:

go
var (
    CodeThingAlreadyExists = errors.MustNewCode("thing_already_exists")
)

func CreateThing(id string) error {
  t, err := thing.GetFromStore(id)
  if err != nil {
    if errors.As(err, errors.TypeNotFound) {
        // thing was not found, create it
        return thing.Create(id)
    }

    // something else went wrong, wrap the error with more context
    return errors.Wrapf(err, errors.TypeInternal, errors.CodeUnknown, "failed to get thing from store")
  }

  return errors.Newf(errors.TypeAlreadyExists, CodeThingAlreadyExists, "thing with id %s already exists", id)
}

Changing the error

Sometimes you may want to change the error while preserving the message:

go
func GetUserSecurely(id string) (*User, error) {
    user, err := repository.GetUser(id)
    if err != nil {
        if errors.Ast(err, errors.TypeNotFound) {
            // Convert NotFound to Forbidden for security reasons
            return nil, errors.New(errors.TypeForbidden, errors.CodeAccessDenied, "access denied to requested resource")
        }
        return nil, err
    }
    return user, nil
}

Why do we need this?

In a large codebase like SigNoz, error handling is critical for maintaining reliability, debuggability, and a good user experience. We believe that it is the responsibility of a function to return well-defined errors that accurately describe what went wrong. With our structured error system:

  • Functions can create precise errors with appropriate additional context
  • Callers can make informed decisions based on the additional context
  • Error context is preserved and enhanced as it moves up the call stack

The caller (which can be another function or a HTTP/gRPC handler or something else entirely), can then choose to use this error to take appropriate actions such as:

  • A function can branch into different paths based on the context
  • An HTTP/gRPC handler can derive the correct status code and message from the error and send it to the client
  • Logging systems can capture structured error information for better diagnostics

Although there might be cases where this might seem too verbose, it makes the code more maintainable and consistent. A little verbose code is better than clever code that doesn't provide enough context.

What should I remember?

  • Think about error handling as you write your code, not as an afterthought.
  • Always use the errors package instead of the standard library's errors.New() or fmt.Errorf().
  • Always assign appropriate codes to errors when creating them instead of using the "catch all" error codes defined in pkg/errors/code.go.
  • Use errors.Wrapf() to add context to errors while preserving the original when appropriate.

File: docs/contributing/go/flagger.md

Flagger

Flagger is SigNoz's feature flagging system built on top of the OpenFeature standard. It provides a unified interface for evaluating feature flags across the application, allowing features to be enabled, disabled, or configured dynamically without code changes.

πŸ’‘ Note: OpenFeature is a CNCF project that provides a vendor-agnostic feature flagging API, making it easy to switch providers without changing application code.

How does it work?

Flagger consists of three main components:

  1. Registry (pkg/flagger/registry.go) - Contains all available feature flags with their metadata and default values
  2. Flagger (pkg/flagger/flagger.go) - The consumer-facing interface for evaluating feature flags
  3. Providers (pkg/flagger/flagger/) - Implementations that supply feature flag values (e.g., configflagger for config-based flags)

The evaluation flow works as follows:

  1. The caller requests a feature flag value via the Flagger interface
  2. Flagger checks the registry to validate the flag exists and get its default value
  3. Each registered provider is queried for an override value
  4. If a provider returns a value different from the default, that value is returned
  5. Otherwise, the default value from the registry is returned

How to add a new feature flag?

1. Register the flag in the registry

Add your feature flag definition in pkg/flagger/registry.go:

go
var (
    // Export the feature name for use in evaluations
    FeatureMyNewFeature = featuretypes.MustNewName("my_new_feature")
)

func MustNewRegistry() featuretypes.Registry {
    registry, err := featuretypes.NewRegistry(
        // ...existing features...
        &featuretypes.Feature{
            Name:           FeatureMyNewFeature,
            Kind:           featuretypes.KindBoolean, // or KindString, KindFloat, KindInt, KindObject
            Stage:          featuretypes.StageStable, // or StageAlpha, StageBeta
            DefaultVariant: featuretypes.MustNewName("disabled"),
            Variants:       featuretypes.NewBooleanVariants(),
        },
    )
    // ...
}

πŸ’‘ Note: Feature names must match the regex ^[a-z_]+$ (lowercase letters and underscores only).

2. Configure the feature flag value (optional)

To override the default value, add an entry in your configuration file:

yaml
flagger:
  config:
    boolean:
      my_new_feature: true

Supported configuration types:

Type Config Key Go Type
Boolean boolean bool
String string string
Float float float64
Integer integer int64
Object object any

How to evaluate a feature flag?

Use the Flagger interface to evaluate feature flags. The interface provides typed methods for each value type:

go
import (
    "github.com/SigNoz/signoz/pkg/flagger"
    "github.com/SigNoz/signoz/pkg/types/featuretypes"
)

func DoSomething(ctx context.Context, flagger flagger.Flagger) error {
    // Create an evaluation context (typically with org ID)
    evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
    
    // Evaluate with error handling
    enabled, err := flagger.Boolean(ctx, flagger.FeatureMyNewFeature, evalCtx)
    if err != nil {
        return err
    }
    
    if enabled {
        // Feature is enabled
    }
    
    return nil
}

Empty variants

For cases where you want to use a default value on error (and log the error), use the *OrEmpty methods:

go
func DoSomething(ctx context.Context, flagger flagger.Flagger) {
    evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
    
    // Returns false on error and logs the error
    if flagger.BooleanOrEmpty(ctx, flagger.FeatureMyNewFeature, evalCtx) {
        // Feature is enabled
    }
}

Available evaluation methods

Method Return Type Empty Variant Default
Boolean() (bool, error) false
String() (string, error) ""
Float() (float64, error) 0.0
Int() (int64, error) 0
Object() (any, error) struct{}{}

What should I remember?

  • Always define feature flags in the registry (pkg/flagger/registry.go) before using them
  • Use descriptive feature names that clearly indicate what the flag controls
  • Prefer *OrEmpty methods for non-critical features to avoid error handling overhead
  • Export feature name variables (e.g., FeatureMyNewFeature) for type-safe usage across packages
  • Consider the feature's lifecycle stage (Alpha, Beta, Stable) when defining it
  • Providers are evaluated in order; the first non-default value wins

File: docs/contributing/go/handler.md

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",
        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/<name> 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)
}

When you need an ID from claims as a valuer.UUID (for example to pass it to a module), derive it with the Must* constructor instead of NewUUID plus an error check. Claims are validated by the auth middleware, so the conversion cannot fail and the error branch would be dead code:

go
// Good β€” claims are pre-validated, the conversion cannot fail.
orgID := valuer.MustNewUUID(claims.OrgID)

// Avoid β€” the error path is unreachable.
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
    render.Error(rw, err)
    return
}

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",
        Request:             new(types.PostableThing),
        RequestContentType:  "application/json",
        RequestQuery:        new(types.QueryableThing),
        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).
  • RequestQuery:
    • RequestQuery is a Go type that descirbes query url params.
  • RequestExamples: An array of handler.OpenAPIExample that provide concrete request payloads in the generated spec. See 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",
    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
}

`oneOf` with a discriminator

For a sum type whose variants are keyed by a property (e.g. kind), expose the variants via JSONSchemaOneOf() and add a discriminator. Without it, code generators intersect the variants (A & B & C) instead of producing a clean discriminated union (A | B | C).

The parent keeps its JSONSchemaOneOf() (the oneOf itself) and additionally tags it via PrepareJSONSchema with the x-signoz-discriminator extension; signoz.attachDiscriminators then promotes that marker to a real OpenAPI 3 discriminator (and strips the duplicate parent properties) after reflection.

go
// On the parent: expose the oneOf variants...
func (Plugin) JSONSchemaOneOf() []any {
    return []any{FooVariant{}}
}

// ...and tag that same oneOf with the discriminator marker.
func (Plugin) PrepareJSONSchema(s *jsonschema.Schema) error {
    if s.ExtraProperties == nil {
        s.ExtraProperties = map[string]any{}
    }
    s.ExtraProperties["x-signoz-discriminator"] = map[string]any{
        "propertyName": "kind",
        "mapping": map[string]string{
            "signoz/Foo": "#/components/schemas/FooVariant",
        },
    }
    return nil
}

Each variant must declare the discriminator property (kind) and mark it required.

This produces the following in the generated OpenAPI spec:

yaml
Plugin:
  discriminator:
    propertyName: kind
    mapping:
      signoz/Foo: '#/components/schemas/FooVariant'
  oneOf:
  - $ref: '#/components/schemas/FooVariant'
  type: object

Note the discriminator property lives in the variants, not on the parent β€” the parent is only the union.

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.
  • Derive IDs from claims with valuer.MustNewUUID (e.g. claims.OrgID, claims.UserID). Claims are pre-validated by the auth middleware, so use the Must* constructor β€” don't write NewUUID followed by an if err != nil { render.Error(...); return } block.

File: docs/contributing/go/provider.md

Provider

SigNoz is built on the provider pattern, a design approach where code is organized into providers that handle specific application responsibilities. Providers act as adapter components that integrate with external services and deliver required functionality to the application.

πŸ’‘ Note: Coming from a DDD background? Providers are similar (not exactly the same) to adapter/infrastructure services.

How to create a new provider?

To create a new provider, create a directory in the pkg/ directory named after your provider. The provider package consists of four key components:

  • Interface (pkg/<name>/<name>.go): Defines the provider's interface. Other packages should import this interface to use the provider.
  • Config (pkg/<name>/config.go): Contains provider configuration, implementing the factory.Config interface from factory/config.go.
  • Implementation (pkg/<name>/<name>/provider.go): Contains the provider implementation, including a NewProvider function that returns a factory.Provider interface from factory/provider.go.
  • Mock (pkg/<name>/<name>test.go): Provides mocks for the provider, typically used by dependent packages for unit testing.

For example, the prometheus provider delivers a prometheus engine to the application:

  • pkg/prometheus/prometheus.go - Interface definition
  • pkg/prometheus/config.go - Configuration
  • pkg/prometheus/clickhouseprometheus/provider.go - Clickhouse-powered implementation
  • pkg/prometheus/prometheustest/provider.go - Mock implementation

How to wire it up?

The pkg/signoz package contains the inversion of control container responsible for wiring providers. It handles instantiation, configuration, and assembly of providers based on configuration metadata.

πŸ’‘ Note: Coming from a Java background? Providers are similar to Spring beans.

Wiring up a provider involves three steps:

  1. Wiring up the configuration
    Add your config from pkg/<name>/config.go to the pkg/signoz/config.Config struct and in new factories:
go
type Config struct {
    ...
	MyProvider myprovider.Config `mapstructure:"myprovider"`
    ...
}

func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig, ....) (Config, error) {
    ...
	configFactories := []factory.ConfigFactory{
        myprovider.NewConfigFactory(),
	}
    ...
}
  1. Wiring up the provider
    Add available provider implementations in pkg/signoz/provider.go:
go
func NewMyProviderFactories() factory.NamedMap[factory.ProviderFactory[myprovider.MyProvider, myprovider.Config]] {
	return factory.MustNewNamedMap(
        myproviderone.NewFactory(),
        myprovidertwo.NewFactory(),
	)
}
  1. Instantiate the provider by adding it to the SigNoz struct in pkg/signoz/signoz.go:
go
type SigNoz struct {
    ...
    MyProvider myprovider.MyProvider
    ...
}

func New(...) (*SigNoz, error) {
    ...
    myprovider, err := myproviderone.New(ctx, settings, config.MyProvider, "one/two")
    if err != nil {
        return nil, err
    }
    ...
}

How to use it?

To use a provider, import its interface. For example, to use the prometheus provider, import pkg/prometheus/prometheus.go:

go
import "github.com/SigNoz/signoz/pkg/prometheus/prometheus"

func CreateSomething(ctx context.Context, prometheus prometheus.Prometheus) {
    ...
    prometheus.DoSomething()
    ...
}

Why do we need this?

Like any dependency injection framework, providers decouple the codebase from implementation details. This is especially valuable in SigNoz's large codebase, where we need to swap implementations without changing dependent code. The provider pattern offers several benefits apart from the obvious one of decoupling:

  • Configuration is defined with each provider and centralized in one place, making it easier to understand and manage through various methods (environment variables, config files, etc.)
  • Provider mocking is straightforward for unit testing, with a consistent pattern for locating mocks
  • Multiple implementations of the same provider are supported, as demonstrated by our sqlstore provider

What should I remember?

  • Use the provider pattern wherever applicable.
  • Always create a provider irrespective of the number of implementations. This makes it easier to add new implementations in the future.

File: docs/contributing/go/readme.md

Go

This document provides an overview of contributing to the SigNoz backend written in Go. The SigNoz backend is built with Go, focusing on performance, maintainability, and developer experience. We strive for clean, idiomatic code that follows established Go practices while addressing the unique needs of an observability platform.

We adhere to three primary style guides as our foundation:

We recommend (almost enforce) reviewing these guides before contributing to the codebase. They provide valuable insights into writing idiomatic Go code and will help you understand our approach to backend development. In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package:

  • Abstractions - When to introduce new types and intermediate representations
  • Authz - Authorization, roles, and access control
  • Errors - Structured error handling
  • Endpoint - HTTP endpoint patterns
  • Flagger - Feature flag patterns
  • Handler - HTTP handler patterns
  • Provider - Dependency injection and provider patterns
  • Packages - Naming, layout, and conventions for pkg/ packages
  • Service - Managed service lifecycle with factory.Service
  • SQL - Database and SQL patterns
  • Types - Domain types, request/response bodies, and storage rows in pkg/types/

File: docs/contributing/go/sql.md

SQL

SigNoz utilizes a relational database to store metadata including organization information, user data and other settings.

How to use it?

The database interface is defined in SQLStore. SigNoz leverages the Bun ORM to interact with the underlying database. To access the database instance, use the BunDBCtx function. For operations that require transactions across multiple database operations, use the RunInTxCtx function. This function embeds a transaction in the context, which propagates through various functions in the callback.

go
type Thing struct {
	bun.BaseModel

	ID            types.Identifiable  `bun:",embed"`
	SomeColumn    string              `bun:"some_column"`
	TimeAuditable types.TimeAuditable `bun:",embed"`
	OrgID         string              `bun:"org_id"`
}

func GetThing(ctx context.Context, id string) (*Thing, error) {
	thing := new(Thing)
	err := sqlstore.
        BunDBCtx(ctx).
        NewSelect().
        Model(thing).
        Where("id = ?", id).
        Scan(ctx)

    return thing, err
}

func CreateThing(ctx context.Context, thing *Thing) error {
	return sqlstore.
        BunDBCtx(ctx).
        NewInsert().
        Model(thing).
        Exec(ctx)
}

πŸ’‘ Note: Always use line breaks while working with SQL queries to enhance code readability.

πŸ’‘ Note: Always use the new function to create new instances of structs.

What are hooks?

Hooks are user-defined functions that execute before and/or after specific database operations. These hooks are particularly useful for generating telemetry data such as logs, traces, and metrics, providing visibility into database interactions. Hooks are defined in the SQLStoreHook interface.

How is the schema designed?

SigNoz implements a star schema design with the organizations table as the central entity. All other tables link to the organizations table via foreign key constraints on the org_id column. This design ensures that every entity within the system is either directly or indirectly associated with an organization.

mermaid
erDiagram
    ORGANIZATIONS {
        string id PK
        timestamp created_at
        timestamp updated_at
    }
    ENTITY_A {
        string id PK
        timestamp created_at
        timestamp updated_at
        string org_id FK
    }
    ENTITY_B {
        string id PK
        timestamp created_at
        timestamp updated_at
        string org_id FK
    }
    
    ORGANIZATIONS ||--o{ ENTITY_A : contains
    ORGANIZATIONS ||--o{ ENTITY_B : contains

πŸ’‘ Note: There are rare exceptions to the above star schema design. Consult with the maintainers before deviating from the above design.

All tables follow a consistent primary key pattern using a id column (referenced by the types.Identifiable struct) and include created_at and updated_at columns (referenced by the types.TimeAuditable struct) for audit purposes.

How to write migrations?

For schema migrations, use the SQLMigration interface and write the migration in the same package. When creating migrations, adhere to these guidelines:

  • Do not implement ON CASCADE foreign key constraints. Deletion operations should be handled explicitly in application logic rather than delegated to the database.
  • Do not import types from the types package in the sqlmigration package. Instead, define the required types within the migration package itself. This practice ensures migration stability as the core types evolve over time.
  • Do not implement Down migrations. As the codebase matures, we may introduce this capability, but for now, the Down function should remain empty.
  • Always write idempotent migrations. This means that if the migration is run multiple times, it should not cause an error.
  • A migration which is dependent on the underlying dialect (sqlite, postgres, etc) should be written as part of the SQLDialect interface. The implementation needs to go in the dialect specific package of the respective database.

What should I remember?

  • Use BunDBCtx and RunInTxCtx to access the database instance and execute transactions respectively.
  • While designing new tables, ensure the consistency of id, created_at, updated_at and an org_id column with a foreign key constraint to the organizations table (unless the table serves as a transitive entity not directly associated with an organization but indirectly associated with one).
  • Implement deletion logic in the application rather than relying on cascading deletes in the database.
  • While writing migrations, adhere to the guidelines mentioned above.