Sample Report
This page shows a genuine, unedited report generated by a PrismaLens investigation of a real Prometheus alert for a database connection pool regression in a live scenario. The purpose here is to show report quality and fidelity—the conclusion shown is the agent’s diagnosis, not a verified absolute truth.
The Setup
Section titled “The Setup”The investigation began when the following alert fired and was passed to PrismaLens:
{ "status": "firing", "alerts": [ { "status": "firing", "labels": { "alertname": "BooklogrApiLatencyP99High", "service": "booklogr-api" }, "annotations": { "summary": "P99 latency is high", "p99_seconds": "1.159" } } ]}The Report
Section titled “The Report”Below is the verbatim output of the investigation.
{ "summary": "[RAW — un-synthesized harness output; no Tier-1 provider configured]\n\n## Diagnosis — Root Cause Found\n\n**Alert:** BooklogrApiLatencyP99High (critical) — p99 = 2.287s vs 300ms threshold on booklogr-api.\n\n**Slow operation:** GET /v1/books (p99 2.249s, avg 0.536s, ~11.8 req/s — it is essentially the entire alert signal; every other path is <5ms).\n\n### Root cause\n**N+1 query in serialization, on top of a completely un-indexed schema.** Two compounding defects in the get_books handler path:\n\n1. **N+1 COUNT queries.** api/routes/books.py:273 calls books_schema.dump(books.items). BooksSchema (api/models.py:149-158) defines num_notes = ma.Method(\"get_num_notes\"), and get_num_notes runs a **separate** SELECT count(*) FROM notes WHERE book_id = :id **per book**. With per_page = 25 (books.py:227), one request = 1 list query + **25 extra COUNT queries** = 26 DB round-trips.\n\n2. **Zero indexes exist.** No migration creates any index (grep create_index migrations/versions/ → **NONE**; only primary keys exist). So:\n - each of those 25 notes COUNTs is a **sequential scan** on notes (no index on notes.book_id),\n - the main query (books.py:251) seq-scans books (no index on books.owner_id — it's a Postgres FK, which is *not* auto-indexed),\n - and it sorts by func.lower(title) (books.py:264), which can't use a plain index even if one existed.\n\n**Mechanism:** high traffic (11.8 rps) × 26 un-indexed queries/request → connection/CPU pressure on Postgres, driving avg to 0.5s and the p99 tail to ~2.3s. (Note: recent commit ba856ce \"Reduce DB connection footprint\" shrank the connection pool, which tightens the tail further under this query multiplication.)\n\n### Evidence\n**VALIDATED**\n- Firing rule/threshold — curl .../api/v2/alerts → p99 2.287s, threshold 300ms.\n- Slow path isolated — histogram_quantile(0.99, ... by (le,path,method) ...) → GET /v1/books 2.249s; all others ≤0.005s.\n- Avg latency 0.536s — rate(..._sum)/rate(..._count){path=\"/v1/books\"}.\n- N+1 code — api/models.py:153-158 get_num_notes does a per-object Notes.query.filter(...).count(), invoked by BooksSchema used at api/routes/books.py:250,273; page size 25 at books.py:227.\n- No indexes anywhere — grep -rc create_index migrations/versions/ → none.\n- FK not indexed — owner_id = db.Column(..., db.ForeignKey(\"profiles.owner_id\")) with no index=True (models.py:109).\n\n**INFERRED**\n- The N+1 (26 queries/req) is the dominant amplifier vs. the single missing-index scan; not directly measured (Flask exporter exposes no per-query DB metrics).\n- Interaction with the reduced connection pool worsening the tail — reasoned from commit ba856ce, not measured.\n\n### Recommended fix\n1. **Kill the N+1** — replace per-book get_num_notes counts with one aggregated query (group-by book_id COUNT joined to the page), or precompute num_notes via a subquery/func.count in the list query. Fastest interim mitigation: eager-load and count in memory, or drop num_notes from the list schema.\n2. **Add indexes** (migration): books(owner_id), notes(book_id), and a functional index lower(title) (plus books(owner_id, reading_status) for the status filter).\n3. Cast limit to int and cap it (books.py:229-230 currently passes a raw string and is unbounded) to prevent large-page blowups.\n\nRead-only investigation — no changes made.", "rootCause": null, "rootCauseCategory": null, "hypotheses": [], "ruledOut": [], "coverage": { "queried": [ "Bash({\"command\":\"curl -s 'http://localhost:9093/api/v2/alerts' | head -c 4000; echo; echo \\\"----RULES----\\\"; curl -s 'http://localhost:9090/api/v1/rules' | head -c 4000\",\"description\":\"Fetch firing alerts and alert rules\"})", "ToolSearch({\"query\":\"select:mcp__plugin_context-mode_context-mode__ctx_batch_execute,mcp__plugin_context-mode_context-mode__ctx_execute,mcp__plugin_context-mode_context-mode__ctx_search\",\"max_results\":5})", "mcp__plugin_context-mode_context-mode__ctx_execute({\"language\":\"shell\",\"code\":\"echo \\\"=== ALERTMANAGER ALERTS ===\\\"; curl -s 'http://localhost:9093/api/v2/alerts' | python3 -c \\\"import sys,json; d=json.load(sys.stdin); [print(a['labels'].get('alertname'),'|',a['labels'].get('severity'),'|',a['status']['state'],'|',a['annotations']) for a in d]\\\"\\necho \\\"=== PROMETHEUS FIRING RULES ===\\\"; curl -s 'http://localhost:9090/api/v1/rules' | python3 -c \\\"\\nimport sys,json\\nd=json.load(sys.stdin)\\nfor g in d['data']['groups']:\\n for r in g['rules']:\\n if r.get('type')=='alerting' and r.get('state')!='inactive':\\n print(r['name'],'|',r['state'],'|expr:',r['query'])\\n\\\"\"})", "mcp__plugin_context-mode_context-mode__ctx_execute({\"language\":\"shell\",\"code\":\"echo \\\"=== P99 by path/method ===\\\"\\ncurl -s 'http://localhost:9090/api/v1/query' --data-urlencode 'query=histogram_quantile(0.99, sum by (le,path,method) (rate(flask_http_request_duration_seconds_bucket{job=\\\"booklogr-api\\\"}[5m])))' | python3 -c \\\"\\nimport sys,json\\nd=json.load(sys.stdin)\\nrows=[]\\nfor r in d['data']['result']:\\n m=r['metric']; v=float(r['value'][1])\\n if v==v:\\n rows.append((v,m.get('method'),m.get('path')))\\nfor v,meth,p in sorted(rows,reverse=True)[:15]:\\n print(f'{v:.3f}s {meth} {p}')\\n\\\"\\necho \\\"=== request rate by path ===\\\"\\ncurl -s 'http://localhost:9090/api/v1/query' --data-urlencode 'query=sum by (path,method,status) (rate(flask_http_request_duration_seconds_count{job=\\\"booklogr-api\\\"}[5m]))' | python3 -c \\\"\\nimport sys,json\\nd=json.load(sys.stdin)\\nfor r in sorted(d['data']['result'],key=lambda x:-float(x['value'][1]))[:15]:\\n m=r['metric']; print(f\\\\\\\"{float(r['value'][1]):.3f}/s {m.get('method')} {m.get('path')} status={m.get('status')}\\\\\\\")\\n\\\"\"})", "mcp__plugin_context-mode_context-mode__ctx_batch_execute({\"commands\":[{\"label\":\"repo tree\",\"command\":\"find . -type f \\\\( -name '*.py' -o -name '*.env*' -o -name '*.cfg' -o -name '*.ini' -o -name '*.toml' \\\\) -not -path './.git/*' -not -path '*/node_modules/*' | head -60\"},{\"label\":\"books route grep\",\"command\":\"grep -rn \\\"/v1/books\\\\|def.*book\\\\|@.*route\\\\|books\\\" --include='*.py' . | grep -iv test | head -40\"}],\"queries\":[\"v1/books route handler definition\",\"books query database\",\"pagination limit books\"]})", "Read({\"file_path\":\"./api/routes/books.py\"})", "mcp__plugin_context-mode_context-mode__ctx_execute_file({\"path\":\"./api/models.py\",\"language\":\"shell\",\"code\":\"echo \\\"=== Books model + BooksSchema (context) ===\\\"; grep -n \\\"class Books\\\\|class BooksSchema\\\\|class BooksStatusSchema\\\\|__tablename__\\\\|owner_id\\\\|index=True\\\\|db.relationship\\\\|Method(\\\\|Function(\\\\|Nested(\\\\|get_progress\\\\|get_\\\\|def \\\" api/models.py | sed -n '1,80p'\"})", "mcp__plugin_context-mode_context-mode__ctx_execute({\"language\":\"shell\",\"code\":\"echo \\\"=== BooksSchema get_num_notes (models.py 143-170) ===\\\"; sed -n '143,170p' api/models.py\\necho \\\"=== Books model columns (97-120) ===\\\"; sed -n '97,120p' api/models.py\\necho \\\"=== index defs across migrations for books ===\\\"; grep -rn \\\"create_index\\\\|index=True\\\" migrations/ api/models.py | grep -i \\\"book\\\\|owner\\\" \"})", "mcp__plugin_context-mode_context-mode__ctx_execute({\"language\":\"shell\",\"code\":\"echo \\\"=== total create_index across all migrations ===\\\"; grep -rc \\\"create_index\\\" migrations/versions/ | grep -v ':0' || echo \\\"NONE — zero indexes created in any migration\\\"\\necho \\\"=== notes table def (initial migration) ===\\\"; grep -rn \\\"op.create_table('notes'\\\\|op.create_table('books'\\\" migrations/versions/\\necho \\\"=== avg latency GET /v1/books (prometheus) ===\\\"\\ncurl -s 'http://localhost:9090/api/v1/query' --data-urlencode 'query=rate(flask_http_request_duration_seconds_sum{job=\\\"booklogr-api\\\",path=\\\"/v1/books\\\"}[5m]) / rate(flask_http_request_duration_seconds_count{job=\\\"booklogr-api\\\",path=\\\"/v1/books\\\"}[5m])' | python3 -c \\\"import sys,json;d=json.load(sys.stdin);[print('avg latency:',round(float(r['value'][1]),3),'s') for r in d['data']['result']]\\\"\"})" ], "notQueried": [] }, "nextSteps": [], "fidelity": { "harness": "claude-code", "mode": "read-only", "fidelity": "enforced", "mechanism": "disallowedTools deny-list: Edit, Write, MultiEdit, NotebookEdit" }}How to Read It
Section titled “How to Read It”- The
[RAW — un-synthesized]marker: This run used NO API key—it is a raw pass-through of the agent’s findings. A raw pass-through is a supported subscription-only path. - VALIDATED vs INFERRED split: Notice how evidence is strictly ordered based on what was proven versus what is hypothesized. PrismaLens enforces an ADR-0002 “no-numeric-confidence” posture, ranking leads instead of faking a score.
- Evidence Lines: They carry real queries and file:line references directly from the codebase.
- Read-only closing note: The report clearly confirms that no changes were made to the system.
Configuring a provider key upgrades reports to a synthesized cross-branch summary—see the Providers & API keys page to get started.