Skip to content

feat(observability): poll_runs + "last polled" line, opt-in OpenTelemetry and /metrics (#359); 1.64.0 - #387

Merged
CryptoJones merged 2 commits into
mainfrom
feat/observability
Sep 27, 2026
Merged

CryptoJones merged 2 commits into
mainfrom
feat/observability

Conversation

@CryptoJones

Copy link
Copy Markdown
Owner

What

  • poll_runs (migration 0053): the Python poller writes one row per account per pass: started_at, finished_at, ats_only (fast lane), leads_added, errors ([{source, error}], only the sources that account draws on, plus its own signed-in sources and the pass itself). applytrack_poller gets INSERT only on it. users.last_polled_at (Feature: operator admin CLI (users, allowlist, usage, disable) and a Backup & restore runbook #358) stays the operator's cross-account stamp for admin usage; poll_runs is the per-pass detail. Pruned by the retention sweep after Retention__PollRunDays (30), never an account's newest pass of either kind.
  • GET /api/poll/status → {last_polled_at, last_leads_added, failing}. The failing list comes from the newest full pass. If a fast-lane pass is newer, the ATS-board entries come from that pass instead.
  • SPA: under the Applications heading, "Last polled 12 min ago · 2 sources failing", with the reasons in a <details> disclosure. It is vanilla JS, refreshes every minute, is not a live region, and keeps the disclosure open across refreshes.
  • Opt-in OpenTelemetry via OTEL_EXPORTER_OTLP_ENDPOINT: ASP.NET Core, HttpClient and Npgsql traces plus metrics. Unset, the SDK is not registered at all. Traces keep route templates (never the slug or query) and only scheme+host for outbound URLs. Telegram, which has its bot token in the URL, is not traced.
  • Prometheus /metrics only on Metrics__Port, checked against the connection's local port (a forged Host header does not open it). The app's port answers 404.
  • Meters: applytrack.llm.duration / applytrack.llm.errors, applytrack.agent.runs (provider, outcome, dry_run), applytrack.submit_requests.pending. Every tag comes from a fixed vocabulary. HttpClient metrics drop server.address and Npgsql metrics drop the pool name (the connection string).
  • README (config table, API, data model, new Observability section), .env.example and both compose files pass the new settings through, empty = off.

Tests

  • Python: run rows per tenant with only its own failing sources, a failed pass is recorded, a broken record never fails a poll, fast-lane scoping, PollRepo.record_run SQL.
  • .NET: ObservabilityTests. Covers: off by default (no MeterProvider, /metrics 404); scrape works on the metrics port only and ignores a spoofed Host; the scrape carries no tenant or host; bad port refused; URL composition; trace scrubbing; LLM metrics; /api/poll/status merge, tenant isolation and 401. Also: poller role grants (INSERT only on poll_runs), account-delete cascade, retention pruning.
  • Web: new axe + keyboard test for the status line. The mobile scroll test now swipes from the list tools: with the new line, the first card begins under the fixed action bar, and the workspace is the scroller either way.
  • Local: dotnet test 1132 passed; NuGet audit clean; ruff/mypy/bandit/pytest green; npm run test:web 80 passed.

Note: local pip-audit against uv.lock flags anyio 4.14.0 (fixed in 4.14.2). That is out of scope here.

Closes #359

Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/

🤖 Generated with Claude Code

https://claude.ai/code/session_01SXYffreKMhXLNgc2wdhsiw

…r pass — started/finished, ats_only, leads_added, and errors naming only that account's failing sources (0053; applytrack_poller gets INSERT on it and nothing else) — read by GET /api/poll/status and shown under the Applications heading as "Last polled 12 min ago · 2 sources failing", the reasons in a keyboard disclosure; pruned by Retention__PollRunDays (30, never the newest pass of either kind); opt-in OpenTelemetry via OTEL_EXPORTER_OTLP_ENDPOINT (ASP.NET Core, HttpClient, Npgsql traces with route templates, host-only outbound URLs, Telegram untraced) and a Prometheus /metrics served only on Metrics__Port, both off by default; meters for LLM latency and errors, agent runs by provider and outcome, and submit-queue depth, none carrying a tenant, host or URL (#359); 1.64.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXYffreKMhXLNgc2wdhsiw
@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

Next included review available in 38 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: bed358c0-84c3-4805-bdf1-67081fe2af35

📥 Commits

Reviewing files that changed from the base of the PR and between 5e4895e and 3855908.

📒 Files selected for processing (3)
  • README.md
  • api/ApplyTrack.Api.Tests/ObservabilityTests.cs
  • api/ApplyTrack.Api/Observability/Telemetry.cs
📝 Summary

Summary by CodeRabbit

  • New Features
    • The app now shows when polling last ran and provides expandable details for sources that failed.
    • Polling results and source errors are recorded for each pass, with configurable retention that preserves the newest full and ATS-only runs.
    • Optional OpenTelemetry tracing and metrics are available, including a Prometheus metrics endpoint on a separately configured port.
  • Documentation
    • Added configuration guidance for poll-run retention and optional observability settings.

Walkthrough

The change adds per-tenant poll-run history and retention, exposes poll status through an API and the web app, and adds opt-in OpenTelemetry and Prometheus instrumentation with configuration and tests.

Changes

Poll history and observability

Layer / File(s) Summary
Poll-run recording and retention
api/ApplyTrack.Api/Migrations/0053_poll_runs.sql, api/ApplyTrack.Api/Migrations/Always/poller_role.sql, src/applytrack/db.py, src/applytrack/worker.py, api/ApplyTrack.Api/Data/Retention.cs, api/ApplyTrack.Api.Tests/AccountEndpointTests.cs, api/ApplyTrack.Api.Tests/DbRoleTests.cs, api/ApplyTrack.Api.Tests/RetentionTests.cs, tests/test_poll.py, README.md, .env.example
The poller records each pass’s start time, type, lead count, and relevant errors. Retention prunes older records while keeping the newest full and ATS-only runs. Database permissions and deletion behavior are covered by tests.
Poll-status API and display
api/ApplyTrack.Api/Data/PollRunRepo.cs, api/ApplyTrack.Api/Endpoints/AppsEndpoints.cs, api/ApplyTrack.Api/Program.cs, api/ApplyTrack.Api/wwwroot/*, api/ApplyTrack.Api.Tests/ObservabilityTests.cs, tests/web/accessibility.spec.js, README.md
GET /api/poll/status returns the current tenant’s latest poll status and merged source failures. The web app displays the last poll time and failing sources, and refreshes the display while the page is visible.
Telemetry configuration and instrumentation
api/ApplyTrack.Api/Observability/*, api/ApplyTrack.Api/Agent/AgentWorker.cs, api/ApplyTrack.Api/Llm/OpenAiCompatibleLlmClient.cs, api/ApplyTrack.Api/ApplyTrack.Api.csproj, api/ApplyTrack.Api/Program.cs, api/ApplyTrack.Api.Tests/ObservabilityTests.cs, docker-compose*.yml, .env.example, README.md, BACKLOG.md, pyproject.toml, src/applytrack/__init__.py
Adds optional OTLP export and a Prometheus listener on a configured port. Application metrics cover LLM calls, agent runs, and submit-queue depth. Trace and metric filters, settings, tests, documentation, and version updates are included.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant PollWorker
  participant PollRepo
  participant PollRuns
  participant PollStatusApi
  participant WebApp
  PollWorker->>PollRepo: record pass results
  PollRepo->>PollRuns: insert tenant poll-run row
  WebApp->>PollStatusApi: GET /api/poll/status
  PollStatusApi->>PollRuns: read latest tenant passes
  PollRuns-->>PollStatusApi: pass times, lead counts, and errors
  PollStatusApi-->>WebApp: return poll status
Loading

Merge Risk: ⚪ Minimal · up to 5e489

No identified issue blocks merging after normal checks.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 5e489

Poll status remains tenant-scoped, but enabling the metrics port also creates another route to the application, and failed history writes can leave the displayed status stale. Telemetry privacy coverage is not yet complete. The default production configuration does not publish the metrics port.

Retained concerns

  • Medium · security · inferred: Enabling the metrics port adds a listener for the whole web application, not just /metrics. A network route granted for scraping can therefore also reach ordinary application paths, outside the usual reverse-proxy route; tenant authentication still applies to protected API paths.
  • Medium · reliability · inferred: A completed poll can update the operator stamp but fail to insert its status row. The new status API then reports older pass details, potentially concealing a newly failing source until another record succeeds.
Security review details

Security Blast Radius

  • inferred — When enabled, the wildcard metrics listener is reachable by peers that can access its container port. Production Compose does not publish it to the host, but access granted to an external scraper could also expose ordinary application paths through that port.

Security Findings and Attack Paths

  • inferred — A caller with network access to the metrics port can request non-metrics paths on that listener. This is an additional ingress route, not evidence of an authentication bypass: protected API requests still traverse the tenant middleware.

Trust Boundaries and Controls

  • observed — OTLP tracing is opt-in. Inbound URL scrubbing replaces paths with route templates and removes queries; outbound scrubbing retains scheme and authority, and Telegram-host requests are excluded from HttpClient tracing. Metric tag filtering is distinct from trace-attribute filtering.
  • observed — The status repository receives the authenticated tenant identity, and the SPA escapes source names and error text before displaying them.

Resilience and Maintainability Implications

  • inferred — History-write failures do not stop staging, but they can also prevent the status display from reflecting a completed pass. A staging failure after earlier inserts may be recorded with zero leads, because the failure path does not count partial writes.

Hardening Proposals

  • proposed — Restrict the metrics listener to scrape traffic at its ingress, or explicitly reject other paths on that port; verify the intended network ACL when publishing it to a scraper.
  • proposed — Inspect exported exception-path spans and default library trace attributes before treating URL helper tests as complete evidence that no dynamic or sensitive attributes reach the collector.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.04% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 81 functions across 18 files. (11 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main changes: poll-run status, opt-in OpenTelemetry, and the metrics endpoint. The version suffix adds detail but does not make the title unclear.
Description check ✅ Passed The description explains the poll-run records, status API and SPA, telemetry, metrics endpoint, configuration, and tests. It is directly related to the changeset.
Linked Issues check ✅ Passed PR #387 meets the coding objectives in [#359]. It adds per-tenant poll_runs records, retention, tenant-scoped status API access, and SPA display of the last poll and failing sources. It adds opt-in …
Out of Scope Changes check ✅ Passed The documented code, tests, configuration, and README changes support [#359]. Version updates and the backlog entry accompany the observability release. The summary does not establish any unrelated ch…
Full details: Docstring Coverage

Explanation

Docstring coverage is 37.04% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 81 functions across 18 files. (11 skipped: 11 unsupported.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai 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.

🔇 Additional comments (28)
api/ApplyTrack.Api/Migrations/0053_poll_runs.sql (1)

11-21: LGTM!

api/ApplyTrack.Api/Migrations/Always/poller_role.sql (1)

34-35: LGTM!

src/applytrack/db.py (1)

196-213: LGTM!

api/ApplyTrack.Api/Data/Retention.cs (1)

137-149: LGTM!

api/ApplyTrack.Api.Tests/AccountEndpointTests.cs (1)

207-223: LGTM!

api/ApplyTrack.Api.Tests/DbRoleTests.cs (1)

78-87: LGTM!

api/ApplyTrack.Api.Tests/RetentionTests.cs (1)

187-211: LGTM!

tests/test_poll.py (1)

1518-1629: LGTM!

README.md (1)

1118-1150: LGTM!

api/ApplyTrack.Api/Data/PollRunRepo.cs (1)

1-79: LGTM!

api/ApplyTrack.Api/Endpoints/AppsEndpoints.cs (1)

140-143: LGTM!

api/ApplyTrack.Api/Program.cs (1)

85-86: LGTM!

Also applies to: 229-233, 359-361

api/ApplyTrack.Api/wwwroot/index.html (1)

70-73: LGTM!

api/ApplyTrack.Api/wwwroot/app.js (1)

3734-3773: LGTM!

api/ApplyTrack.Api/wwwroot/app.css (1)

238-243: LGTM!

api/ApplyTrack.Api.Tests/ObservabilityTests.cs (1)

1-302: LGTM!

tests/web/accessibility.spec.js (1)

120-130: LGTM!

Also applies to: 1288-1302

.env.example (1)

109-111: 🎯 Functional Correctness

The Compose files already map METRICS_PORT to Metrics__Port, so the proposed rename is unnecessary.

api/ApplyTrack.Api/Observability/AppMetrics.cs (1)

1-78: LGTM!

api/ApplyTrack.Api/Llm/OpenAiCompatibleLlmClient.cs (1)

59-77: LGTM!

api/ApplyTrack.Api/Agent/AgentWorker.cs (1)

267-271: LGTM!

Also applies to: 740-740, 760-760, 777-777, 787-787

api/ApplyTrack.Api/ApplyTrack.Api.csproj (1)

8-8: LGTM!

Also applies to: 20-25

docker-compose.production.yml (1)

51-60: LGTM!

Also applies to: 111-115

docker-compose.yml (1)

49-59: LGTM!

Also applies to: 117-121

pyproject.toml (1)

7-7: LGTM!

src/applytrack/__init__.py (1)

5-5: LGTM!

BACKLOG.md (1)

49-49: LGTM!

api/ApplyTrack.Api/Observability/Telemetry.cs (1)

92-96: 🔒 Security & Privacy | 🛡️ Detected with Advanced Tier | 🟠 Major | ⚡ Quick win

Sensitive Data Exposure

Reachability: External
Exploitability: Moderate
CWE: CWE-532 — Insertion of Sensitive Information into Log File

⚠️ Unverified finding
Verification did not complete.

Scrub the inbound span on requests that end in an exception.

EnrichWithHttpResponse runs only when a response is produced through the normal path. The ASP.NET Core instrumentation sets url.path and url.query when the request starts. If a request throws, the EnrichWithException path can end the span before ScrubRequest runs. The exported span then keeps the raw path, for example a tenant slug, and the query, for example ?token=. This contradicts the stated contract that traces "never" carry the raw path or query. Call ScrubRequest from EnrichWithHttpRequest as well, or from EnrichWithException, so every span is scrubbed. The route endpoint is resolved later, so keep the call in EnrichWithHttpResponse too.

Proposed fix
                 .AddAspNetCoreInstrumentation(o =>
                 {
                     o.Filter = ctx => !ctx.Request.Path.StartsWithSegments(MetricsPath);
+                    o.EnrichWithHttpRequest = (activity, request) => ScrubRequest(activity, request.HttpContext);
                     o.EnrichWithHttpResponse = (activity, response) => ScrubRequest(activity, response.HttpContext);
+                    o.EnrichWithException = (activity, _) =>
+                    {
+                        if (activity.GetTagItem("url.query") is not null || activity.GetTagItem("url.path") is not null)
+                            activity.SetTag("url.query", null);
+                    };
                 })

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 95066b2d-7029-4b13-ad55-68ad638b4fbd

📥 Commits

Reviewing files that changed from the base of the PR and between 823875b and 5e4895e.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • .env.example
  • BACKLOG.md
  • README.md
  • api/ApplyTrack.Api.Tests/AccountEndpointTests.cs
  • api/ApplyTrack.Api.Tests/DbRoleTests.cs
  • api/ApplyTrack.Api.Tests/ObservabilityTests.cs
  • api/ApplyTrack.Api.Tests/RetentionTests.cs
  • api/ApplyTrack.Api/Agent/AgentWorker.cs
  • api/ApplyTrack.Api/ApplyTrack.Api.csproj
  • api/ApplyTrack.Api/Data/PollRunRepo.cs
  • api/ApplyTrack.Api/Data/Retention.cs
  • api/ApplyTrack.Api/Endpoints/AppsEndpoints.cs
  • api/ApplyTrack.Api/Llm/OpenAiCompatibleLlmClient.cs
  • api/ApplyTrack.Api/Migrations/0053_poll_runs.sql
  • api/ApplyTrack.Api/Migrations/Always/poller_role.sql
  • api/ApplyTrack.Api/Observability/AppMetrics.cs
  • api/ApplyTrack.Api/Observability/Telemetry.cs
  • api/ApplyTrack.Api/Program.cs
  • api/ApplyTrack.Api/wwwroot/app.css
  • api/ApplyTrack.Api/wwwroot/app.js
  • api/ApplyTrack.Api/wwwroot/index.html
  • docker-compose.production.yml
  • docker-compose.yml
  • pyproject.toml
  • src/applytrack/__init__.py
  • src/applytrack/db.py
  • src/applytrack/worker.py
  • tests/test_poll.py
  • tests/web/accessibility.spec.js

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: Python — lint + test + audit
  • GitHub Check: .NET — test + audit
  • GitHub Check: Web — WCAG checks
🧰 Additional context used
📓 Path-based instructions (2)
Source excerpt: **Core API: .NET 10** — ASP.NET Core Minimal APIs on Kestrel.

📄 CodeRabbit inference engine (CLAUDE.md)

Files:

  • api/ApplyTrack.Api/Program.cs
Source excerpt: `api/` — the .NET solution (`ApplyTrack.Api`), with the SPA in `wwwroot/`.

📄 CodeRabbit inference engine (CLAUDE.md)

Files:

  • api/ApplyTrack.Api/wwwroot/app.css
  • api/ApplyTrack.Api/wwwroot/index.html
  • api/ApplyTrack.Api/wwwroot/app.js
🪛 ast-grep (0.45.3)
api/ApplyTrack.Api/wwwroot/app.js

[warning] 3763-3766: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: el.innerHTML = <span>${when}</span>${n ? · <details class="poll-failing"${wasOpen ? " open" : ""}>

${n} source${n === 1 ? "" : "s"} failing
    ${failing.map((f) => <li><span class="mono">${escapeHtml(f.source)}</span>: ${escapeHtml(f.error)}</li>).join("")}

: ""}
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)


[warning] 3771-3771: Avoid using the initial state variable in setState
Context: setInterval(() => { if (!document.hidden && !$("#poll-status")?.hidden) showPollStatus(); }, POLL_STATUS_MS)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🪛 OpenGrep (1.30.0)
api/ApplyTrack.Api/wwwroot/app.js

[WARNING] 3764-3767: Setting innerHTML with dynamic content can lead to XSS. Use textContent or createElement with proper escaping instead.

(coderabbit.xss.innerhtml-assignment)

🪛 Squawk (2.64.0)
api/ApplyTrack.Api/Migrations/0053_poll_runs.sql

[warning] 17-17: Using 32-bit integer fields can result in hitting the max int limit. Use 64-bit integer values instead to prevent hitting this limit.

(prefer-bigint-over-int)

… — every other path there is a 404, so a route opened for a scraper is not a second way into the app around its proxy; inbound spans are scrubbed when the request starts as well as at its response, so one that throws first keeps neither the raw path nor the query (CodeRabbit on #387) (#359)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXYffreKMhXLNgc2wdhsiw
@CryptoJones
CryptoJones merged commit 5ed4ef8 into main Sep 27, 2026
5 checks passed
@CryptoJones
CryptoJones deleted the feat/observability branch September 27, 2026 07:20
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.

Feature: observability — opt-in OpenTelemetry, /metrics, and a poll_runs table ("last polled 12 min ago · 2 sources failing")

1 participant