Observability¶
GTB services emit the three OpenTelemetry signals โ traces, metrics and logs โ over OTLP/HTTP to a collector. The plumbing lives as subpackages of pkg/telemetry, over a shared OTel core, and the per-request instrumentation lives beside the request logging in pkg/http and pkg/grpc.
This is distinct from the product analytics in the same package. Analytics is the vendor learning about users and runs on informed consent (opt-in). Observability is the operator instrumenting their own service and runs on implied consent (configuration). The two share the telemetry.* config root and the export core, but never a consent gate. See The two consent models.
Package layout¶
| Package | Role |
|---|---|
pkg/telemetry/otelcore |
Shared core: OTLP endpoint parsing, the service resource, and telemetry.* config resolution. Imports no signal exporters. |
pkg/telemetry/tracing |
TracerProvider over an OTLP trace exporter; parent-based ratio sampler. |
pkg/telemetry/metrics |
MeterProvider over an OTLP metric exporter; periodic push. |
pkg/telemetry/logs |
LoggerProvider over an OTLP log exporter, plus an otelslog bridge handler. |
pkg/telemetry (Setup) |
Builds the enabled providers, installs the OTel globals, registers shutdown on the controller. |
pkg/http, pkg/grpc |
OTelMiddleware / OTelStatsHandler: per-request spans + server metrics, reading the global providers. |
Setup¶
telemetry.Setup is the one call that wires everything from props.Props:
It resolves each signal from config, builds only the enabled providers, installs them as the OTel globals, sets the W3C trace-context + baggage propagators, and โ when a controller is supplied โ registers a telemetry service so the providers flush on graceful stop. Call it in serve, after the controller exists:
controller := controls.NewController(ctx, controls.WithLogger(p.Logger))
if _, err := telemetry.Setup(ctx, p, controller); err != nil {
return err
}
Signals whose telemetry.<signal>.enabled is false are skipped, so an unconfigured service pays nothing.
Transport instrumentation¶
Spans and the standard server metrics come from the OTel contrib libraries, wrapped as one-line helpers that read the global providers Setup installed.
gRPC โ a stats handler passed to Register:
HTTP โ a Chain-compatible middleware. Put it ahead of the logging middleware so the access log can read the active span:
chain := http.NewChain(
http.OTelMiddleware("macguffin"),
http.LoggingMiddleware(p.Logger),
)
http.Register(ctx, "http", controller, p.Config, p.Logger, mux, http.WithMiddleware(chain))
Custom, business-level instrumentation needs no GTB API โ use the OTel globals directly:
Trace correlation in logs¶
When OTelMiddleware / OTelStatsHandler precede the request logging, the logging middleware and interceptor pull the active span from the request context and add trace_id and span_id to the access log. Because they are ordinary log fields, the correlation reaches both the human-readable stderr output (so kubectl logs line up with traces) and any OTLP log records exported via the logs bridge. With no active span the fields are simply absent.
Configuration¶
All under the telemetry.* root, resolved shared-then-per-signal (the same shared-plus-override style as pkg/tls). An empty endpoint is intentional โ it lets the OTel SDK read the standard OTEL_EXPORTER_OTLP_* environment variables.
| Key | Type | Default | Meaning |
|---|---|---|---|
telemetry.endpoint |
string | โ | Shared OTLP/HTTP base URL for all signals. |
telemetry.headers |
map | โ | Shared exporter headers (e.g. an auth token). |
telemetry.insecure |
bool | false |
Shared: plaintext OTLP (local collectors only). |
telemetry.tracing.enabled |
bool | false |
Enable trace export. |
telemetry.tracing.endpoint |
string | shared | Per-signal endpoint override. |
telemetry.tracing.sampling |
float | 0.1 |
Parent-based ratio. Set 1.0 to record every trace in dev. |
telemetry.metrics.enabled |
bool | false |
Enable metric export. |
telemetry.metrics.interval |
duration | 60s |
Periodic export interval. |
telemetry.logs.enabled |
bool | false |
Enable OTLP log export (stderr output is unaffected). |
Per-signal endpoint, headers and insecure override the shared values individually.
The two consent models¶
The defining distinction of this package:
- Informed consent โ analytics.
telemetry.Collectorcollects usage data about the user. It is off by default and runs only on opt-in (telemetry.enabled), withTelemetryConfig.ForceEnabledfor enterprise embedded config. Unchanged by observability. - Implied consent โ observability.
Setupcollects operational data about the service, emitted by the operator to the operator's own collector. It is gated only bytelemetry.<signal>.enabled, never bytelemetry.enabled. Enabling tracing does not enable analytics, and disabling analytics does not disable tracing. There is no consent prompt, no machine-ID hashing and no GDPR deletion flow on this path โ those are analytics concerns.
The principle: the kind of data decides the consent model. Personal/usage data needs informed consent; operational data runs on implied consent. The CLI and the web service are the canonical homes of each.
Lifecycle¶
Each provider batches and exports asynchronously, and flushes its buffer on Shutdown. Setup registers that shutdown as a controller service, so a SIGTERM drains telemetry in the same graceful window that drains in-flight requests โ no dropped spans on a clean stop. Callers without a controller get a Shutdown return value to defer themselves.