OpenDecree integrates with OpenTelemetry for distributed tracing, metrics, and log correlation. All observability features are opt-in -- nothing is enabled by default.
Enable everything with Docker Compose:
# Start the service with OTel Collector and Jaeger.
# The first run builds the server and migration images from source
# (a few minutes); later runs reuse the cached images.
docker compose up -d --wait service otel-collector jaegerThen add OTel environment variables to the service. In docker-compose.yml, add to the service container's environment:
environment:
# ... existing vars ...
OTEL_ENABLED: "true"
OTEL_TRACES_GRPC: "true"
OTEL_TRACES_DB: "true"
OTEL_METRICS_GRPC: "true"
OTEL_METRICS_CONFIG: "true"
OTEL_EXPORTER_OTLP_ENDPOINT: "http://otel-collector:4317"View traces at http://localhost:16686 (Jaeger UI).
OTEL_ENABLED is the master switch. When set to true or 1, it:
- Initializes the OpenTelemetry SDK with an OTLP gRPC exporter
- Creates a
TracerProviderandMeterProvider - Wraps the slog JSON handler to inject
trace_idandspan_idinto every log entry
When OTEL_ENABLED is false (the default), all other OTel flags are ignored and no telemetry overhead is added.
Traces show the full lifecycle of a request across gRPC, database, and Redis.
Creates a span for every incoming gRPC call with:
- RPC method name (e.g.,
centralconfig.v1.ConfigService/GetField) - gRPC status code
- Duration
This is the top-level span that all other spans nest under.
Creates a span for every PostgreSQL query and transaction via pgx instrumentation:
- SQL statement (parameterized)
- Database name
- Duration
Useful for identifying slow queries and understanding how many database calls each RPC makes.
Creates a span for every Redis command:
- Command name (GET, SET, PUBLISH, etc.)
- Duration
Shows cache hits/misses and pub/sub publish latency.
A typical GetField trace looks like:
[gRPC] ConfigService/GetField (2.1ms)
└── [Redis] GET config:tenant:field (0.3ms) ← cache hit
A cache miss adds a database span:
[gRPC] ConfigService/GetField (5.4ms)
├── [Redis] GET config:tenant:field (0.2ms) ← cache miss
├── [PostgreSQL] SELECT ... FROM config_values (3.1ms)
└── [Redis] SET config:tenant:field (0.4ms) ← populate cache
Metrics provide aggregate counters and gauges for monitoring dashboards and alerting.
Standard otelgrpc metrics:
rpc.server.duration-- request latency histogram by method and statusrpc.server.request.size-- request message sizesrpc.server.response.size-- response message sizesrpc.server.requests_per_rpc-- request count by method
Connection pool gauges (reported for both write and read pools):
- Total connections
- Acquired (in-use) connections
- Idle connections
- Max connections
Useful for alerting on pool exhaustion.
config.cache.hits-- counter of cache hitsccs.cache.miss-- counter of cache misses
Monitor your cache hit ratio to tune TTL and capacity.
config.writes-- counter of config write operations (labeled byaction)config.versions-- gauge of the current config version
By default, neither metric carries a tenant_id label. In deployments with many short-lived
tenants, adding tenant_id would create unbounded label cardinality and exhaust the
Prometheus/OTel collector cardinality budget.
To attach tenant_id for a known subset of tenants (useful when debugging noisy activity from
specific tenants), set OTEL_METRICS_TENANT_ALLOWLIST:
OTEL_METRICS_TENANT_ALLOWLIST=tenant-a,tenant-bOnly the listed tenant IDs receive the tenant_id attribute; all other tenants record into the
unlabeled series. The allowlist is additive — removing a tenant from the list does not delete
historical labeled series from your backend.
Trade-off: without tenant_id, you lose per-tenant resolution on write activity. Use the
allowlist sparingly (e.g. during an incident investigation) and remove entries once you are done.
ccs.schema.publishes-- counter of schema publish operations
When OTEL_ENABLED is true, OpenDecree wraps the slog JSON handler to inject trace context into every log entry:
{
"time": "2025-06-15T10:30:00Z",
"level": "INFO",
"msg": "field updated",
"tenant_id": "abc-123",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7"
}This lets you correlate logs with traces in your observability backend -- click a trace in Jaeger and find the corresponding log entries, or search logs by trace ID.
The Docker Compose stack includes an OpenTelemetry Collector that receives telemetry from OpenDecree and exports it to backends. The collector config lives at deploy/otel-collector.yaml.
The default setup exports traces to Jaeger. To export to other backends (Grafana Tempo, Datadog, etc.), modify the collector config.
OpenDecree respects standard OpenTelemetry SDK variables:
| Variable | Default |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4317 |
OTEL_SERVICE_NAME |
decree |
OTEL_RESOURCE_ATTRIBUTES |
-- |
Start with these flags and expand as needed:
OTEL_ENABLED=true
OTEL_TRACES_GRPC=true # request-level visibility
OTEL_METRICS_GRPC=true # latency and error rate
OTEL_METRICS_CONFIG=true # config activity
OTEL_METRICS_DB_POOL=true # pool health
OTEL_METRICS_CACHE=true # cache effectivenessAdd OTEL_TRACES_DB and OTEL_TRACES_REDIS when debugging performance -- they add span overhead per query/command, which may not be needed for routine monitoring.
- Server Configuration -- all OTel environment variables
- Deployment -- Docker Compose with OTel stack