Documentation
¶
Overview ¶
Phase 58 D-06: centralized constructors for the otelgrpc stats handlers used by the forwarder client (internal/forwarder/dial.go) and the daemon server (internal/daemon/daemon.go). Both sites need (a) an explicit TracerProvider (no-global D-01) and (b) the W3C TraceContext propagator wired per-handler (the propagator extension of the no-global rule).
Centralising the helpers here means the client and server propagator option set cannot drift — a future contributor cannot accidentally land a dial site that omits WithPropagators while the server site retains it (or vice versa), breaking trace continuity asymmetrically.
Design rules:
- No call to otel.SetTextMapPropagator in this file or anywhere else in internal/obs (D-01 extends to the propagator).
- Both helpers take an explicit trace.TracerProvider; nil callers are not supported (use obs.Provider.TracerProvider() which is non-nil by Noop construction).
- Return type is google.golang.org/grpc/stats.Handler so call sites pass the result directly to grpc.WithStatsHandler / grpc.StatsHandler.
Phase 11 metrics scaffolding: pre-registered Prometheus vectors on an owned registry. Middleware and lspool (plans 11-02 / 11-03) consume via *obs.Metrics only — prometheus/client_golang is intentionally kept confined to this package so the blast radius of the dependency stays tight.
Design rules (frozen for downstream plans):
- Exactly ONE *prometheus.Registry owned by each *Metrics instance. The prometheus global registerer is NEVER touched (PITFALLS.md meta-rule, T-11-05 mitigation).
- Metric vectors are constructed ONCE in newMetrics(); middleware resolves labels per call via .WithLabelValues(...) (Pattern 1, RESEARCH.md).
- AllowedLabels is an ARRAY, not a slice — immutable at the type level.
- Noop providers still get a real Metrics sink so call sites never branch on nil (see obs.Noop). The vectors are unscraped in the noop path; the cost of constructing them at startup is negligible.
- lspool sink helper signatures below are the source of truth for the lspool.MetricsSink interface in plan 11-03. Changing them here is a breaking change for that plan.
Package obs holds the observability scaffolding for Helix.
Phase 10 shipped the noop slog ContextHandler wrapper. Phase 11 adds Prometheus metrics (see metrics.go) — the obs package is now the single home for all prometheus/client_golang imports so the blast radius of that dependency stays contained.
Design rules for this package (pin them early so later phases don't drift):
- Provider is a struct, never an interface, so future phases can add methods without breaking call sites.
- Noop always returns a non-nil *Provider with non-nil internal fields; call sites never branch on `provider == nil`. This extends to Metrics() — the noop path also holds a real *Metrics sink.
- Third-party deps live HERE only. Phase 11: prometheus/client_golang. Phase 12 will add go.opentelemetry.io/* in the same package.
Phase 12: TracerProvider construction, OTLP/gRPC exporter lifecycle, and the WithTracing constructor that returns a degraded-optional Provider.
Design rules (frozen for downstream Phase 12 plans):
- newTracerProvider is an internal factory; only WithTracing calls it.
- WithTracing starts from Noop(inner) so every field is always non-nil. If exporter construction fails, WithTracing logs a warning and returns the noop Provider unchanged — degraded-optional (D-09).
- The default path (TracingEndpoint == "") MUST use tracenoop, NOT the SDK tracer with a 0% sampler. SDK tracer allocates per span even when dropped (D-17 hot-path budget).
- No call to otel.SetTracerProvider anywhere in this package (D-01).
Index ¶
- Variables
- func ClientStatsHandler(tp trace.TracerProvider) stats.Handler
- func NewContextHandler(inner slog.Handler) slog.Handler
- func ServerStatsHandler(tp trace.TracerProvider) stats.Handler
- type ContextHandler
- type Metrics
- func (m *Metrics) EditOutcomeInc(toolName, outcome, strategy string)
- func (m *Metrics) LSPoolCircuitStateSet(language string, state float64)
- func (m *Metrics) LSPoolEviction(language, reason string)
- func (m *Metrics) LSPoolLookup(language, result string)
- func (m *Metrics) LSPoolRestart(language string)
- func (m *Metrics) LSPoolWorkersSet(language string, delta float64)
- func (m *Metrics) Registry() *prometheus.Registry
- func (m *Metrics) RenameStrategyInc(strategy string)
- func (m *Metrics) RepoMapExtractObserve(language, extractor string, seconds float64)
- func (m *Metrics) RepoMapLookup(language, result string)
- func (m *Metrics) SemanticExtractionTotal(language, outcome string)
- func (m *Metrics) SemanticStoreOpenInc(workspaceLabel, outcome string)
- func (m *Metrics) SemanticStoreQuarantineInc(workspaceLabel, reason string)
- func (m *Metrics) SessionLifecycleInc(phase, transport string)
- type Provider
- type SpanContext
- type TracingConfig
Constants ¶
This section is empty.
Variables ¶
var AllowedLabels = [5]string{"tool_name", "profile", "mode", "language", "outcome"}
AllowedLabels is the bounded-label allowlist enforced at CI time by TestMetricsLabelsAllowlist (D-03..D-05). Changing this list requires a matching change to metrics_labels_test.go and a plan-level decision.
Functions ¶
func ClientStatsHandler ¶
func ClientStatsHandler(tp trace.TracerProvider) stats.Handler
ClientStatsHandler returns the standard otelgrpc client handler with TraceContext propagator pre-wired. Phase 58 D-06: keeps propagator wiring in one place so dial sites cannot drift from server sites.
func NewContextHandler ¶
NewContextHandler wraps inner with a trace-aware handler. The returned handler is safe to use anywhere a slog.Handler is expected; it delegates all decisions (level gating, formatting, output destination) to inner.
func ServerStatsHandler ¶
func ServerStatsHandler(tp trace.TracerProvider) stats.Handler
ServerStatsHandler is the symmetric server-side helper. Use this in grpc.NewServer(grpc.StatsHandler(...)) to keep traceparent extraction consistent with the client side.
Types ¶
type ContextHandler ¶
type ContextHandler struct {
// contains filtered or unexported fields
}
ContextHandler wraps an inner slog.Handler and, when a SpanContext is present on ctx, appends trace_id and span_id attributes to each record. When ctx carries no span, Handle forwards to the inner handler without touching the record — this is the zero-alloc fast path that the Phase 10 stub extractor always takes. See BenchmarkContextHandler_Handle.
func (*ContextHandler) Handle ¶
Handle injects trace_id/span_id when a span is present on ctx, then forwards to the inner handler. The early return is the OBS-06 hot-path guarantee: no attr construction, no allocation, no slice growth.
type Metrics ¶
type Metrics struct {
// RED metric vectors for MCP tool calls (plan 11-02).
ToolCalls *prometheus.CounterVec
ToolDuration *prometheus.HistogramVec
// lspool gauges and counters (plan 11-03).
LSPoolWorkers *prometheus.GaugeVec
LSPoolEvictions *prometheus.CounterVec
LSPoolCircuitState *prometheus.GaugeVec
LSPoolRestarts *prometheus.CounterVec
// Phase 47 D-07: rename_symbol dispatcher strategy counter.
// Closed-enum label "strategy" ∈ {"lsp-native", "rust-client-side"};
// enforced at emission sites (see *Metrics.RenameStrategyInc and
// internal/mcp.RecordRenameStrategy). The "strategy" label is carved
// out of AllowedLabels in metrics_labels_test.go for this family only.
RenameStrategy *prometheus.CounterVec
// Phase 53 D-01/D-04: lspool worker-pool cache lookup counter.
// Closed-enum label "result" ∈ {"hit","miss"} — hit = shared warm worker,
// miss = spawn (or refusal); enforced at emission via LSPoolLookup helper.
LSPoolLookups *prometheus.CounterVec
// Phase 53 D-01/D-03/D-04: repomap TagCache mtime-match lookup counter.
// Closed-enum label "result" ∈ {"hit","miss"}.
RepoMapLookups *prometheus.CounterVec
// Phase 53 D-05/D-06/D-07: repomap extractor latency histogram (cache-miss path only).
// Closed-enum label "extractor" ∈ {"treesitter","lsp","fallback"}.
// Custom buckets target 1ms→2.5s to give fast-path resolution for tree-sitter
// and tail visibility for LSP fallback.
RepoMapExtract *prometheus.HistogramVec
// Phase 53 D-08/D-09: session lifecycle counter.
// Closed-enum "phase" ∈ {"started","ended","error"}; "transport" ∈ {"stdio","http"}.
SessionLifecycle *prometheus.CounterVec
// Phase 53 D-10/D-11/D-12: edit-tool outcome counter (7 tools).
// Closed-enum "outcome" ∈ {"success","no_match","ambiguous_match","validation_failed","ls_error","internal"};
// "strategy" ∈ {"exact","whitespace_normalized","indentation_flexible","none"} (per Q-4 resolution).
// "tool_name" reuses the existing AllowedLabels entry — NOT a carve-out.
EditOutcome *prometheus.CounterVec
// Phase 57 D-06/D-07: semantic store DuckDB-file quarantine counter.
// Closed-enum "reason" ∈ {"corrupt_file","schema_forward_incompat",
// "schema_unreadable","unknown"}; "workspace_label" is the bounded
// hashed/truncated identifier used by the existing helix_lspool_*
// metrics (T-57-02-06 mitigation — no raw paths).
SemanticStoreQuarantine *prometheus.CounterVec
// Phase 57: semantic store open-attempt counter.
// Closed-enum "outcome" ∈ {"opened","quarantined","created"}.
SemanticStoreOpen *prometheus.CounterVec
// Phase 59 P02: tree-sitter extraction outcome counter.
// Closed-enum "language" ∈ {"go","typescript","python","other"};
// "outcome" ∈ {"ready","partial","unsupported","failed"}. Both labels
// are members of AllowedLabels; bounded-cardinality is enforced at
// emission via SemanticExtractionTotal (T-59-02-02 mitigation).
SemanticExtraction *prometheus.CounterVec
// contains filtered or unexported fields
}
Metrics holds all Prometheus vectors owned by this package plus the private registry they are registered against. Access via *obs.Provider.Metrics().
func (*Metrics) EditOutcomeInc ¶
EditOutcomeInc increments helix_edit_outcome_total. outcome ∈ {"success","no_match","ambiguous_match","validation_failed","ls_error","internal"}; strategy ∈ {"exact","whitespace_normalized","indentation_flexible","none"}; any other value is dropped (Phase 53 D-10/D-11/Q-4 closed enums, T-53-01 mitigation). tool_name is unbounded by helper but bounded in practice by the 7-tool surface (D-12).
func (*Metrics) LSPoolCircuitStateSet ¶
LSPoolCircuitStateSet publishes the circuit breaker state for a language: 0=closed, 1=half-open, 2=open (D-14).
func (*Metrics) LSPoolEviction ¶
LSPoolEviction increments the eviction counter for a language + reason. reason must be one of {idle, pressure, crash, shutdown} (D-13).
func (*Metrics) LSPoolLookup ¶
LSPoolLookup increments helix_lspool_lookups_total. result ∈ {"hit","miss"}; any other value is dropped (Phase 53 D-04 closed enum, T-53-01 mitigation).
func (*Metrics) LSPoolRestart ¶
LSPoolRestart increments the worker restart counter for a language (D-15).
func (*Metrics) LSPoolWorkersSet ¶
LSPoolWorkersSet adjusts the active worker gauge for a language by delta. Positive delta on acquire, negative delta on release/evict.
func (*Metrics) Registry ¶
func (m *Metrics) Registry() *prometheus.Registry
Registry returns the owned *prometheus.Registry for use with promhttp.HandlerFor on the admin listener (plan 11-01, task 3).
func (*Metrics) RenameStrategyInc ¶
RenameStrategyInc increments the rename dispatcher strategy counter. strategy MUST be one of the closed-enum values {"lsp-native","rust-client-side"}; any other value is dropped to preserve bounded cardinality (Phase 47 D-07, threat T-47-08 mitigation).
func (*Metrics) RepoMapExtractObserve ¶
RepoMapExtractObserve records seconds for the cache-miss extractor path. extractor ∈ {"treesitter","lsp","fallback"}; any other value is dropped (Phase 53 D-07 closed enum, T-53-01 mitigation).
func (*Metrics) RepoMapLookup ¶
RepoMapLookup increments helix_repomap_lookups_total. Mirrors LSPoolLookup (Phase 53 D-04 closed enum, T-53-01 mitigation).
func (*Metrics) SemanticExtractionTotal ¶ added in v1.10.1
SemanticExtractionTotal increments helix_semantic_extraction_total. Phase 59 P02 / T-59-02-02 mitigation:
- language ∉ {go,typescript,python,other} is COERCED to "other".
- outcome ∉ {ready,partial,unsupported,failed} DROPS the emission.
Cardinality bound: 4 languages × 4 outcomes = 16 combos.
func (*Metrics) SemanticStoreOpenInc ¶
SemanticStoreOpenInc increments helix_semantic_store_open_total. outcome ∈ {"opened","quarantined","created"}; any other value is dropped.
func (*Metrics) SemanticStoreQuarantineInc ¶
SemanticStoreQuarantineInc increments helix_semantic_store_quarantine_total. reason ∈ {"corrupt_file","schema_forward_incompat","schema_unreadable","unknown"}; any other value is dropped (Phase 57 D-07 closed enum, T-57-02-06 mitigation).
func (*Metrics) SessionLifecycleInc ¶
SessionLifecycleInc increments helix_session_lifecycle_total. phase ∈ {"started","ended","error"}; transport ∈ {"stdio","http"}; any other value is dropped (Phase 53 D-08/D-09 closed enums, T-53-01 mitigation).
type Provider ¶
type Provider struct {
// contains filtered or unexported fields
}
Provider is the single entry point for all observability wiring. Phase 10 exposed SlogHandler; Phase 11 adds Metrics; Phase 12 will add a Tracer accessor on this same type.
func NewForTest ¶
func NewForTest(tp trace.TracerProvider) *Provider
NewForTest constructs a Provider with the given TracerProvider. Intended for test code that needs to inject a tracetest-backed provider. The slog handler and metrics sink are noop-initialised.
func Noop ¶
Noop returns a Provider that does nothing beyond wrapping the supplied inner slog.Handler with the trace-aware ContextHandler and pre-constructing a Metrics sink on an owned registry. The log hot path remains zero-cost (spanContextFromContext is a stub); the metrics sink is unscraped unless the admin listener mounts /metrics (plan 11-01 task 3).
func WithTracing ¶
WithTracing constructs a Provider with a real SDK TracerProvider backed by an OTLP/gRPC exporter. If cfg.Endpoint is empty, the caller should use Noop() instead — this function is intended for the non-empty endpoint path.
On any construction error, WithTracing logs a warning via logger and returns a noop-initialized Provider (degraded-optional, D-09). The returned Provider is always non-nil and safe to use.
func (*Provider) Metrics ¶
Metrics returns the Prometheus metrics sink. Never returns nil for a Provider constructed via Noop — downstream code is contractually allowed to call methods on the result without nil checks.
func (*Provider) ShutdownTracing ¶
ShutdownTracing flushes and shuts down the TracerProvider if it implements Shutdown (i.e. it is an SDK TracerProvider, not the noop). No-op for the noop path. The caller should pass a context with a deadline (e.g. 5s) to bound the flush time (PITFALLS #4).
func (*Provider) SlogHandler ¶
SlogHandler returns the slog.Handler that should be passed to slog.New() at daemon and forwarder startup. Never returns nil.
func (*Provider) Tracer ¶
Tracer returns a trace.Tracer scoped to the Helix module. Never returns nil (D-04): even the noop path yields a functional (no-op) tracer.
func (*Provider) TracerProvider ¶
func (p *Provider) TracerProvider() trace.TracerProvider
TracerProvider returns the underlying trace.TracerProvider. Never nil — Noop sets it to tracenoop.NewTracerProvider(), WithTracing may replace it with an SDK provider. Callers that need an explicit provider (e.g. otelgrpc.WithTracerProvider) use this accessor.
type SpanContext ¶
SpanContext carries the minimal trace identifiers that ContextHandler attaches to every log record when a span is active on the context.
type TracingConfig ¶
type TracingConfig struct {
// Endpoint is the OTLP/gRPC collector endpoint. Empty disables tracing.
Endpoint string
// ServiceName is the OTel resource service.name attribute.
ServiceName string
// SampleRatio is the TraceIDRatioBased fraction (0.0 = off, 1.0 = all).
SampleRatio float64
}
TracingConfig is the package-local copy of tracing configuration fields. The daemon maps config.ObservabilityConfig into this struct.