Contributing to SigNoz
Single authoritative reference — architecture, dev environment, Go standards, frontend patterns, and testing. Everything in one place.
System Architecture Overview
SigNoz is an OpenTelemetry-native observability platform. Telemetry flows from instrumented apps → OTel Collector → ClickHouse → Go backend → React frontend. All backend services are packaged as a single binary.
Core Components
Frontend Architecture
React 18 TypeScript SPA built with Webpack. Ant Design components, Redux for global state, React Query for server data. All pages lazy-loaded. RBAC enforced at route and component level.
Backend Architecture
Single Go 1.24 binary. API server exposes :8080 (public) and :8085 (private). The provider pattern is used throughout — each subsystem is a typed interface with pluggable implementations wired via pkg/signoz.
Data Flow Pipeline
Repository Structure
signoz/
├── frontend/ # React 18 TypeScript SPA
│ ├── src/
│ │ ├── AppRoutes/ # Routes, PrivateRoute, RBAC guards
│ │ ├── api/ # API client functions
│ │ ├── components/ # Reusable UI components
│ │ ├── container/ # AppLayout, SideNav, TopNav
│ │ ├── providers/ # Context: App, Dashboard, QueryBuilder
│ │ └── store/ # Redux store
│ └── src/container/OnboardingV2Container/onboarding-configs/
│ └── onboarding-config-with-links.json # New Source / Get Started config
│
├── pkg/query-service/ # OSS Go backend
│ └── app/
│ ├── clickhouseReader/ # All ClickHouse reads (interfaces.Reader)
│ ├── http_handler.go # HTTP route handlers
│ └── server.go # Server struct, :8080/:8085
│
├── pkg/ # Shared Go packages (OSS)
│ ├── apiserver/signozapiserver/ # Route registration, OpenAPI defs
│ ├── errors/ # Structured error package
│ ├── factory/ # Provider + Service lifecycle
│ ├── flagger/ # Feature flag system (OpenFeature)
│ ├── modules/ # Business logic modules (user, session, org)
│ ├── signoz/ # IoC container — wires all providers
│ ├── sqlstore/ # SQL abstraction (Bun ORM, PostgreSQL/SQLite)
│ └── types/ # Shared domain types
│
├── ee/query-service/ # Enterprise-only features (never import from pkg/)
├── .devenv/docker/clickhouse/ # Local dev ClickHouse compose
├── deploy/docker/ # Docker Compose (standalone + HA)
├── tests/integration/ # Python/pytest integration tests
├── docs/api/openapi.yml # Auto-generated OpenAPI spec
├── docs/contributing/ # This guide's source files
└── go.mod # Go 1.24.0, 300+ deps
Development Environment Setup
SigNoz has three main components you need to run locally: ClickHouse, Backend, and Frontend. Run make help to see all available Make commands.
Prerequisites
| Tool | Version | Purpose |
|---|---|---|
Go | see go.mod | Backend development |
Node.js | see frontend/.nvmrc | Frontend development |
Yarn | 1.x classic | Frontend package manager |
Docker + Compose | 20.10+ / v2+ | ClickHouse + Postgres locally |
git clone https://github.com/SigNoz/signoz.git && cd signoz
1 — Start ClickHouse
# Starts ClickHouse single-shard single-replica + Zookeeper + schema migrations
make devenv-clickhouse
# Start OTel Collector (listens :4317 gRPC, :4318 HTTP)
make devenv-signoz-otel-collector
# Or start both at once
make devenv-up
# Verify
curl http://localhost:8123/ping # → Ok.
curl http://localhost:13133 # → OTel Collector health
2 — Run the Backend
# OSS build (runs on :8080)
make go-run-community
# Verify
curl http://localhost:8080/api/v1/health # → {"status":"ok"}
3 — Run the Frontend
cd frontend
yarn install
# Create .env
echo "VITE_FRONTEND_API_ENDPOINT=http://localhost:8080" > .env
yarn dev # starts on :3301 with HMR, auto-rebuilds on change
Sending Test Data
# OTLP gRPC: localhost:4317 | OTLP HTTP: localhost:4318
curl -X POST http://localhost:4318/v1/traces \
-H "Content-Type: application/json" \
-d '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"test-service"}}]},"scopeSpans":[{"spans":[{"traceId":"12345678901234567890123456789012","spanId":"1234567890123456","name":"test-span","startTimeUnixNano":"1609459200000000000","endTimeUnixNano":"1609459201000000000"}]}]}]}'
Deployment Options
| Mode | Config | Use Case | HA |
|---|---|---|---|
| Docker Compose | deploy/docker/docker-compose.yaml | Single-node / dev | ❌ |
| Docker Compose HA | deploy/docker/docker-compose.ha.yaml | 3-node CH + 3-node ZK | ✅ |
| Docker Swarm | deploy/docker-swarm/docker-compose.yaml | Multi-node orchestrated | Partial |
| Kubernetes | Helm charts (SigNoz/charts) | Cloud-native | ✅ |
| Quick install | deploy/install.sh | One-command Linux/macOS | ❌ |
Go Backend — Contributing Standards
Packages
All shared Go code lives under pkg/. Each package represents a distinct domain concept with a clear public interface.
| Rule | Detail |
|---|---|
| Naming | Short, lowercase, single-word: querier, authz, cache. No underscores or camelCase. Domain-specific — not generic (util, helpers, common are banned). |
| When to create | Distinct domain concept used by 2+ other packages. Do NOT create for code used in only one place — keep it local. |
| Interface file | File matching the package name (e.g. cache.go in pkg/cache/) defines the public interface. Keep implementation details out. |
| Implementations | Put each implementation in its own sub-package: memorycache/, rediscache/. |
| Test helpers | Put in {pkg}test/ sub-package (e.g. cachetest/). Never pollute the main package with test-only code. |
| Circular imports | Never. Extract shared types into pkg/types/. |
| OSS vs EE | OSS code → pkg/. Enterprise-only → ee/. Never import ee/ from pkg/. |
Import Groups
import (
// 1. Standard library
"fmt"
"net/http"
// 2. External dependencies
"github.com/gorilla/mux"
// 3. Internal
"github.com/SigNoz/signoz/pkg/errors"
)
Abstractions
Every exported type, interface, or wrapper is a permanent commitment. Before introducing one, answer four questions in your PR description:
- What already exists? Name the specific type/function that covers this today.
- What does the new abstraction add? Name the concrete capability. "Cleaner" or "more reusable" are insufficient.
- What does it drop? List what it cannot represent. Every gap must be justified or error-handled.
- Who consumes it? List all call sites. One producer + one consumer = you need a function, not a type.
| Rule | Detail |
|---|---|
| Functions over types | If it has one input and one output, write a function — not a struct. func ConvertConfig(src ExternalConfig) (InternalConfig, error) beats a ConfigAdapter struct. |
| Don't duplicate external types | Operate on library output directly. A partial copy silently loses data, drifts, and doubles code surface. |
| Never silently discard input | Unrecognised enum values, missing cases — always return an error. default: return nil, fmt.Errorf("unsupported %T: %v", v, v) |
| No lossy methods | Don't add methods that strip meaning. Write standalone functions that operate on the full struct instead. |
| Discover interfaces | Never define an interface before you have ≥2 concrete implementations. Exception: testing mocks, which go in the consuming package. |
| Wrappers must add semantics | A wrapper is justified only when it adds invariants/validation the underlying type doesn't carry. Ask: what does it guarantee? If nothing — don't wrap. |
When a new type IS warranted
- Serialization boundary — must be persisted, sent over wire, or written to config.
- Invariant enforcement — constructor/methods enforce constraints raw data doesn't carry.
- Multiple distinct consumers — 3+ call sites use it in meaningfully different ways.
- Dependency firewall — lives in lightweight package so consumers avoid heavy import.
Errors
SigNoz has its own structured error package at pkg/errors. Always use it instead of the standard library.
// Instead of errors.New() or fmt.Errorf():
errors.New(typ, code, message)
errors.Newf(typ, code, message, args...)
errors.Wrapf(err, typ, code, message) // adds context, preserves original
| Concept | Detail |
|---|---|
| Typ | Categorises errors, loosely coupled to HTTP/gRPC status codes. Defined in pkg/errors/type.go: TypeInvalidInput, TypeNotFound, TypeForbidden, etc. Cannot be declared outside the package. |
| Code | Granular categorisation within a type. Create with errors.MustNewCode("thing_not_found"). Must match ^[a-z_]+$ or panics. Always assign specific codes — avoid catch-all codes. |
| Wrapping | Use errors.Wrapf to add context while preserving the original error as it travels up the call stack. |
| Checking | Use errors.As(err, errors.TypeNotFound) to branch on error type. Handlers derive HTTP status codes from the type automatically. |
Provider Pattern
Providers are the dependency injection mechanism. They adapt external services and deliver functionality to the application. Think of them as typed, configurable adapters wired together in pkg/signoz.
Anatomy of a provider
| File | Purpose |
|---|---|
pkg/<name>/<name>.go | Public interface — other packages import this. |
pkg/<name>/config.go | Configuration, implements factory.Config. |
pkg/<name>/<impl><name>/provider.go | Concrete implementation with NewProvider returning factory.Provider. |
pkg/<name>/<name>test.go | Mock for use in unit tests by dependent packages. |
Wiring (3 steps in pkg/signoz)
// 1. Add config to pkg/signoz/config.go
type Config struct {
MyProvider myprovider.Config `mapstructure:"myprovider"`
}
// 2. Register factory in pkg/signoz/provider.go
func NewMyProviderFactories() factory.NamedMap[...] {
return factory.MustNewNamedMap(myproviderone.NewFactory())
}
// 3. Instantiate in pkg/signoz/signoz.go SigNoz struct
myprovider, err := myproviderone.New(ctx, settings, config.MyProvider, "one/two")
Service Lifecycle
A service is a component with a managed lifecycle: it starts, runs for the application's lifetime, and stops gracefully. Services are distinct from providers — a provider adapts an external dependency; a service has its own lifecycle tied to the application.
The factory.Service interface
type Service interface {
Start(context.Context) error // MUST BLOCK until stopped or error
Stop(context.Context) error // causes Start to unblock
}
Shutdown coordination — always use stopC
// Constructor
stopC: make(chan struct{})
// Start: block
<-provider.stopC
// Stop: unblock
close(provider.stopC)
| Shape | Pattern | Example |
|---|---|---|
| Idle | Start blocks on <-stopC. Stop closes channel, optionally cleans up. | JWT tokenizer — responds to calls, no periodic work |
| Scheduled | Start runs a ticker loop with select { case <-stopC / case <-ticker.C }. Errors logged, not returned (would kill app). | Opaque tokenizer GC — periodic gc + flush |
Wiring to the registry
registry, err := factory.NewRegistry(
instrumentation.Logger(),
factory.NewNamedService(factory.MustNewName("myservice"), myService),
// ... more services
)
// Registry calls Start on all services concurrently.
// Any Start error triggers application shutdown.
// Stop is called on all services concurrently at shutdown.
Start if the failure is unrecoverable. Log and continue for transient errors in polling loops — returning an error shuts down the entire application.
HTTP Handlers
Handlers are thin adapters — they decode HTTP requests, call the appropriate module, and return structured responses. Business logic belongs in modules (pkg/modules/user, pkg/modules/session, etc.), not in handlers.
Typical handler implementation
func (h *handler) CreateThing(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil { render.Error(rw, err); return }
var in types.PostableThing
if err := binding.JSON.BindBody(req.Body, &in); err != nil {
render.Error(rw, err); return
}
out, err := h.module.CreateThing(req.Context(), claims.OrgID, &in)
if err != nil { render.Error(rw, err); return }
render.Success(rw, http.StatusCreated, out)
}
Registering a route in signozapiserver
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),
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
// After adding a new endpoint, regenerate the OpenAPI spec:
// go run cmd/enterprise/*.go generate openapi
OpenAPI schema tags
| Tag | When to use |
|---|---|
required:"true" | The JSON key must be present (even if zero value). Different from non-null. |
nullable:"true" | The value can be null. Critical for slice/map fields — Go's nil serializes to null. Mark nullable if Go code can return nil; otherwise initialize to empty slice. |
Enum() []any | Implement on any type with a fixed set of values. Generates enum constraint in the spec. Required for all enum types. |
JSONSchema() | Implement jsonschema.Exposer for types needing a completely custom schema (e.g. accepts string OR number). |
Endpoint Design
Think of endpoint structure like navigating a file system. Resources should be pluralized and hierarchical.
POST /v1/organizations # Create
GET /v1/organizations/:id # Get by ID
PUT /v1/organizations/:id # Update
DELETE /v1/organizations/:id # Delete
GET /v1/organizations/:id/users # Nested resource
GET /v1/organizations/me/users # me = resolved via auth context
me endpoints are symlinks — they resolve to the authenticated user's actual org/resource via the auth mechanism. When in doubt, diagram the resource relationships as a file system tree before coding.
Feature Flags (Flagger)
Flagger is built on OpenFeature (CNCF). Three components: Registry (pkg/flagger/registry.go) — all available flags and defaults. Flagger (pkg/flagger/flagger.go) — consumer interface. Providers (pkg/flagger/<provider>flagger/) — supply override values.
Adding a new flag (2 steps)
// Step 1: Register in pkg/flagger/registry.go
var FeatureMyNewFeature = featuretypes.MustNewName("my_new_feature")
// Add to MustNewRegistry():
&featuretypes.Feature{
Name: FeatureMyNewFeature,
Kind: featuretypes.KindBoolean,
Stage: featuretypes.StageStable,
Description: "Controls whether my new feature is enabled",
DefaultVariant: featuretypes.MustNewName("disabled"),
Variants: featuretypes.NewBooleanVariants(),
}
// Step 2 (optional): Override in config
// flagger.config.boolean.my_new_feature: true
Evaluating flags
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
// With error handling
enabled, err := flagger.Boolean(ctx, flagger.FeatureMyNewFeature, evalCtx)
// Non-critical — returns false on error and logs
if flagger.BooleanOrEmpty(ctx, flagger.FeatureMyNewFeature, evalCtx) { ... }
^[a-z_]+$. Export the name variable (e.g. FeatureMyNewFeature) for type-safe usage across packages. Providers are evaluated in order; the first non-default value wins.
SQL & Database Patterns
SigNoz uses Bun ORM (pkg/sqlstore) against PostgreSQL or SQLite. The schema follows a star schema with organizations as the central entity — all other tables link to it via org_id foreign key.
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"`
}
// Read
err := sqlstore.BunDBCtx(ctx).NewSelect().Model(thing).Where("id = ?", id).Scan(ctx)
// Transactions across multiple operations
err := sqlstore.RunInTxCtx(ctx, func(ctx context.Context) error { ... })
Migration rules
- Never use
ON CASCADE— handle deletion in application logic. - Never import from
pkg/typesinside migrations — define local types so migrations stay stable as types evolve. - No
Downmigrations for now — leave the function empty. - Always write idempotent migrations.
- Dialect-specific migrations go in the
SQLDialectinterface, not inline. - All tables must have
id,created_at,updated_at, andorg_id(with FK toorganizations), unless it's a transitive entity.
Query Range v5 — Design Principles
The Central Type: TelemetryFieldKey
Every filter, aggregation, group-by, order-by, and select is expressed in terms of TelemetryFieldKey. It is identified by three dimensions: Name, FieldContext (resource / attribute / span / log / body), and FieldDataType.
The Abstraction Stack (must never be bypassed)
StatementBuilder ← orchestrates into executable SQL
├── AggExprRewriter ← rewrites aggregation expressions
├── ConditionBuilder ← builds WHERE predicates
└── FieldMapper ← TelemetryFieldKey → ClickHouse column
Inviolable Rules
1User-facing types never contain ClickHouse column names or SQL fragments.
2Field-to-column translation happens only in FieldMapper.
3Normalization happens once at the API boundary (TelemetryFieldKey.Normalize() during JSON unmarshal). Never re-parse or re-normalize downstream.
4Historical aliases in fieldContexts (tag→attribute, spanfield→span) must never be removed — existing saved queries depend on them.
5Formula evaluation (A + B, A / B) stays in Go — do not push into ClickHouse JOINs.
6Zero-defaulting is aggregation-type-dependent. Only additive aggregations (count, sum, rate) default to zero. Statistical aggregations (avg, min, max, percentiles) must show gaps.
7Positive operators (=, IN) implicitly assert field existence. Negative operators (!=, NOT IN) do not — they include records where the field doesn't exist.
8Post-processing functions (ewma, fillZero, timeShift, etc.) operate on Go result sets, not in SQL.
9All user-facing types reject unknown JSON fields with Levenshtein-based suggestions. Implement custom UnmarshalJSON with DisallowUnknownFields.
10Query names must be unique within a composite query. Multi-aggregation refs use indexed (A.0) or aliased (A.total) notation.
11Validation rules are gated by request type (time_series, scalar, raw, etc.).
12The four-layer abstraction stack must not be bypassed or flattened.
Finding Issues to Work On
good first issue — well-scoped. Best entry point.Pull Request Workflow
maingit checkout main && git pull && git checkout -b feat/my-featureOne logical change per PR. Unrelated changes → separate PR.
yarn test && go test ./...No merge conflicts with main. Address CI failures. Respond to review comments within 1–2 business days.
For large contributions, split: (1) Structure — interfaces, types, configs. (2) Core implementation. (3) Docs + e2e tests.
Commit Convention
SigNoz follows Conventional Commits.
type(scope): short description
# Optional body (wrap at 72 chars)
Closes #1234
| Type | When | Example |
|---|---|---|
| feat | New feature | feat(logs): add regex filter |
| fix | Bug fix | fix(traces): correct span duration |
| docs | Docs only | docs(contributing): add Go guide |
| refactor | No functional change | refactor(query-service): extract builder |
| test | Tests only | test(clickhouse): add reader unit tests |
| chore | Build, deps, CI | chore(deps): upgrade react-query |
Testing Guide
Frontend
yarn test # all unit tests
yarn test:coverage # with coverage report
yarn test --watch src/components/X # watch mode
Co-locate tests: ComponentName.test.tsx or __tests__/. Use React Testing Library — test behaviour, not implementation.
Backend
go test ./... # all unit tests
go test -v ./pkg/query-service/... # verbose
go test -run TestQueryBuilder ./... # specific test
golangci-lint run ./... # lint
Integration Tests
Python/pytest framework in tests/integration/ runs against real ClickHouse, PostgreSQL, and Zookeeper via testcontainers. Wiremock provides service doubles.
cd tests/integration && uv syncuv run pytest --basetemp=./tmp/ -vv --reuse src/bootstrap/setup.py::test_setupuv run pytest --basetemp=./tmp/ -vv --teardown -s src/bootstrap/setup.py::test_teardownRunning tests
uv run pytest --basetemp=./tmp/ -vv --reuse src/ # all
uv run pytest --basetemp=./tmp/ -vv --reuse src/querier/ # suite
uv run pytest --basetemp=./tmp/ -vv --reuse src/auth/01_register.py::test_register # single
Key rules
- Always use
--reusewhen writing tests — avoids recreating the environment. - Always use
--teardownwhen done — avoids resource leaks. - Prefix new test files with two-digit numbers (
04_,05_) for execution order. - Each suite must complete in under 10 minutes.
- Test both success and failure scenarios. Use descriptive test names.
Frontend Code Standards
| Area | Standard |
|---|---|
| Language | TypeScript strict: true. No implicit any. |
| Components | Functional + hooks only. No new class components. |
| State | Server state → React Query. Global UI → Redux. Feature state → Context API. |
| Styling | Ant Design first. Styled Components for theme overrides. SCSS for legacy pages. |
| Imports | Use TS path aliases. No deep relative imports. |
| Code splitting | All page-level components must be lazy-loaded via Loadable. |
| RBAC | New routes must specify roles in route config. |
| i18n | User-visible strings in public/locales/en/. Use useTranslation. |
Adding a New Data Source (Onboarding)
The onboarding "New Source / Get Started" flow is configured via JSON at frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json.
Required fields per data source object
| Field | Type | Description |
|---|---|---|
dataSource | string | Unique kebab-case identifier (e.g. "aws-ec2") |
label | string | Display name (e.g. "AWS EC2") |
tags | string[] | Category tags: AWS, Azure, database, logs, etc. |
module | string | Destination after onboarding: apm, logs, metrics, dashboards, infra-monitoring-hosts, etc. |
imgUrl | string | Path to SVG logo: "/Logos/ec2.svg" — SVG required, optimize with SVGOMG before committing. |
Checklist before submitting
- SVG logo exists in
public/Logos/and has been optimized (npx svgo public/Logos/your-logo.svg). - Use
internalRedirect: trueonly for internal app routes (/integrations?...), not for docs links. - Keys use kebab-case. Pattern:
{service}-{subtype}-{action}. - Test locally at
http://localhost:3301/get-started-with-signoz-cloud— verify the data source appears, search works, links redirect correctly, and questions show correct help text. - Tags must include at least one value; use existing tags when possible.
relatedSearchKeywords— include name variations, lowercase, alphabetically sorted.
Other Repositories
Each repo has its own CONTRIBUTING.md. Always check it before submitting PRs.
Community & Getting Help
| Channel | Purpose |
|---|---|
#contributing (Slack) | General contribution questions |
#contributing-frontend (Slack) | Frontend-specific help |
| GitHub Discussions | Design proposals, architecture decisions |
| GitHub Issues | Bug reports, feature requests |
| Security Policy | Private vulnerability disclosure only |