Astrolabe / ajxpublic /astrolabe
Repository

Telemetry

reference scope internal/telemetry Verification failed
  • Providers
  • internal/telemetry/telemetry.go
  • internal/telemetry/logging.go
  • internal/telemetry/metrics.go
  • internal/telemetry/keys.go
Relevant source files · 4

Astrolabe runs a long, LLM-heavy pipeline: it walks a source graph, drives an agent through multi-round drafting, and verifies the output. The internal/telemetry package is the observability layer that makes that pipeline legible. It wires the binary into OpenTelemetry so that every LLM request, tool call, and pipeline stage can be exported as traces, metrics, and logs — over OTLP (gRPC or HTTP/protobuf) or to stdout — configured entirely through the standard OTEL_* environment variables. When nothing is configured, the package deliberately does nothing: Setup returns no-op providers and the pipeline runs with zero telemetry overhead.

Attribute and metric names follow the OpenTelemetry GenAI semantic conventions (gen_ai.*). Because those conventions are still moving upstream, every name lives in this package as a string constant — a rename upstream is a one-line change rather than a semconv dependency to chase (keys.go).

How it works

Everything starts at Setup, which cmd/astrolabe calls once during startup. It parses the environment into a config (which exporter kind and endpoint each of the three signals should use, plus protocol, insecure flag, and headers via parseConfig), builds a resource tagged with service.name, service.version, OS, and architecture, and then wires the three signals one by one (Setup):

  1. Traces — tracesProvider returns a no-op tracer when disabled, a stdouttrace exporter for debugging, or an OTLP exporter over gRPC/HTTP.
  2. Metrics — setupMetrics attaches a periodic reader (15 s interval) to a MeterProvider and installs it globally via otel.SetMeterProvider.
  3. Logs — setupLogs creates an OTLP-backed LoggerProvider, installs it globally, and — the important link — hands back an otelslog handler as Providers.SlogHandler so the ordinary log/slog output also flows into OTLP (setupLogs).

The result is bundled into Providers, whose fields are always non-nil; a disabled signal is simply a no-op provider (Providers). Shutdown runs the collected closers in order (flushing metrics and logs before shutting down), and Enabled reports whether anything real is wired up — useful for skipping expensive attribute assembly when nobody is listening (Shutdown).

The zero-config path is a first-class design choice. resolveExporter decides per signal: an explicit OTEL_TRACES_EXPORTER wins; if unset, OTLP is used only when an endpoint is configured, otherwise none (resolveExporter). So a plain astrolabe run touches no exporters at all.

Logging: one logger, two sinks

Logging is kept deliberately simple. NewLogger always writes logfmt text to the console, and, when the logs signal is enabled, fans out to the OTel bridge through a slog.NewMultiHandler — both sinks share the same level (NewLogger). The package adds a trace level below debug (LevelTrace = -8) for full model replies, reasoning, and tool results, with a ReplaceAttr that renders it as "TRACE" on the console (ParseLevel). Because the OTel bridge would otherwise accept everything its provider enables, a small minLevel wrapper filters it by the configured level. Records logged with a context carry the active span's trace ID, so logs join up with traces in the backend.

Metrics: GenAI client conventions

Metrics are minimal and focused on the LLM boundary. The package owns two instruments and two record helpers: RecordOperationDuration samples gen_ai.client.operation.duration (adding error.type on failure), and RecordTokenUsage samples gen_ai.client.token.usage per token type, skipping non-positive counts (RecordOperationDuration, RecordTokenUsage). The LLM client layer and the agent record against these as calls happen; see LLM Client Layer and Agent Core for where the samples originate. errorType walks the wrap chain to produce a stable class — timeout for deadline exceeded, else error.

Attribute vocabulary

The largest file, keys.go, is the shared vocabulary. It defines three families:

