log/slog, zero deps) and OpenTelemetry tracing (OTLP gRPC export).
Both are opt-in — pkg/core remains dependency-free.
Quick setup (agent options)
The simplest way to enable observability is via agent options. No extra imports needed:WithLoggingandWithTracingwrap the provider transparently — no changes to how you callRunorRunStream.WithTracing()uses the global OTel tracer. IfInitTracerProviderwas not called, a noop tracer is used silently.- Both options can be stacked in any order. Recommended:
WithLoggingbeforeWithTracingso log events nest inside the span.
Logging middleware (advanced)
For finer-grained control — or to wrap a memory store — usepkg/middleware/logging directly:
Log events emitted
| Event | Level | Fields |
|---|---|---|
provider.Chat: start | Debug | provider, model, messages |
provider.Chat: done | Info | provider, model, duration, stop_reason, input_tokens, output_tokens |
provider.Chat: error | Error | provider, model, duration, error |
provider.ChatStream: start | Debug | provider, model, messages |
provider.ChatStream: done | Info | provider, model, duration, stop_reason, text_bytes |
provider.ChatStream: stream error | Error | provider, duration, error |
memory.Get: done | Debug | session, messages, duration |
memory.Append: done | Debug | session, count, duration |
memory.Clear: done | Info | session, duration |
OpenTelemetry tracing
pkg/middleware/otel wraps providers and memory with OTel spans. Requires
an OTLP-compatible backend (Jaeger, Grafana Tempo, Honeycomb, etc.).
Setup
Spans and attributes
| Span name | Attributes |
|---|---|
chainforge.provider.chat | provider, model, messages, session_id (auto), stop_reason, input_tokens, output_tokens |
chainforge.provider.chat_stream | provider, model, messages, session_id (auto), stop_reason, input_tokens, output_tokens |
chainforge.memory.get | session_id, message_count |
chainforge.memory.append | session_id, message_count |
chainforge.memory.clear | session_id |
session_id attribute is injected automatically by the agent loop before every provider call — no user action required.
Custom span attributes
UseWithTraceAttributes to append arbitrary attributes to every span. The function receives the call context, allowing extraction of request-scoped values:
WithTraceAttributes has no effect when WithTracing is not set.
Streaming span lifecycle
chainforge.provider.chat_stream ends after the last event is drained,
not after the channel is opened. This captures true end-to-end streaming
latency including tool dispatch time.
Composing logging + tracing (advanced)
Prometheus metrics
pkg/middleware/metrics records three metric families for every provider call.
Setup
ProviderBuilder:
Metrics emitted
| Metric | Type | Labels | Notes |
|---|---|---|---|
chainforge_provider_requests_total | Counter | provider, status (ok|error) | Incremented after each call completes |
chainforge_provider_request_duration_seconds | Histogram | provider | Latency in seconds; for streams covers open → channel close |
chainforge_provider_tokens_total | Counter | provider, token_type (input|output) | From Usage in the response or Done stream event |
ChatStream, all three metrics are recorded after the channel is fully drained, not at stream-open time.
Per-tool metrics
Wrap individual tools to record per-tool latency and call counts:| Metric | Type | Labels |
|---|---|---|
chainforge_tool_calls_total | Counter | tool, status (ok|error) |
chainforge_tool_duration_seconds | Histogram | tool |