Overview
telemetry is a standalone Go module (github.com/gp-system/telemetry, +telemetry/otelx, +telemetry/sentryx) with no dependency on any logging library: measurement and error reporting, booted by a single call, that takes the default logger's extra sinks as plain slog.Handler options. In a generated project you don't even write that call: the first thing server.Run and worker.Run do is wire the standalone logx module into 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.
Install
go get github.com/gp-system/telemetry@v0.0.0-20260805082731-54141fe347c3 # the main-branch commit the framework pins, no tag published yet
go get github.com/gp-system/telemetry@main
| Import path | What it adds |
|---|---|
github.com/gp-system/telemetry | the facade: Setup composes the two below into one call, plus whatever slog.Handler you pass it |
github.com/gp-system/telemetry/otelx | the OpenTelemetry SDK bootstrap |
github.com/gp-system/telemetry/sentryx | the optional Sentry integration |
github.com/gp-system/logx (+/monolog) | the slog composition: console format, level filtering, fanout. A fully standalone module, unrelated to telemetry; see logx: Overview |
A binary that only needs one piece can import just that subpackage (or, for logx, the module on its own): a CLI or migrator that wants OTel tracing but no Sentry imports telemetry/otelx alone and never links sentry-go.
The telemetry facade
Two building blocks compose inside telemetry, and logx supplies the console sink from the outside:
| Building block | What it owns | Page |
|---|---|---|
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 two above in a single Setup, plus caller-supplied slog.Handlers) | this page |
logx (+ logx/monolog) | the default slog logger's console formatting: console format, level filtering, fanout. A standalone module, not a telemetry dependency | logx: Overview |
telemetry.Config is exactly what the server.Config.Telemetry and worker.Config.Telemetry fields hold; the chassis configs hold a separate Log logx.Config field alongside it:
type Config struct {
Otel otelx.Config // OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION, OTEL_DEV_MODE
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, opts ...Option) (shutdown func(context.Context) error, err error)
func WithLogHandler(h slog.Handler) Option // adds a leaf to the default logger's fanout
func WithLogLevel(l slog.Level) Option // gates the whole fanout; default slog.LevelInfo
telemetry composes otelx and sentryx without either knowing about the other, and has no opinion on where else a log record goes: it accepts any slog.Handler through WithLogHandler. This is not cosmetics: a non-chassis binary (a CLI, a migrator) that only uses otelx never links sentry-go, and one that wants no console handler at all never links logx either.
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 chassis builds a console handler withlogx.ConsoleHandler(always in dev, in prod only withLOG_STDOUT=true) and passes it in viaWithLogHandler;Setupfans it out alongside the OTLP log bridge and the Sentry capture handler, gates the whole thing with the level fromWithLogLevel(LOG_LEVEL), and installs it as the process's default logger. If no handler was passed and neither OTel nor Sentry is active,Setupleaves the previous default logger untouched.
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 framework 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:
server.Run(ctx, cfg.Server, register, server.WithoutTelemetry())
worker.Run(ctx, cfg.Worker, register, worker.WithoutTelemetry())
realtime.Run(ctx, cfg.Realtime, register, realtime.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 framework 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.