Family Examples Purpose
GenAI attributes gen_ai.operation.name, gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.tool.call.id Span/metric attributes matching the GenAI semantic conventions
Claude-compatible flat keys span.type, input_tokens, stop_reason, tool_name, success, attempt Same shape Claude Code puts on its agent spans, so dashboards built for those traces work unchanged (keys.go)
astrolabe pipeline keys astrolabe.page.slug, astrolabe.redraft, astrolabe.verification.failures, astrolabe.graph.nodes Pipeline-level observability: which page, which round, verification outcome

Content capture is opt-in and off by default — prompts carry source code, so nothing lands on spans unless OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true (CaptureContent). Truncate bounds attribute strings for both spans and logs.

Endpoint handling

The OTLP option builders normalize endpoints per transport: for gRPC, endpointParts splits a URL into host:port and derives the insecure flag from an http:// scheme (defaulting to localhost:4317); for HTTP, the endpoint is passed through with insecure inferred the same way. Headers parsed from OTEL_EXPORTER_OTLP_HEADERS (k=v,k2=v2) are applied to every exporter.

Relationships

flowchart TB
    main["cmd/astrolabe/main.go"] -->|"Setup, Shutdown"| Providers["Providers"]
    Providers --> setupMetrics["Providers::setupMetrics"]
    Providers --> setupLogs["Providers::setupLogs"]
    setupLogs --> SlogHandler["Providers.SlogHandler"]
    SlogHandler --> NewLogger["NewLogger"]
    agent["internal/agent.Agent"] -->|"spans + metrics"| keys["keys.go constants"]
    llm["internal/llm.Client"] -->|"RecordOperationDuration / RecordTokenUsage"| metrics["metrics.go instruments"]
    tools["internal/agent/tools"] -->|"spans"| keys

The package sits at the edge of the pipeline: CLI & Pipeline Orchestration initializes it, while Agent Core, Agent Tools & Budget, and LLM Client Layer emit spans and metric samples using the vocabulary defined here.

Configuration

Variable Effect
OTEL_TRACES_EXPORTER / OTEL_METRICS_EXPORTER / OTEL_LOGS_EXPORTER otlp, stdout, or none; unset means OTLP if an endpoint exists, else none
OTEL_EXPORTER_OTLP_PROTOCOL grpc (default) or http/protobuf; anything else is an error for enabled signals
OTEL_EXPORTER_OTLP_ENDPOINT (and per-signal _TRACES_/_METRICS_/_LOGS_ variants) Collector endpoint; signal-specific value wins
OTEL_EXPORTER_OTLP_INSECURE true disables TLS (also implied by http:// endpoints)
OTEL_EXPORTER_OTLP_HEADERS Comma-separated k=v headers sent with every export
OTEL_SERVICE_NAME Overrides the service.name resource attribute
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT true opts into putting message content on spans

Reference

Symbol Kind What it does Location
Setup function Parses OTEL_* env, builds resource, wires traces/metrics/logs, installs globals internal/telemetry/telemetry.go#L146-L184
Providers struct Bundles tracer/meter/logger providers plus the slog bridge handler internal/telemetry/telemetry.go#L118-L125
Providers::Shutdown method Flushes and shuts down all configured exporters internal/telemetry/telemetry.go#L128-L136
Providers::Enabled method True when any signal is wired to a real exporter internal/telemetry/telemetry.go#L139
Providers::setupMetrics method Periodic-reader meter provider over stdout or OTLP internal/telemetry/telemetry.go#L216-L251
Providers::setupLogs method OTLP log provider + otelslog handler bridge internal/telemetry/telemetry.go#L253-L288
ParseLevel function Parses `trace debug
NewLogger function Console logfmt logger fanned out to the OTel bridge internal/telemetry/logging.go#L36-L52
RecordOperationDuration function Samples gen_ai.client.operation.duration with error.type on failure internal/telemetry/metrics.go#L37-L43
RecordTokenUsage function Samples gen_ai.client.token.usage per token type internal/telemetry/metrics.go#L47-L56
CaptureContent function Reports whether GenAI message content may go on spans internal/telemetry/keys.go#L118-L120
Truncate function Bounds a string for span attributes and logs internal/telemetry/keys.go#L123-L128

Generated by astrolabe from commit ecc180b3 (tag 0.4.0).