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
SigNoz Cloud (Recommended)
Fully managed SigNoz with a 30-day free trial, no credit card required, usage-based pricing that starts at $49, and regional data hosting.
Enterprise
Enterprise Cloud, BYOC, or Enterprise Self-Hosted with compliance, support, custom retention, RBAC, ingestion controls, data residency, and region selection.
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.
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
- OpenTelemetry-native
Instrument once with open standards and keep ownership of your telemetry. - Correlated signals
Move from service charts to traces, logs, infra metrics, and exceptions without switching tools. - Single columnar database
Built for high-cardinality, high-volume observability workloads. - Predictable pricing
No per-host pricing, no user-seat pricing, and no special pricing for custom metrics. - 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.
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.shscript and thedocker-composemanifests 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 organizationGET /v1/organizations/:idβ Get an organization by idDELETE /v1/organizations/:idβ Delete an organization by idPUT /v1/organizations/:idβ Update an organization by idGET /v1/organizations/:id/usersβ Get all users in an organizationGET /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.
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:
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
meendpoints 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:
// 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 providedTypeNotFound- 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:
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.
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.
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.
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 adid you mean: `x`correction (when a close typo match exists) followed by the valid-references list.NewValidReferences(noun, values...)β formats a capped list asvalid <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-elementdid 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.
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:
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:
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()orfmt.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:
- Registry (
pkg/flagger/registry.go) - Contains all available feature flags with their metadata and default values - Flagger (
pkg/flagger/flagger.go) - The consumer-facing interface for evaluating feature flags - Providers (
pkg/flagger/flagger/) - Implementations that supply feature flag values (e.g.,configflaggerfor config-based flags)
The evaluation flow works as follows:
- The caller requests a feature flag value via the
Flaggerinterface - Flagger checks the registry to validate the flag exists and get its default value
- Each registered provider is queried for an override value
- If a provider returns a value different from the default, that value is returned
- 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:
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:
flagger:
config:
boolean:
my_new_feature: trueSupported 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:
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:
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
*OrEmptymethods 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:
- A
Handlerinterface is defined in the module (for example,user.Handler,session.Handler,organization.Handler). - The
apiserverprovider wires those handlers into HTTP routes using Gorillamux.Router.
Each route wraps a module handler method with the following:
- Authorization middleware (from
pkg/http/middleware) - A generic HTTP
handler.Handler(frompkg/http/handler) - An
OpenAPIDefthat describes the operation for OpenAPI generation
For example, in pkg/apiserver/signozapiserver:
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.CreateInviteis a handler method.provider.authZ.AdminAccess(...)wraps that method with authorization checks and context setup.handler.Newconverts it into an HTTP handler and wires it to OpenAPI via theOpenAPIDef.
How to write a new handler method?
When adding a new endpoint:
- Add a method to the appropriate module
Handlerinterface. - Implement that method in the module.
- Register the method in
signozapiserverwith the correct route, HTTP method, auth, andOpenAPIDef.
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:
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 thebindingpackage - Calls module functions
- Uses the
renderpackage to write responses or errors
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:
// 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:
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:
go run cmd/enterprise/*.go generate openapiThis 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:
Requestis a Go type that describes the request body or form.RequestContentTypeis usually"application/json"or"application/x-www-form-urlencoded"(for callbacks like SAML).
- RequestQuery:
RequestQueryis a Go type that descirbes query url params.
- RequestExamples: An array of
handler.OpenAPIExamplethat provide concrete request payloads in the generated spec. See Adding request examples below. - Response / ResponseContentType:
Responseis the Go type for the successful response payload.ResponseContentTypeis 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, and500toErrorStatusCodeswhen 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:
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:
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.
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 benull. Use this when the Go code may returnnilfor the slice. - Non-nullable list (no
nullabletag): the field is always an array, nevernull. Ensure the Go code initializes it to an empty slice (e.g.make([]T, 0)) before serializing.
// 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.
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:
Signal:
enum:
- traces
- logs
- metrics
type: stringEvery 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:
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.
// 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:
Plugin:
discriminator:
propertyName: kind
mapping:
signoz/Foo: '#/components/schemas/FooVariant'
oneOf:
- $ref: '#/components/schemas/FooVariant'
type: objectNote 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
signozapiserverusinghandler.Newand a completeOpenAPIDef. - Choose accurate request/response types from the
typespackages 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 benull. Pay special attention to slices and maps -- in Go these default tonilwhich serializes tonull. 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 properenumconstraints. - Add request examples via
RequestExamplesinOpenAPIDeffor any non-trivial endpoint. Seepkg/apiserver/signozapiserver/querier.gofor reference. - Derive IDs from
claimswithvaluer.MustNewUUID(e.g.claims.OrgID,claims.UserID). Claims are pre-validated by the auth middleware, so use theMust*constructor β don't writeNewUUIDfollowed by anif 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 thefactory.Configinterface from factory/config.go. - Implementation (
pkg/<name>/<name>/provider.go): Contains the provider implementation, including aNewProviderfunction that returns afactory.Providerinterface 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 definitionpkg/prometheus/config.go- Configurationpkg/prometheus/clickhouseprometheus/provider.go- Clickhouse-powered implementationpkg/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:
- Wiring up the configuration
Add your config frompkg/<name>/config.goto thepkg/signoz/config.Configstruct and in new factories:
type Config struct {
...
MyProvider myprovider.Config `mapstructure:"myprovider"`
...
}
func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig, ....) (Config, error) {
...
configFactories := []factory.ConfigFactory{
myprovider.NewConfigFactory(),
}
...
}- Wiring up the provider
Add available provider implementations inpkg/signoz/provider.go:
func NewMyProviderFactories() factory.NamedMap[factory.ProviderFactory[myprovider.MyProvider, myprovider.Config]] {
return factory.MustNewNamedMap(
myproviderone.NewFactory(),
myprovidertwo.NewFactory(),
)
}- Instantiate the provider by adding it to the
SigNozstruct inpkg/signoz/signoz.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:
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:
- Effective Go - For writing idiomatic Go code
- Code Review Comments - For understanding common comments in code reviews
- Google Style Guide - Additional practices from Google
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.
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
newfunction 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.
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 CASCADEforeign 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
sqlmigrationpackage. 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
Downmigrations. As the codebase matures, we may introduce this capability, but for now, theDownfunction 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
BunDBCtxandRunInTxCtxto access the database instance and execute transactions respectively. - While designing new tables, ensure the consistency of
id,created_at,updated_atand anorg_idcolumn with a foreign key constraint to theorganizationstable (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.