Skip to content

fix: wire the metrics option live — Prometheus module becomes the implementation (LAB-517)#75

Open
27Bslash6 wants to merge 5 commits into
mainfrom
lab-517-wire-metrics
Open

fix: wire the metrics option live — Prometheus module becomes the implementation (LAB-517)#75
27Bslash6 wants to merge 5 commits into
mainfrom
lab-517-wire-metrics

Conversation

@27Bslash6

Copy link
Copy Markdown
Contributor

Closes LAB-517.

Problem

createCache({ metrics }) was accepted by the types and defaulted to true by every production intent preset — then silently ignored. CacheImpl never read it, and the entire Prometheus module (src/metrics/prometheus.ts) was dead code: unexported, referenced only by its own tests. A user who wired Prometheus per the types got no metrics and no error. Found by the LAB-275 cross-SDK audit; this is the LAB-388 pattern as a runtime surface.

What changed

The dead module is now the live implementation (per the issue's AC — wire it, don't reject it):

  • CacheImpl threads metrics through get/set/delete/wrap:
    • cachekit_hits_total{layer=l1|l2} — including the wrap() SWR fast path
    • cachekit_misses_total
    • cachekit_operations_total{operation,status} — recorded per backend attempt (inside retry), the honest reading of an operations counter
    • cachekit_errors_total{error_type}
    • cachekit_operation_duration_seconds{operation}
    • cachekit_l1_entries / cachekit_l1_memory_bytes gauges (updated on every L1 mutation)
    • cachekit_circuit_breaker_state gauge (published after each reliability-stack execution)
  • metrics now accepts boolean | MetricsConfig. Two latent bugs in the module fixed on the way:
    • MetricsConfig.registry was accepted-and-ignored (the same bug class this ticket is about) — now honored.
    • Two caches sharing a prefix+registry used to kill init with prom-client's duplicate-registration error — metrics are now reused via getSingleMetric.
  • L1 hit/miss counters actually increment (live counters in CacheImpl), and the SaaS X-CacheKit-L1-* telemetry metricsProvider is auto-wired from them for CachekitIO config backends. An explicit user-supplied provider still wins.
  • No silent middle: metrics: true without the optional prom-client peer dep reports once through the library logger with an actionable message ("install prom-client or set metrics: false") and degrades to no-ops. Decision per the AC's "throw or warn loudly": warn loudly, because intent presets default metrics: true and throwing would break every existing preset user who never opted in.
  • Pluggable logger hook (setLogger): all ad-hoc console.error sites (cache.ts, background-refresh.ts, redis.ts, redis-channel.ts, metrics fallback) route through it. Default remains console.error — zero behavior change unless opted in.
  • Exports: CacheMetrics, NoopMetrics, createMetrics, MetricsCollector, MetricsConfig, setLogger, CachekitLogger from the package index.

Non-goals (per issue)

OpenTelemetry tracing; Python's enable_prometheus_metrics() DX helper.

Testing

  • 17 new tests: cache.metrics.test.ts drives real prom-client through CacheImpl end-to-end (no mocks on the metrics path) — hits/misses/ops/errors/durations/gauges, wrap() SWR path, shared-registry reuse, custom prefix, disabled-by-default; cachekitio.test.ts covers telemetry header auto-wire + user override + L1-disabled; logger.test.ts covers the hook incl. a live background-refresh failure.
  • Full suite: 573 passed, lint clean, type-check clean, ESM+CJS build clean.

