Skip to content

feat(api): export metrics over OTLP beside traces#227

Merged
mforce merged 2 commits into
mainfrom
feat/215-metrics
Jul 26, 2026
Merged

feat(api): export metrics over OTLP beside traces#227
mforce merged 2 commits into
mainfrom
feat/215-metrics

Conversation

@mforce

@mforce mforce commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Closes #215. Builds on #226 (merged): same Otlp:Endpoint gate, same eager validation, same fake-collector test rig.

What

  • .WithMetrics() beside the existing tracing: ASP.NET Core request metrics (http.server.request.duration, Kestrel), .NET runtime (GC, thread pool, exceptions), Npgsql and Microsoft.EntityFrameworkCore meters. New package: OpenTelemetry.Instrumentation.Runtime; lock files synced.
  • Endpoint unset = no exporter — behavior identical to today (meters still exist in-process for anyone who attaches, nothing leaves).
  • OtlpOptions.ResolveMetricsEndpoint/v1/metrics appended for http/protobuf through the same UriBuilder path as traces (shared ResolveSignalEndpoint; query strings survive, no double-append).
  • Fake collector v2 — captures bodies per signal path since traces + metrics arrive as separate POSTs in nondeterministic order; listener faults still surface through the wait instead of a generic timeout.
  • Boot line reports both signal endpoints; deploy/.env.example documents the meter set (issue AC) and notes that request-duration histograms include health probes — the metrics side has no per-request filter; aggregated and low-cardinality, unlike the per-probe spans feat(api): activate the telemetry pipeline — OTLP export, request logs, TraceId correlation #226 filters out.

Decision recorded in #215: OTLP push, no Prometheus scrape endpoint (single container behind Cloudflare — a scrape port would need its own auth story).

Tests

5 new: 4 endpoint-resolution facts (append-once, vendor base path, grpc untouched) + a metrics export smoke test (flush forced by host disposal, payload must carry the Cluckwork.Api service name). Traces smoke test re-verified through the collector rewrite.

Suite: 732 green, integration run twice, build clean (warnings-as-errors).

mforce added 2 commits July 25, 2026 20:14
- WithMetrics on the existing OTLP gate: ASP.NET Core request metrics
  (http.server.request.duration, Kestrel), .NET runtime (GC/threads/
  exceptions), Npgsql and EF Core meters. Endpoint unset = no exporter,
  unchanged behavior.
- OtlpOptions.ResolveMetricsEndpoint: /v1/metrics appended for http/protobuf
  via the same UriBuilder path as traces (shared ResolveSignalEndpoint).
- Fake collector captures per-signal paths — traces and metrics arrive as
  separate POSTs in nondeterministic order; listener faults still surface.
- Boot line now states both signal endpoints; .env.example documents the
  meter set and that request-duration histograms include health probes
  (metrics side has no per-request filter; aggregated + low-cardinality).

5 new tests; suite 732 green, integration run twice.
Review: codex + pi + 2 Claude agents. Accepted:

- Cross-signal endpoint foot-gun (agent, Important): a signal-suffixed
  Otlp:Endpoint ('.../v1/traces') would send metrics to /v1/traces/v1/metrics
  — silent 404 at the collector. http/protobuf endpoints must now be the BASE
  url; any known signal suffix fails at boot with the pointed fix. This
  supersedes #226's append-once tolerance (with two signals on one endpoint a
  suffixed URL can never be right).
- Smoke-test depth (codex): driver is now a login POST (real DB round-trip so
  Npgsql/EF instruments record) and the payload must carry one representative
  instrument per required source — http.server.request.duration,
  dotnet.gc.collections, db.client.operation.duration,
  microsoft.entityframeworkcore. Names taken from a real export dump.
- DRY (pi + cavecrew, converging): one ConfigureResource on the OTel builder,
  one exporter recipe closure, cached otlpMetricsEndpoint, uniform guards.
- Docs truth (agent): AddAspNetCoreInstrumentation subscribes ALL built-in
  ASP.NET Core meters (rate limiting, auth/Identity, memory pool included —
  this app uses the lot); .env.example and the code comment now say so, plus
  a note on temporality/interval env overrides for delta-preferring vendors.

Rejected: swapping OpenTelemetry.Instrumentation.Runtime for a bare
AddMeter("System.Runtime") — the package IS the framework facility and
keeps TFM differences its problem.

Resolution facts reworked to assert both signals per endpoint (symmetric
coverage); suite 734 green, integration run twice.
@mforce

mforce commented Jul 26, 2026

Copy link
Copy Markdown
Owner Author

Review round (repo policy: codex + 2 Claude agents + pi)

Accepted → fixed in 3ab4a56:

Finding Source Fix
Cross-signal endpoint foot-gun: Otlp:Endpoint=…/v1/traces sends metrics to /v1/traces/v1/metrics → silent collector 404 agent (Important) http/protobuf endpoints must be the BASE url — any known signal suffix now fails at boot with a pointed message; 4 rejection facts. Supersedes #226's append-once tolerance (two signals on one endpoint = suffixed URL can never be right)
Smoke test proved "a metrics payload", not the required meters codex, agent driver is a login POST (real DB round-trip); payload must carry http.server.request.duration, dotnet.gc.collections, db.client.operation.duration, microsoft.entityframeworkcore — names taken from a real export dump, not guessed
Resource + exporter config duplicated across signals; metrics endpoint resolved twice; guards inconsistent pi, cavecrew (5 findings, converging) one ConfigureResource on the OTel builder (the intended API), one exporter-recipe closure, cached otlpMetricsEndpoint, uniform is not null guards
Docs understate the meter set — AddAspNetCoreInstrumentation subscribes ALL 13 built-in ASP.NET Core meters (rate limiting, auth, Identity, memory pool incl.) agent (verified against Instrumentation.AspNetCore 1.17.0 source) .env.example + code comment now tell the truth; meters kept — this app uses rate limiting, JWT auth, and Identity, so those series are useful, and all are low-cardinality
No temporality/interval guidance for delta-preferring vendors agent (minor) .env.example documents the OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE / OTEL_METRIC_EXPORT_INTERVAL env overrides
Metrics resolution facts thinner than trace facts agent (minor) theories reworked to assert BOTH signals per endpoint row

Rejected: swapping OpenTelemetry.Instrumentation.Runtime for a bare AddMeter("System.Runtime") (agent minor) — the package is the framework facility; TFM differences stay its problem.

Reviewer verification highlights (agent 1 checked shipped assemblies, not docs): Npgsql 10.0.3 meter name Npgsql ✓, EF Core 10 meter Microsoft.EntityFrameworkCore ✓, Runtime 1.17.0 forwards to built-in System.Runtime meter with modern dotnet.* semconv ✓.

Suite after fixes: 734 green, integration run twice, build clean.

@mforce
mforce merged commit 021874c into main Jul 26, 2026
7 checks passed
@mforce
mforce deleted the feat/215-metrics branch July 26, 2026 03:49
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.

Observability: metrics — runtime, HTTP, and DB meters over OTLP

1 participant