OTel observability: logs, metrics, traces
(replaces grackleclub/log)
go get github.com/grackleclub/signals
requires go v1.25+
func main() {
ctx := context.Background()
shutdown, log, err := signals.Setup(ctx, signals.Config{
Env: "prod",
Version: build.SHA,
})
if err != nil {
panic(fmt.Errorf("setup signals: %w", err)
}
defer shutdown(ctx) // flush telemetry on exit
slog.SetDefault(log) // make log the process-wide default
log.InfoContext(ctx, "starting", "addr", addr) // contains span details for correlation
}Setup installs the global tracer, meter, and logger providers (one shared
resource) plus the W3C propagator, then returns a ready *slog.Logger. Call it
once from main.
Traces and metrics go into OTel's global registry, so any library can summon them with no plumbing:
var tracer = otel.Tracer("github.com/grackleclub/rulette/game")
var meter = otel.Meter("github.com/grackleclub/rulette/game")The logger is the exception: Setup returns it rather than calling
slog.SetDefault, because a library mutating the caller's process-wide default
is a surprising side effect that belongs to main, not to signals. Declare it
as the default yourself, once, in main:
slog.SetDefault(log)Now the same global-registry ergonomics apply to all three signals: use
package-level slog.InfoContext(ctx, ...) anywhere, with no logger field to
thread and no parameter to pass. Skip the SetDefault call only if you
deliberately want to scope the logger and pass log around by hand.
Every field falls back to OTEL_* env, then a default.
| field | purpose | default |
|---|---|---|
Env |
deployment.environment.name |
OTEL_RESOURCE_ATTRIBUTES, then unknown |
Service |
service.name |
OTEL_SERVICE_NAME, then argv0 |
Version |
service.version |
none |
Endpoint |
OTLP/HTTP URL | OTEL_EXPORTER_OTLP_ENDPOINT |
Token |
bearer auth, merged over OTEL_EXPORTER_OTLP_HEADERS |
OTLP_INGEST_TOKEN |
StderrLevel |
console threshold | INFO |
DisableRuntimeMetrics |
drop Go runtime metrics | false |
Console |
pterm console tuning (time, caller, layout) | time auto (on when captured); tree on tty/GitHub, else one line |
The console is a pterm logger, so anything in the build that imports pterm directly (tables, spinners, boxes) shares signals' styling with no extra wiring. See examples.md.
| topic | behavior |
|---|---|
| logs | two sinks: pterm console (pretty, StderrLevel-gated, NO_COLOR aware) and OTLP |
| correlation | a log with a span's context carries trace_id/span_id; console shows a short trace_id |
| levels | console honors StderrLevel; OTLP ships every level, filter in SigNoz |
| graceful off | no endpoint configured: console only, no error |
| traces | ParentBased(AlwaysSample), override via OTEL_TRACES_SAMPLER; tail-sample at the collector |
| metrics | periodic OTLP reader plus Go runtime metrics; host metrics are the collector's |
| command | does |
|---|---|
bin/test unit |
fast, no docker |
bin/test pretty |
print the colored console output |
bin/test ci |
collector roundtrip (docker) |