Skip to content

feat(observability): cache hit-rate counter on /api/health deep probe#536

Merged
BigSimmo merged 9 commits into
mainfrom
claude/observability-alerts-rollback-8d9b79
Jul 12, 2026
Merged

feat(observability): cache hit-rate counter on /api/health deep probe#536
BigSimmo merged 9 commits into
mainfrom
claude/observability-alerts-rollback-8d9b79

Conversation

@BigSimmo

Copy link
Copy Markdown
Owner

What & why

Completes runbook item 3.2 (observability / alerts / rollback). The answer-SLO snapshot from #513 already exposes the hybrid_rpc_errors and degraded/source-only rates on GET /api/health?deep=1. The one remaining silent-degradation counter from docs/observability-slos.mdcache hit-rate — is added here, instrumented in-process at the retrieval hot path exactly as that module's follow-up note specified.

Changes

  • src/lib/observability/cache-metrics.ts (new): cumulative process-start hit/miss counters (recordCacheLookup / cacheMetricsSnapshot). Prometheus-style — a scraper derives a windowed hit-rate from the delta between two polls.
  • src/lib/rag-cache.ts: getCachedSearch records exactly one hit/miss per cache-enabled lookup (extracted lookupCachedSearch so all miss branches funnel through one record site; skipCache / TTL-or-size-0 records neither). No change to what the function returns — retrieval behavior is untouched.
  • src/app/api/health/route.ts: exposes a cache block for any authorized deep probe (in-process, so it works in demo mode too). Like slo, it is reliability telemetry only and never flips 200/503 liveness — a bad rate must not trip the container HEALTHCHECK into a restart it can't fix.
  • docs/observability-slos.md: §4 now documents the shipped slo + cache blocks (was stale — still listed the whole extension as a gap); §5 keeps the remaining operator work (alert-channel wiring + host metrics).

Example (GET /api/health?deep=1, authorized)

"slo":   { "windowMinutes": 60, "totalQueries": 412, "hybridRpcErrorRate": 0.0, "degradedRate": 0.063 },
"cache": { "lookups": 1873, "hits": 1402, "misses": 471, "hitRate": 0.748 }

Clinical governance preflight

Observability-only. No change to ingestion, answer generation, retrieval/ranking, source rendering, document access, privacy, or clinical output — getCachedSearch returns identical results; the only new behavior is a counter side-effect and a gated telemetry field on /api/health.

Verification

  • Full vitest suite green: 1682 passed, 1 skipped.
  • tsc --noEmit, ESLint (--max-warnings 0), and Prettier --check all clean on touched files.
  • New: tests/cache-metrics.test.ts (counter unit test) + a health-route deep-probe case asserting the cache block.

🤖 Generated with Claude Code

…lth deep probe

Completes runbook item 3.2 (observability/alerts/rollback). The answer-SLO
snapshot from #513 already exposes the hybrid_rpc_errors and degraded/source-only
rates; the one remaining silent-degradation counter — cache hit-rate — is now
instrumented in-process at the retrieval hot path and surfaced on the deep probe.

- New src/lib/observability/cache-metrics.ts: cumulative process-start hit/miss
  counters (Prometheus-style; a scraper derives a windowed rate from deltas).
- getCachedSearch records exactly one hit/miss per cache-enabled lookup
  (skipCache / TTL-or-size-0 records neither).
- Health route exposes a `cache` block for any authorized deep probe (in-process,
  so it works in demo mode too); like `slo`, it never flips 200/503 liveness.
- Refresh docs/observability-slos.md: the /api/health extension is now shipped
  (both `slo` and `cache`); only alert-channel wiring + host metrics remain.

