Skip to content

feat(api): expose application business metrics via RoadRunner - #226

Merged
vokomarov merged 2 commits into
masterfrom
feat/prometheus-app-metrics
Aug 28, 2026
Merged

feat(api): expose application business metrics via RoadRunner#226
vokomarov merged 2 commits into
masterfrom
feat/prometheus-app-metrics

Conversation

@vokomarov

Copy link
Copy Markdown
Contributor

Problem statement

The API emitted only RoadRunner framework metrics. There was no visibility into application-level behaviour — auth outcomes, user growth and engagement, transactional activity, or database load — so dashboards and alerts could not be built on anything the product actually does.

Changes

Adds application business metrics on top of the RoadRunner metrics plugin, served on the existing :2112 endpoint.

New series:

Metric Type Labels
app_db_queries_total counter
app_db_query_duration_seconds histogram
app_auth_logins_total counter method, result
app_auth_registrations_total counter result
app_users gauge state (verified/unverified)
app_users_active gauge window (daily/weekly/monthly)
app_charges_created_total / app_charges_deleted_total counter type
app_wallets_created_total / app_wallets_archived_total counter
app_tags_created_total counter
app_tag_assignments_total counter

Implementation notes:

  • Collectors are declared statically in .rr.yaml (metrics.collect:) — the plugin registers them once before workers boot, so there is no per-worker declare() race flooding the logs.
  • AppMetrics is a thin typed facade over MetricsInterface (App\Service\Metrics). Every emission is wrapped so a metrics outage cannot break a user request; MetricsCollectConfigTest fails the build on any drift between AppMetrics::definitions() and the YAML.
  • NullMetrics is bound when APP_ENV=testing (no RR RPC relay in tests).
  • DB query count/latency are fed by a Cycle\Database\LoggerFactoryInterface decorator that reads only the elapsed value — no SQL text is stored.
  • app_users / app_users_active are not event-driven, so a schedule:work service block in .rr.yaml runs AppUserMetricsRefresher every 5 minutes to recompute and push them.
  • New non-unique index on users.active_at (migration + entity annotation) keeps the active-user COUNT queries index-backed.

Emission points wired into the four Auth controllers, GoogleAuthService, ChargeWalletService, WalletService, and TagService.

Verification

  • Independent code review over 4 rounds; final round returned no material findings. Confirmed against the RoadRunner metrics v5.1.4 and Spiral roadrunner-metrics sources that the implementation uses the documented approach (static collect: + MetricsInterface add/set/observe) and not a custom transport.
  • composer checks — new suites green: tests/Unit/Service/Metrics (25), tests/Feature/Metrics (18), UserRepositoryTest (3). phpcs (PSR-12) and psalm (strict) clean. The only failures are three pre-existing, unrelated tests (JwksControllerTest ×2, InternalHeadersMiddlewareTest), reconfirmed by reproducing them against a clean checkout.
  • Migration applies and rolls back cleanly against the real database.
  • Isolated rr serve: zero collector-declare warnings at boot; schedule:run populates app_users / app_users_active end to end; /metrics on :2112 exposes all twelve series.

Note for the deploy

compose.app.yml caps the api container at mem_limit: 768m with 6 HTTP workers budgeted. This change adds a resident schedule:work process plus a transient per-minute schedule:run kernel boot. Worth watching OOM headroom on first deploy, or raising the limit pre-emptively.

Add application-level Prometheus metrics on top of the RoadRunner metrics
plugin, served on :2112 next to the existing framework metrics.

Metrics:
- app_db_queries_total / app_db_query_duration_seconds - SQL statement
  count and latency, fed by a Cycle database logger decorator
- app_auth_logins_total{method,result} and
  app_auth_registrations_total{result} - auth outcomes across the
  password, Google and passkey flows
- app_users{state} - verified/unverified user counts
- app_users_active{window} - DAU/WAU/MAU derived from users.active_at
- app_charges_created_total{type} / app_charges_deleted_total{type}
- app_wallets_created_total / app_wallets_archived_total
- app_tags_created_total / app_tag_assignments_total

Collectors are declared statically in .rr.yaml (metrics.collect:) so the
plugin registers them once before any worker boots, avoiding a per-worker
declare race. AppMetrics is a thin typed facade over MetricsInterface;
emission failures are swallowed so a telemetry outage can never break a
user request. NullMetrics is bound when APP_ENV=testing.

The DB-derived gauges are not event-driven, so a schedule:work service in
.rr.yaml runs a 5-minute job that recomputes and pushes them. Adds a
non-unique index on users.active_at to keep the active-user COUNT queries
index-backed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GG9BBhSh7sJsKc2PBaacJX
@codecov

codecov Bot commented Aug 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.69%. Comparing base (1a294bc) to head (22ff37b).

Additional details and impacted files
@@             Coverage Diff              @@
##             master     #226      +/-   ##
============================================
+ Coverage     99.68%   99.69%   +0.01%     
- Complexity     1431     1480      +49     
============================================
  Files           221      227       +6     
  Lines          4815     4981     +166     
============================================
+ Hits           4800     4966     +166     
  Misses           15       15              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Close the Codecov patch-coverage gaps on the application-metrics change:

- AppMetrics: add tests for the swallowed-and-logged failure paths of
  observe() and set(), matching the existing add() failure test.
- NullMetrics: assert every operation is a no-op that never throws.
- MetricsBootloader: exercise the MetricsInterface factory closure in both
  branches (NullMetrics under APP_ENV=testing, SuppressExceptionsMetrics
  otherwise), and run the registered `refresh-app-user-metrics` scheduler
  callback end to end against the real repository.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GG9BBhSh7sJsKc2PBaacJX
@vokomarov
vokomarov merged commit 9b9b35e into master Aug 28, 2026
8 checks passed
@vokomarov
vokomarov deleted the feat/prometheus-app-metrics branch August 28, 2026 14:11
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