Skip to content

feat(metrics): add HTTP client metrics#29

Merged
jstojiljkovic merged 9 commits into
tracewayapp:masterfrom
srekcud:feat/metrics-http-client
May 11, 2026
Merged

feat(metrics): add HTTP client metrics#29
jstojiljkovic merged 9 commits into
tracewayapp:masterfrom
srekcud:feat/metrics-http-client

Conversation

@srekcud

@srekcud srekcud commented May 4, 2026

Copy link
Copy Markdown
Contributor

Adds OpenTelemetry metrics for outgoing requests through the Symfony HttpClient, sibling of
TraceableHttpClient on the trace side. Off by default.

Instruments

  • http.client.request.duration (Histogram, s) — semconv Stable
  • http.client.request.body.size (Histogram, By) — Development
  • http.client.response.body.size (Histogram, By) — Development

Attributes: http.request.method, server.address, server.port, url.scheme,
http.response.status_code on response, error.type on transport failure (4xx/5xx are not
synthesised as errors — alert on status_code >= 400 instead).

Config

open_telemetry: 
    metrics:
        enabled: true
        http_client:
            enabled: true
            excluded_hosts: []   # OTLP endpoint is auto-excluded                           

Behaviour

  • Decorator pattern: MeteredHttpClient wraps any HttpClientInterface, MeteredResponse
    finalises metrics lazily when the caller reads status/headers/content.
  • Same OTLP-endpoint auto-exclusion and re-entrance guard as the trace decorator — the OTLP
    exporter using this client doesn't recurse.

Notes

  • Review patterns from Feat/metrics foundation #27 applied up-front: error.type uses FQCN with parent-class fallback
    for anonymous classes, duration histogram declares second-based buckets, every recording
    path is wrapped in a swallowing try/catch (request-body, response, failure) so a broken
    meter provider cannot mask the original transport exception. inFlight is reset in a finally.
  • Introduces Util\ErrorTypeResolver for the FQCN/anonymous fallback. The same class is
    duplicated by the parallel http-server and doctrine PRs; whichever merges first lands it on
    master, the others rebase clean.
  • 11 new HttpClient tests + 4 ErrorTypeResolver tests, suite green.

@codecov-commenter

codecov-commenter commented May 4, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 96.59574% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/HttpClient/MeteredHttpClient.php 96.77% 4 Missing ⚠️
src/HttpClient/MeteredResponse.php 92.98% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread src/HttpClient/MeteredHttpClient.php Outdated
srekcud added 9 commits May 10, 2026 15:12
Adds automatic metrics for outgoing HTTP requests via a decorator of
Symfony's HttpClientInterface, sibling of the existing TraceableHttpClient
on the trace side. Off by default, gated on metrics.http_client.enabled
which requires metrics.enabled.

Three instruments emitted on every outgoing request:

- http.client.request.duration       (Histogram, 's',  Stable)
- http.client.request.body.size      (Histogram, 'By', Development)
- http.client.response.body.size     (Histogram, 'By', Development)

Attributes follow OTel HTTP client semantic conventions:
- http.request.method, server.address, server.port (required)
- url.scheme                (conditional, when present in URL)
- http.response.status_code (on response)
- error.type                (on transport failure)

Request body size is extracted from explicit Content-Length header or
string body. Response body size comes from the response Content-Length
header or the fully-consumed body. Transport failures (connection
refused, timeouts, etc.) record error.type with duration; 4xx/5xx
responses are recorded with status_code (no synthesized error.type).

Implementation:
- MeteredHttpClient decorator: lazy MeterInterface cached locally, same
  re-entrance guard as TraceableHttpClient, same excluded_hosts list,
  OTLP endpoint auto-excluded to prevent instrumentation loops.
- MeteredResponse wrapper: mirrors TracedResponse. Records metrics on
  first access to getStatusCode / getHeaders / getContent (Symfony
  responses are lazy). __destruct falls back to a best-effort finalize.
- HttpClientMetricsPass: compiler pass mirroring HttpClientTracingPass,
  decorates 'http_client' and all 'http_client.client' tagged services
  at priority -8 (outer wrapper of the tracing decorator at -16) so the
  recorded duration reflects the full outgoing call as the app sees it.

New nested config block:

    metrics:
        http_client:
            enabled: false
            excluded_hosts: []

Validation rejects metrics.http_client.enabled without metrics.enabled.
Unit test for MeteredHttpClient (13 cases):
- duration emission with required OTel attributes
- response body size from Content-Length header
- request body size from explicit Content-Length header
- request body size from string body when no header
- no body metric when nothing measurable
- 4xx status code recorded without synthesized error.type
- transport failure (thrown exception) adds error.type
- excluded host skips emission entirely
- OTLP endpoint (OTEL_EXPORTER_OTLP_ENDPOINT) auto-excluded
- request() returns a MeteredResponse instance
- withOptions() returns a new instance preserving the decorator
- stream() unwraps MeteredResponse objects before delegating
- reset() clears the cached meter

BundleBootTest additions:
- http_client metrics disabled by default when metrics are enabled
- http_client metrics enabled sets container parameters for the pass
- validation rejects metrics.http_client.enabled without metrics.enabled
- Adds an HTTP Client table to the Metrics section listing the three
  instruments emitted, their stability status and attribute set.
- Updates the main config YAML block with metrics.http_client.enabled
  and metrics.http_client.excluded_hosts, mentioning the OTLP auto-
  exclusion.
- Notes that connection-pool metrics (http.client.open_connections,
  http.client.connection.duration, http.client.active_requests) are
  out of scope because Symfony HttpClient does not expose the pool.
Use FQCN per OTel semconv with a parent-class fallback for anonymous
classes, mirroring the messenger middleware fix.
…est.duration

The OTel SDK defaults are tuned for milliseconds and collapse all
sub-5s requests into the first bucket, making p95/p99 unusable.
Swallow exceptions from the body-size and duration histograms, and
move inFlight reset into a finally so a broken metric provider cannot
replace the original transport exception.
Replaces the inline anonymous-class fallback in MeteredHttpClient with
a reusable Util\ErrorTypeResolver, so the HTTP server subscriber and
the messenger middleware can share the same canonical implementation.
Adds MeteredResponseTest covering every public response method (header
finalisation with/without Content-Length, content/toArray success and
transport-failure paths, cancel, getInfo, getInnerResponse, double
finalisation idempotency, destructor fallback). Adds
HttpClientMetricsPassTest covering decoration of the default and tagged
HTTP clients, the disabled and missing-parameter early-returns, and the
empty-excluded-hosts fallback. Adds MeteredHttpClient cases for the
inFlight re-entrance short-circuit and reset() clearing the flag.
Without the SplObjectStorage map, chunks yielded by the inner client are
keyed on the bare response, so RetryableHttpClient (or any decorator
above this one) cannot look up the wrapper it handed in and Symfony's
AsyncResponse throws "UnexpectedValueException: Object not found".
Mirrors the fix applied to TraceableHttpClient in tracewayapp#35.
@srekcud
srekcud force-pushed the feat/metrics-http-client branch from 9d0bd1b to 11ac2e9 Compare May 10, 2026 13:38
@jstojiljkovic
jstojiljkovic self-requested a review May 11, 2026 09:37
@jstojiljkovic
jstojiljkovic merged commit 5c474c0 into tracewayapp:master May 11, 2026
5 checks passed
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.

3 participants