Imported from SumitKumar-17/Cache-Pot (
internal/observability/AGENTS.md). Install upstream withnpx skills add SumitKumar-17/Cache-Pot --skill observability. Copyright stays with the author.
internal/observability
Process-wide instrumentation: atomic counters (Metrics), two decorator wrappers that
instrument embed.Provider and llm.CompletionProvider, a slog constructor, and the
/metrics (Prometheus text), /stats (JSON), /dashboard (HTML) HTTP handlers served
on the MCP port. Every other domain package (resp handlers, internal/mcp,
internal/storage/memstore) reports into the single *observability.Metrics instance
built once in internal/server/server.go; this package never reads from those packages,
only gets called into. Shipped in v0.5.0.
Key types/contracts
Metrics(metrics.go): all fields areatomic.Int64/mutex-guarded maps, safe for concurrent use from any goroutine (RESP connection handlers, MCP tool handlers, the TTL reaper). One process-wide instance, constructed viaNewMetrics(), threaded everywhere viaresp.Deps.Metricsandmcp.New(...)— same "one shared instance, no adapters" discipline as the domain stores. Never construct a secondMetricsfor a subsystem.Metrics.Snapshot()is the only way to read current values — it copies themcpCallsByToolmap andlatencymap under their mutexes, so aSnapshotis safe to hold and range over without further locking.MetricsHandler/StatsHandler/DashboardHandlerall build their output purely from oneSnapshot()call, never touchingMetricsfields directly.latencyAccumulatoris deliberately count/sum/max only, not a histogram — this project's stated scope doesn't need bucketed histograms (see comment inmetrics.go). Don't add histogram buckets without discussing the tradeoff; it mirrors the "bounded sampling over full scans" philosophy in the root AGENTS.md.InstrumentProvider(inner embed.Provider, m *Metrics, tracker *analytics.Tracker) embed.ProviderandInstrumentCompletionProvider(inner llm.CompletionProvider, m *Metrics, tracker *analytics.Tracker) llm.CompletionProvider: decorators wrapping the real embed/completion provider exactly once inserver.go, before it's handed to any consumer (internal/semantic,internal/memory,internal/consolidate,internal/graph). This is why none of those packages call intoobservabilitythemselves — one wrap covers every current and future caller. If you add a new embedding/completion consumer, it gets instrumentation for free as long as it's handed the already-wrapped provider fromserver.go— never wrap a provider a second time.- Optional-capability forwarding is load-bearing, not incidental.
InstrumentProvidertype-assertsinneragainstembed.UsageEmbedder(only the real OpenAI provider implements it, notembed.NewMock) and returns a different wrapper type (instrumentedUsageProvider) only if the assertion succeeds — Go doesn't forward optional interfaces through a wrapper automatically, hence the embedding trick ininstrumentedUsageProvider(embeds*instrumentedProviderfor promoted methods, implementsEmbedBatchWithUsageexplicitly). If you add a new optional provider capability, follow this exact pattern rather than makingUsageEmbeddermandatory on the base interface. tracker *analytics.Trackermay always be nil in both instrument functions and inStatsHandler/DashboardHandler— every call site nil-checks before touching it (analyticsSnapshothelper). Don't remove those nil checks; tests rely on passingnilwhen analytics isn't under test.Metrics.KeyEvictedis a plain no-arg callback, passed tomemstore.WithOnEvict(s.metrics.KeyEvicted)inserver.go—internal/storage/memstorenever imports this package. Keep new storage-side instrumentation hooks shaped this way (callback in, no reverse dependency) rather than having memstore import observability directly.- Zero counts are honest, not bugs.
EntitiesExtracted(0)/RelationsExtracted(0)reflect the mockCompletionProvider's real "no entities extracted" behavior (seeinternal/graph);MemoriesDeduped(0)reflects a consolidation pass that found no near-duplicates. Don't treat a zero snapshot value as a wiring bug without checking which provider is configured.
Conventions/gotchas specific to this package
- No metrics client library, no Prometheus SDK —
MetricsHandlerhand-rolls the Prometheus text exposition format directly inhttp.go(matches this project's stdlib-only precedent, e.g. the OpenAI embeddings provider). If you add a counter, add both aMetricsfield/method AND a corresponding# HELP/# TYPE/value line inMetricsHandler, AND a JSON field instatsResponse/buildDashboardViewif it belongs on/stats//dashboardtoo — there's no reflection-based auto-export.metrics_test.go'sTestMetricsHandlerRendersPrometheusText/TestStatsHandlerRendersJSONassert on literal substrings, so a renamed metric needs the test updated too.metrics_test.goline count andhttp.goline count will make an omission obvious. DashboardHandlerdeliberately does not show a "tokens avoided" figure — aCACHE.SEMANTIChit still re-embeds the query prompt to compute similarity, so no embedding tokens are actually avoided by a hit. Showing a fabricated avoided-tokens number would violate the project's honesty policy; the dashboard only shows tokens consumed and dollars saved (from caller-reportedCOST). Don't add a token-savings stat without first checking whether it's actually measured anywhere.dashboardHTMLis a singlehtml/templatestring constant with package-level template funcs (avgMillis,maxMillis,mulf) registered viatemplate.FuncMap—html/templatefuncs can't be methods on a type defined elsewhere, hence the free functions. No external CSS/JS.NewLoggeris the only sanctioned way to construct aslog.Loggerin this codebase — JSON handler on stdout, level configurable. Don't callslog.New(slog.NewJSONHandler(...))ad hoc elsewhere.
Testing
go test ./internal/observability/...
-race is worth running here specifically (TestConcurrentCounters spawns 200
goroutines hammering the same Metrics) — it's part of the standard root
go test ./... -race but this package is exactly the kind of concurrent-counter code
that benefits from being raced directly:
go test ./internal/observability/... -race
Test doubles: fakeProvider/fakeUsageProvider (minimal embed.Provider/
embed.UsageEmbedder doubles) and fakeCompletionProvider (llm.CompletionProvider
double) — all hand-written in the _test.go files, no mocking library, no dependency on
internal/embed's or internal/llm's real mock.go. HTTP handlers are tested via
net/http/httptest directly against MetricsHandler/StatsHandler, asserting on
literal output substrings.
Limitations
- No histograms/percentiles — only count/avg/max per command family. If you need p99
latency, this package doesn't have it; that's a real gap, not an oversight (see the
latencyAccumulatorcomment). /dashboardre-renders from a freshSnapshot()on every request with no caching or auto-refresh — it's an operator/debug view, not a monitoring product (documented in its own doc comment).