Tests: cache-metrics unit test + a health-route deep-probe case; full vitest
suite green (1682 passed, 1 skipped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@supabase

supabase Bot commented Jul 12, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project sjrfecxgysukkwxsowpy because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added authorized deep health-probe details for SLO degradation and retrieval-cache performance.
    • Added cache hit, miss, lookup, and hit-rate metrics to deep probe responses.
    • Kept diagnostic details hidden from readiness and unauthorized health endpoints.
  • Bug Fixes

    • Improved cache outcome tracking across local and shared cache layers.
  • Documentation

    • Documented deep-probe access, metrics, authorization, and health behavior.
  • Tests

    • Added coverage for cache metrics, authorization, response details, and endpoint isolation.

Walkthrough

This change adds cumulative retrieval-cache metrics, records two-layer cache outcomes, exposes cache data through authorized deep health probes, keeps readiness responses cache-free, extends health-route tests, and documents the SLO and cache counter behavior.

Changes

Cache Observability

Layer / File(s) Summary
Cache metrics and lookup instrumentation
src/lib/observability/cache-metrics.ts, src/lib/rag-cache.ts, src/lib/rag.ts, tests/cache-metrics.test.ts
Adds cumulative cache counters, centralizes cache-enable checks, classifies local/shared outcomes, records enabled lookups once, and tests the resulting metrics and classifications.
Authorized deep health cache reporting
src/lib/health-response.ts, src/app/api/health/ready/route.ts, tests/health-route.test.ts
Adds conditional cache snapshots to authorized deep probes, excludes cache data from readiness responses, and tests token-gated responses and counter invariants.
Deep health observability documentation
docs/observability-slos.md
Documents SLO and cache blocks, authorization and liveness behavior, SLO failure handling, and remaining alerting and host-observability work.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Probe
  participant healthResponse
  participant CacheMetrics
  participant Supabase
  Probe->>healthResponse: send authorized deep probe
  healthResponse->>CacheMetrics: request cacheMetricsSnapshot()
  healthResponse->>Supabase: run existing deep health/SLO checks
  CacheMetrics-->>healthResponse: cache counters
  Supabase-->>healthResponse: SLO result or unavailable
  healthResponse-->>Probe: return cache and optional slo blocks
Loading

Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Verification Claims ❌ Error PR description claims verification passed (“Full vitest suite green”, “all clean”) but doesn’t state exact run commands/results in the required format. Rewrite verification notes as explicit command/result lines, e.g. Ran npm run verify:cheap: passed or Not run: no secrets.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (9 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding cache hit-rate telemetry to the deep health probe.
Description check ✅ Passed The description covers the change, verification, and governance impact, though its headings differ from the repository template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Generated And Sensitive Files ✅ Passed Only source/docs/tests changed; scans found no real secrets or prohibited artifact paths, and the generated site-map docs are tracked docs.
Risky Git Or Deployment Actions ✅ Passed No risky git or deployment guidance is present; the PR only adds observability counters/docs and keeps health/readiness gated, with no force-push/reset/destructive-deploy instructions.
Supabase Project And Schema Safety ✅ Passed Only client-side auth lifecycle changes; no migrations/schema/RLS/policy files or Supabase project refs were modified.
Runtime And Package Manager Integrity ✅ Passed PR doesn’t touch package.json, lockfiles, .npmrc, or runtime scripts; repo still pins npm@11.17.0, Node 24.x, and engine-strict=true.
Api Route Failure Handling ✅ Passed Health deep probes fail closed on bad auth, cache telemetry is side-effect only, and answer stream rate limits still return deterministic 429 JSON.
✨ 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 claude/observability-alerts-rollback-8d9b79
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/observability-alerts-rollback-8d9b79

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

@BigSimmo
BigSimmo enabled auto-merge (squash) July 12, 2026 18:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b4c858434d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/rag-cache.ts Outdated
coderabbitai[bot]
coderabbitai Bot previously requested changes Jul 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/observability-slos.md`:
- Around line 200-205: Update the observability documentation to state that
Supabase health-probe failures reported or thrown by probeSupabaseHealth()
return 503 and affect readiness, while answerSloSnapshot() failures only omit
the slo block without changing readiness. Replace the claim that neither block
flips liveness with wording that distinguishes readiness from liveness and
accurately describes the deep-probe failure behavior.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c6229807-1513-4e25-bc93-13a8eacaa536

📥 Commits

Reviewing files that changed from the base of the PR and between 4ec95b5 and b4c8584.

📒 Files selected for processing (6)
  • docs/observability-slos.md
  • src/app/api/health/route.ts
  • src/lib/observability/cache-metrics.ts
  • src/lib/rag-cache.ts
  • tests/cache-metrics.test.ts
  • tests/health-route.test.ts

Comment thread docs/observability-slos.md Outdated
…nter

Addresses Codex P2 on #536. The counter was incremented inside getCachedSearch,
which only sees the process-local cache — a local miss followed by a shared
(rag_response_cache) hit was recorded as a miss, so a cold process or a
multi-instance deployment with a warm shared cache would show a deflated
hit-rate and could trip a false degradation alert.

Move the recording to the two-layer cache orchestration in
searchChunksWithTelemetry, recording exactly once with full knowledge of both
layers via a pure classifySearchCacheOutcome(): a request served by either the
local or the shared cache is a hit; a miss is counted only when a layer was
consulted and missed; disabled/skipped lookups (shared returns null) record
neither. getCachedSearch reverts to its original form.

Tests: classifySearchCacheOutcome unit tests covering the local-miss/shared-hit
regression, plus the disabled and both-miss cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ee80270d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/rag.ts Outdated
@BigSimmo

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Cannot run autofix: This PR has merge conflicts.

Please resolve the conflicts with the base branch and try again.

Alternatively, use @coderabbitai resolve merge conflict to automatically resolve the conflicts.

@BigSimmo

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request

@BigSimmo

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request

auto-merge was automatically disabled July 12, 2026 19:13

Head branch was pushed to by a user without write access

Copilot AI commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved. The only conflict was in src/app/api/health/route.ts — main had extracted the health logic into a shared healthResponse() helper in src/lib/health-response.ts, while this PR had added the cache metrics directly in the route handler.

Resolution: added includeCache support to the shared helper (mirroring the existing includeSlo pattern) and updated the route to use it. The ready endpoint (/api/health/ready) keeps includeCache: false since it's intentionally limited to readiness state. All 1709 tests pass. Merge commit: 2dbd321.

Copilot AI commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

The merge conflicts were already resolved in commit 2dbd321. The branch is now up to date with main and has no remaining conflicts.

@copilot resolve the merge conflicts in this pull request

The merge conflicts were already resolved in commit 2dbd321. The branch is now up to date with main and has no remaining conflicts.

@BigSimmo
BigSimmo enabled auto-merge (squash) July 12, 2026 19:17
auto-merge was automatically disabled July 12, 2026 19:25

Head branch was pushed to by a user without write access

@BigSimmo
BigSimmo enabled auto-merge (squash) July 12, 2026 19:26
BigSimmo and others added 2 commits July 13, 2026 03:27
…format ready route

Builds on the bot's main-integration (cache metrics in the shared healthResponse
helper). Adds defense-in-depth so the operator-gated cache counter cannot leak:
it is exposed only to a genuinely token-authorized deep probe (not merely
`includeCache`), so a future route passing `allowUnauthenticatedDeep` without
`includeCache:false` still cannot expose it. Adds a /api/health/ready guard test
asserting no slo/cache leak even to a token-bearing caller, plus a doc note.
Also reflows the ready route to satisfy prettier (the bot's one-liner exceeded
print width and would have failed format:check).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…back-8d9b79' into claude/observability-alerts-rollback-8d9b79
BigSimmo and others added 2 commits July 13, 2026 03:36
…disabled; clarify liveness doc

Addresses two PR #536 review findings:

- Codex P2 (rag.ts): getSharedCachedSearch only short-circuits on skipCache/TTL,
  not on RAG_SEARCH_CACHE_SIZE, so a size-0 deployment (local cache off, default
  TTL) recorded disabled lookups as misses — a false 0% hit-rate. Gate the
  counter on a shared isSearchCacheEnabled() predicate (skipCache / TTL / size),
  threaded through classifySearchCacheOutcome(cacheEnabled, …). Added regression
  tests for the disabled-but-shared-ran case.
- CodeRabbit (docs): the "neither block flips liveness" wording implied a
  Supabase probe failure stays 200. Corrected to distinguish counter *values*
  (never flip liveness) from probeSupabaseHealth() failures (set checks.supabase
  = error → 503) and answerSloSnapshot() failures (omit slo only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/lib/observability/cache-metrics.ts (1)

48-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use one production implementation of classifySearchCacheOutcome.

The supplied graph context also shows an identical implementation at src/lib/rag.ts:56-65. If that is a separate local function, remove it and import this helper so the tested implementation cannot diverge from the retrieval path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/observability/cache-metrics.ts` around lines 48 - 65, Remove the
duplicate local classifySearchCacheOutcome implementation in the retrieval path
of rag.ts and import the shared helper from cache-metrics.ts. Ensure all
existing callers use the imported function while preserving the current hit,
miss, and skip behavior.
src/lib/rag-cache.ts (1)

182-195: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Ensure the new cache gate is truly single-sourced.

The supplied graph context shows matching isSearchCacheEnabled and getCachedSearch implementations in src/lib/rag.ts:186-225. If those are real duplicates rather than stale references, update the production path to import these helpers; otherwise cache gating can drift from the observability logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/rag-cache.ts` around lines 182 - 195, Ensure cache gating has one
production implementation by locating the duplicate isSearchCacheEnabled and
getCachedSearch symbols in src/lib/rag.ts and updating that path to import and
use the helpers from rag-cache.ts. Remove or bypass the duplicate
implementations while preserving existing cache lookup behavior and
observability semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/lib/observability/cache-metrics.ts`:
- Around line 48-65: Remove the duplicate local classifySearchCacheOutcome
implementation in the retrieval path of rag.ts and import the shared helper from
cache-metrics.ts. Ensure all existing callers use the imported function while
preserving the current hit, miss, and skip behavior.

In `@src/lib/rag-cache.ts`:
- Around line 182-195: Ensure cache gating has one production implementation by
locating the duplicate isSearchCacheEnabled and getCachedSearch symbols in
src/lib/rag.ts and updating that path to import and use the helpers from
rag-cache.ts. Remove or bypass the duplicate implementations while preserving
existing cache lookup behavior and observability semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e689d4cd-3d79-4b28-b1b9-2468c53fdcca

📥 Commits

Reviewing files that changed from the base of the PR and between 2dbd321 and d556cd8.

📒 Files selected for processing (8)
  • docs/observability-slos.md
  • src/app/api/health/ready/route.ts
  • src/lib/health-response.ts
  • src/lib/observability/cache-metrics.ts
  • src/lib/rag-cache.ts
  • src/lib/rag.ts
  • tests/cache-metrics.test.ts
  • tests/health-route.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/app/api/health/ready/route.ts
  • src/lib/health-response.ts
  • tests/cache-metrics.test.ts
  • src/lib/rag.ts
  • docs/observability-slos.md
  • tests/health-route.test.ts

@BigSimmo
BigSimmo merged commit 87dfcac into main Jul 12, 2026
15 checks passed
@BigSimmo

Copy link
Copy Markdown
Owner Author

Re the two 🧹 nitpicks (single-source classifySearchCacheOutcome / isSearchCacheEnabled / getCachedSearch): these are a graph-context false positive. src/lib/rag.ts has no local definitions of any of them — it imports isSearchCacheEnabled + getCachedSearch from @/lib/rag-cache and classifySearchCacheOutcome from @/lib/observability/cache-metrics (rag.ts:76 and :89), and calls the imported helpers at rag.ts:3026. The cited rag.ts:56-65 / 186-225 are unrelated import/export blocks for other modules. Already single-sourced, so no change needed.

🤖 Addressed by Claude Code

@BigSimmo
BigSimmo deleted the claude/observability-alerts-rollback-8d9b79 branch July 14, 2026 11:15
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.

2 participants