Overview
In gpsystem, logging, measurement and error reporting are all booted by a single call, and you don't even write that call: the first thing fiberx.Run, chix.Run and worker.Run do is telemetry.Setup. By the time the shop's main.go starts serving, logging, tracing and error reporting are already wired, before the first request arrives.
The telemetry facade
Three packages each own one capability, and a fourth composes them:
| Package | What it owns | Page |
|---|---|---|
logx (+ logx/monolog) | the default slog logger's composition: console format, level filtering, fanout | Logging |
otelx | the OpenTelemetry SDK bootstrap: traces, metrics, OTLP log bridge | OpenTelemetry |
sentryx | the optional Sentry integration: span exporter + slog capture handler | Sentry |
telemetry | the facade the chassis calls (composes the other three in a single Setup) | this page |
telemetry.Config is exactly what the server.Config.Telemetry and worker.Config.Telemetry fields hold:
type Config struct {
Otel otelx.Config // OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION, OTEL_DEV_MODE
Log logx.Config // LOG_LEVEL, LOG_STDOUT, LOG_FORMAT
Sentry sentryx.Config // SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_TRACES, SENTRY_DEBUG
}
It embeds without a prefix, so the environment variables load under their standard names: a deployment configures the service exactly like any other OTel-instrumented app. The full variable list: Configuration reference.
func Setup(ctx context.Context, cfg Config) (shutdown func(context.Context) error, err error)
telemetry is the only package that imports all three building blocks: otelx and sentryx know nothing about each other, and logx has no opinion on where else a log record goes. This is not cosmetics: a non-chassis binary (a CLI, a migrator) that only uses otelx never links sentry-go.
What happens inside Setup
The order matters, because the pieces wire into each other:
- Sentry first.
sentryx.Setupruns first: withoutSENTRY_DSNevery hook of it is a no-op, and everything behaves exactly as if Sentry did not exist. - OTel, with Sentry's span exporter.
otelx.Setupreceives Sentry's exporter via theotelx.WithSpanExporter(sentryHandle.SpanExporter())option: the same spans go to Sentry through an extra batcher, so an error links to the full request trace. - One
slog.Default(). The console handler (always in dev, in prod only withLOG_STDOUT=true), the OTLP log bridge and the Sentry capture handler are combined into a single handler withlogx.Fanout, alogx.LevelFilter(LOG_LEVEL, ...)goes in front, and the whole thing becomes the process's default logger.
So a single slog.InfoContext(ctx, ...) call in your service is, at once: a readable console line (dev), a trace-correlated log record exported over OTLP (prod), and, on error, a Sentry event with a breadcrumb trail.
What you get with zero configuration
Dev (OTEL_DEV_MODE=true) | Prod (default) | |
|---|---|---|
| Console log | always, readable text (or Monolog format) | only with LOG_STDOUT=true, JSON |
| Traces / metrics | no exporters, no collector needed | OTLP per the standard OTEL_* variables |
| OTLP log bridge | none | present (logs ship to the collector too) |
| Sentry | off (SENTRY_DSN empty) | off (SENTRY_DSN empty) |
OTEL_SERVICE_NAME | optional | required (Setup fails without it) |
OTEL_DEV_MODE=true. The service boots with readable logs and a request logger, no infrastructure required. Add LOG_FORMAT=monolog and the console prints Monolog-style lines familiar from PHP, with clickable stack traces.There is one exception: if you set SENTRY_DSN in dev mode, the kit builds a Sentry-only tracer provider, so you get full traces and errors in Sentry locally, without a collector; details on the Sentry page.
The flush runs last
Setup returns an aggregated shutdown function that the chassis runs as the Flush step of the application lifecycle, last of all, after the HTTP drain and after all your WithCloser closers. That ordering is deliberate: closers themselves emit telemetry (the log line of a pool close, a span opened during cleanup), and those only make it out if the exporters are still alive.
There is ordering inside the shutdown too: the OTel providers stop first, and Sentry flushes last, so events produced while the providers were shutting down still get out.
If you don't want it, or want to own it
All three chassis have an off switch:
fiberx.Run(ctx, cfg.Server, register, fiberx.WithoutTelemetry())
chix.Run(ctx, cfg.Server, register, chix.WithoutTelemetry())
worker.Run(ctx, cfg.Worker, register, worker.WithoutTelemetry())
WithoutTelemetry skips telemetry.Setup and the HTTP-side OTel middleware. In tests this is the default posture (no global state fiddling); in production you use it when the process configures its own telemetry: for example, to fit into a pre-existing OTel setup.
In a non-chassis process (a CLI, a one-shot cron-like run) telemetry.Setup can be called directly: you get the same composition, you just defer the shutdown function yourself. If you don't need Sentry either, otelx.Setup stands on its own.
Where to next
- Logging: the
slogcomposition and the Monolog-format dev console; through PHP eyes, the most striking part. - OpenTelemetry: what the kit instruments on its own, and how the shop's "follow the order" trace comes together.
- Sentry: error reporting with
errs-code issue grouping, layered on top of OTel.