Docs pass (mandatory gate)

  • packages/cachekit/README.md Observability section: full metric list (was missing gauges), missing-peer-dep behavior, setLogger, SaaS telemetry auto-wire.
  • docs.cachekit.io: TS configuration reference gains a metrics row + Observability section — PR follows in cachekit-io/docs.
  • Feature matrix: the Observability section lives on unmerged protocol PR chore(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 #29 (LAB-275), whose TS cells record this exact bug — commented there with the cells to flip once this merges.

…lementation (LAB-517)

createCache({ metrics }) was accepted by the types and every intent preset,
then silently ignored: CacheImpl never read it and the entire Prometheus
module was dead code. Silent config no-ops are trust bugs.

- CacheImpl now threads metrics through get/set/delete/wrap: hits (l1/l2),
  misses, per-attempt operation counters, error counters, duration
  histograms, L1 entry/memory gauges, and the circuit-breaker state gauge.
- metrics accepts boolean | MetricsConfig (prefix, defaultLabels, registry,
  onError). The registry option was previously accepted-and-ignored — now
  honored; metrics are reused via getSingleMetric so multiple caches sharing
  a registry no longer kill initialization with duplicate-registration.
- Live L1/L2 hit and miss counters feed the SaaS X-CacheKit-L1-* telemetry
  headers: the CachekitIO metricsProvider is auto-wired from them (an
  explicit user-supplied provider still wins).
- If metrics is enabled without the optional prom-client peer dependency,
  the SDK reports once through the library logger and degrades to no-ops —
  loud, never silent.
- New pluggable logger hook (setLogger): all ad-hoc console.error sites
  (invalidation channel, background refresh, Redis error events, metrics
  fallback) now route through it; default remains console.error.
- CacheMetrics/NoopMetrics/createMetrics/MetricsCollector/MetricsConfig and
  setLogger/CachekitLogger exported from the package index.

Co-authored-by: multica-agent <github@multica.ai>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 283a9dd4-0474-4e5a-a927-8a3e0804b5fd

📥 Commits

Reviewing files that changed from the base of the PR and between d70d225 and a56bf71.

📒 Files selected for processing (20)
  • packages/cachekit-core-ts/index.js
  • packages/cachekit/README.md
  • packages/cachekit/src/backends/cachekitio.test.ts
  • packages/cachekit/src/backends/redis.ts
  • packages/cachekit/src/cache-core.ts
  • packages/cachekit/src/cache.metrics.test.ts
  • packages/cachekit/src/cache.single-flight.test.ts
  • packages/cachekit/src/cache.ts
  • packages/cachekit/src/cache/background-refresh.ts
  • packages/cachekit/src/constants.ts
  • packages/cachekit/src/exports-common.ts
  • packages/cachekit/src/index.test.ts
  • packages/cachekit/src/index.ts
  • packages/cachekit/src/intents-core.ts
  • packages/cachekit/src/invalidation/redis-channel.ts
  • packages/cachekit/src/logger.test.ts
  • packages/cachekit/src/logger.ts
  • packages/cachekit/src/metrics/prometheus.test.ts
  • packages/cachekit/src/metrics/prometheus.ts
  • packages/cachekit/src/types/cache.ts
 _____________________________
< Here comes the bug bouncer. >
 -----------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lab-517-wire-metrics

Comment @coderabbitai help to get the list of available commands.

@27Bslash6 27Bslash6 changed the title LAB-517: wire the metrics option live — Prometheus module becomes the implementation fix: wire the metrics option live — Prometheus module becomes the implementation (LAB-517) Jul 23, 2026
@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

27Bslash6 and others added 4 commits July 24, 2026 00:37
Expert-panel review found exists() was the only public op left
uninstrumented: L1 hits went uncounted and an L2 backend error degraded
to a silent false with no errors_total trace — invisible in the very
metrics this PR wires live. Root cause: the instrument()-inside /
execute()-outside pairing was copy-pasted per method, so exists() simply
drifted out of the set. Extract it into a single run() helper that every
op routes through, making the omission structurally impossible.

Co-authored-by: multica-agent <github@multica.ai>
…LAB-519)

A cold L1+L2 miss previously executed the wrapped function once per
concurrent caller — N cold callers meant N upstream calls, N L2 GETs
(N billed misses under metered-misses), and N L2 writes.

In-process (always on): concurrent wrap() calls for one cache key share
a single in-flight promise covering the L2 read, compute, and write.
The L2 read must be inside the flight — the GET-miss is the billed
event, so deduping only the compute would still bill N misses.

Cross-process (opt-in, stampede.distributedLock): wires the previously
caller-less LockableBackend capability around the miss path, mirroring
cachekit-py's acquire_lock flow — acquire, double-check L2, compute,
write, release. Contested waiters retry the lock on an interval rather
than polling get() (a poll GET against a still-cold key is itself a
billed miss), then fall through to computing after lockWaitMs: the
lease is best-effort mitigation, never a correctness gate. Lock calls
stay outside the reliability executor so lock failures neither stack
retry latency nor open the circuit breaker for data operations.

Deliberately no general admission-control cap beyond L1's
maxConcurrentRefreshes: on Node's single-threaded event loop concurrent
misses don't compete for threads, single-flight collapses the per-key
herd, and distinct-key floods are bounded by backend timeouts plus the
circuit breaker. Decision recorded on StampedeConfig.

Co-authored-by: multica-agent <github@multica.ai>
Port LAB-519 single-flight + distributed-lock cold path onto the
cache-core.ts extraction from LAB-595 (#78): CacheImpl moved to the
shared core, so the single-flight map, stampede config validation, and
resolveMiss/resolveUnderLock/computeAndStore land there (now shared
with the Workers entrypoint). Backend construction stays in the
runtimes — CacheRuntime.resolveBackend grows an optional stampede
param so the Node runtime can select cachekitioWithLocking for apiKey
configs; the Workers runtime is deliberately untouched (lock-capable
backends remain an explicit construction there). StampedeConfig export
moves to exports-common.ts (platform-neutral surface).
…lab-517-wire-metrics

Stacks LAB-517 on LAB-519 per the LAB-761 merge plan (metrics observe
the cache, so they land on top; when #77 merges this PR's diff
collapses to metrics-only). Ports the metrics wiring onto the
cache-core.ts extraction from LAB-595:

- Instrumentation (run/execute/instrument, hit/miss/L1-stats recording)
  moves into the shared CacheImpl in cache-core.ts, composing with the
  LAB-519 single-flight cold path — followers share one recorded miss.
- cache-core imports metrics/prometheus.js TYPES only (prom-client is
  Node-only and dynamic import still enters esbuild's resolve graph);
  the collector is injected via a new optional CacheRuntime.createMetrics
  hook, with an inline no-op fallback, so Workers degrade to no-ops the
  same way a missing prom-client peer does on Node.
- resolveBackend gains an l1Telemetry getter param so the Node runtime
  auto-wires the SaaS X-CacheKit-L1-* metricsProvider from live
  counters, composed with the LAB-519 lockable-wrapper selection.
- setLogger/CachekitLogger move to exports-common.ts: logger.js is in
  the shared graph (background-refresh, invalidation) and Workers apps
  need the same error sink control. Prometheus exports stay Node-only.
- intents metrics?: boolean | MetricsConfig widening ports to
  intents-core.ts (type-only import, workers-safe).
@27Bslash6

Copy link
Copy Markdown
Contributor Author

Un-dirtied per the LAB-761 merge plan ("#75 rebases on #77"): this branch now stacks on lab-519-cold-miss-single-flight (which itself contains main). The diff will show #77's single-flight changes until #77 merges — review this PR after #77 lands and the diff collapses to metrics-only.

Port notes (main's LAB-595 refactor moved CacheImpl to the shared cache-core.ts):

  • Instrumentation (run/execute/instrument, hit/miss/L1-stats recording) lives in cache-core.ts and composes with single-flight — N cold followers share one recorded miss, matching metered-misses semantics.
  • cache-core imports metrics/prometheus.js types only (prom-client is Node-only; even a dynamic import enters esbuild's resolve graph). The collector is injected via a new optional CacheRuntime.createMetrics hook with a no-op fallback — Workers degrade to no-ops exactly like a missing prom-client peer does on Node.
  • resolveBackend gains an l1Telemetry getter so the Node runtime auto-wires the SaaS X-CacheKit-L1-* metricsProvider from live counters, composed with feat(cache): cold-miss single-flight + opt-in cross-process locking (LAB-519) #77's lockable-wrapper selection.
  • setLogger/CachekitLogger moved to exports-common.ts (logger.js is in the shared graph via background-refresh/invalidation); Prometheus value exports stay Node-only in index.ts.
  • metrics?: boolean | MetricsConfig widening ported to intents-core.ts.

Verified locally: type-check, lint, format, full vitest (588 passed — metrics + single-flight suites together), Workers test lane (54 passed), and check:workers-bundle (no prom-client/node:*/NAPI in the workers graph).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant