obs

package
v1.10.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 5, 2026 License: MIT Imports: 14 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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

func NewContextHandler(inner slog.Handler) slog.Handler

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) Enabled

func (h *ContextHandler) Enabled(ctx context.Context, l slog.Level) bool

Enabled delegates to the inner handler.

func (*ContextHandler) Handle

func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error

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.

func (*ContextHandler) WithAttrs

func (h *ContextHandler) WithAttrs(attrs []slog.Attr) slog.Handler

WithAttrs wraps the inner's WithAttrs result so the trace-injection wrapper identity is preserved across derived handlers (slog.Logger.With).

func (*ContextHandler) WithGroup

func (h *ContextHandler) WithGroup(name string) slog.Handler

WithGroup wraps the inner's WithGroup result for the same reason as WithAttrs — derived handlers must keep the ContextHandler wrapper.

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

func (m *Metrics) EditOutcomeInc(toolName, outcome, strategy string)

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

func (m *Metrics) LSPoolCircuitStateSet(language string, state float64)

LSPoolCircuitStateSet publishes the circuit breaker state for a language: 0=closed, 1=half-open, 2=open (D-14).

func (*Metrics) LSPoolEviction

func (m *Metrics) LSPoolEviction(language, reason string)

LSPoolEviction increments the eviction counter for a language + reason. reason must be one of {idle, pressure, crash, shutdown} (D-13).

func (*Metrics) LSPoolLookup

func (m *Metrics) LSPoolLookup(language, result string)

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

func (m *Metrics) LSPoolRestart(language string)

LSPoolRestart increments the worker restart counter for a language (D-15).

func (*Metrics) LSPoolWorkersSet

func (m *Metrics) LSPoolWorkersSet(language string, delta float64)

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

func (m *Metrics) RenameStrategyInc(strategy string)

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

func (m *Metrics) RepoMapExtractObserve(language, extractor string, seconds float64)

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

func (m *Metrics) RepoMapLookup(language, result string)

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

func (m *Metrics) SemanticExtractionTotal(language, outcome string)

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

func (m *Metrics) SemanticStoreOpenInc(workspaceLabel, outcome string)

SemanticStoreOpenInc increments helix_semantic_store_open_total. outcome ∈ {"opened","quarantined","created"}; any other value is dropped.

func (*Metrics) SemanticStoreQuarantineInc

func (m *Metrics) SemanticStoreQuarantineInc(workspaceLabel, reason string)

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

func (m *Metrics) SessionLifecycleInc(phase, transport string)

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

func Noop(inner slog.Handler) *Provider

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

func WithTracing(inner slog.Handler, cfg TracingConfig, logger *slog.Logger) *Provider

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

func (p *Provider) Metrics() *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

func (p *Provider) ShutdownTracing(ctx context.Context) error

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

func (p *Provider) SlogHandler() slog.Handler

SlogHandler returns the slog.Handler that should be passed to slog.New() at daemon and forwarder startup. Never returns nil.

func (*Provider) Tracer

func (p *Provider) Tracer() trace.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

type SpanContext struct {
	TraceID string
	SpanID  string
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL