Skip to content

feat(metrics): add HTTP server metrics#30

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

feat(metrics): add HTTP server metrics#30
jstojiljkovic merged 11 commits into
tracewayapp:masterfrom
srekcud:feat/metrics-http-server

Conversation

@srekcud

@srekcud srekcud commented May 4, 2026

Copy link
Copy Markdown
Contributor

Adds OpenTelemetry metrics for incoming requests handled by the Symfony HttpKernel, sibling
of OpenTelemetrySubscriber on the trace side. Off by default.

Instruments

  • http.server.request.duration (Histogram, s) — semconv Stable
  • http.server.active_requests (UpDownCounter, {request}) — Development
  • http.server.request.body.size (Histogram, By) — Development
  • http.server.response.body.size (Histogram, By) — Development

Attributes: http.request.method, url.scheme, http.route (from the route template, not
the raw path), http.response.status_code, server.address, server.port, error.type on
failure.

Config

open_telemetry:
    metrics:
        enabled: true                                                                       
        http_server:
            enabled: true                                                                   
            excluded_paths: [/health, /_profiler, /_wdt]

Behaviour

  • Event subscriber bound to kernel.request / kernel.controller / kernel.response /
    kernel.exception / kernel.finish_request. Only main requests are measured (sub-requests are
    already inside the main duration).
  • Per-request state held in a WeakMap keyed by Request, cleaned at kernel.finish_request.
  • Route template substitution mirrors the trace subscriber — /api/items/{id} rather than
    /api/items/42.

Notes

  • Review patterns from Feat/metrics foundation #27 applied up-front: error.type uses FQCN with parent-class
    fallback, duration histogram declares second-based buckets, every event listener wraps its
    instrument calls in a swallowing try/catch so a broken meter provider cannot leak into
    Symfony's HttpKernel flow.
  • active_counted only flips when the +1 increment actually succeeded, so the up/down stays
    balanced even when the meter provider is broken on the increment path.
  • Same Util\ErrorTypeResolver as the parallel http-client and doctrine PRs; whichever merges
    first lands it on master, the others rebase clean.
  • Suite green, 23 new tests covering nominal emission, route templating, exception path,
    sub-request exclusion, body sizes, active count balance, broken-counter swallow.

@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 99.44134% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...EventSubscriber/OpenTelemetryMetricsSubscriber.php 99.29% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

namespace Traceway\OpenTelemetryBundle\EventSubscriber;

use OpenTelemetry\API\Globals;
use OpenTelemetry\API\Metrics\CounterInterface;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: imported but never used

srekcud added 10 commits May 10, 2026 15:19
Adds automatic metrics for incoming HTTP requests, sibling of the existing
OpenTelemetrySubscriber (trace side). Off by default, gated on
metrics.http_server.enabled which requires metrics.enabled.

Four instruments emitted on main requests only (sub-requests are already
included in the main request duration):

- http.server.request.duration       (Histogram, 's',  Stable)
- http.server.active_requests        (UpDownCounter, '{request}', Development)
- http.server.request.body.size      (Histogram, 'By', Development)
- http.server.response.body.size     (Histogram, 'By', Development)

Attributes follow OTel HTTP semantic conventions:
- http.request.method, url.scheme, server.address, server.port (required)
- http.route               (conditional, once routing resolves)
- http.response.status_code (conditional, on response)
- error.type               (conditional, on failure)

Body size histograms are emitted only when Content-Length is set.

Implementation mirrors ConsoleSubscriber: lazy MeterInterface resolved via
Globals::meterProvider(), instruments cached locally, ResetInterface to
clear cached state between worker iterations or long-running processes.
Per-request state lives in a WeakMap<Request, array> so dead requests are
garbage-collected naturally.

New nested config block:

    metrics:
        http_server:
            enabled: false
            excluded_paths: []

Validation rejects metrics.http_server.enabled without metrics.enabled.
Subscriber unit test (10 cases):
- subscribed events
- main request emits duration + active count
- route template substitution (/api/items/{id})
- exception adds error.type attribute
- excluded paths emit nothing
- sub-requests emit nothing
- body size histograms from Content-Length headers
- no body metrics without Content-Length
- active_requests increments on request, decrements on finish
- reset clears cached meter and pending request state

BundleBootTest additions:
- http_server disabled by default when metrics are enabled
- http_server enabled registers the subscriber
- validation rejects metrics.http_server.enabled without metrics.enabled
- Extends the Metrics section with an HTTP Server table listing the four
  instruments emitted, their stability status and attribute sets.
- Updates the main config YAML block with metrics.http_server.enabled and
  metrics.http_server.excluded_paths.
- Notes that only main requests are measured; sub-requests are already
  covered by the main request duration.
Same issue as the metrics foundation fix: iterator_to_array() on PHP 8.1
requires a strict Traversable, but the OTel SDK types dataPoints as
iterable which can be either an array or a Traversable. The spread
operator [...$x] accepts both and works on PHP 8.1+.
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.
…dling

Wrap each event listener's instrument calls in a swallowing try/catch
so that a broken meter provider cannot leak into Symfony's HttpKernel
flow. Track active_counted only when the counter increment succeeded
to keep up/down balance.
Replaces the inline anonymous-class fallback in the subscriber with
a reusable Util\ErrorTypeResolver, so the messenger middleware and
the HTTP client decorator can share the same canonical implementation.
Adds Configuration tests for the http_server.excluded_paths
normalization closure (mixed string/non-string entries, prepended
slash) and its default. Adds subscriber cases covering the data-null
short-circuits (every event firing on an untracked main request) and
extends the sub-request test to also exercise onRoute and onException.
@srekcud
srekcud force-pushed the feat/metrics-http-server branch from 4bc8b9e to d6d617c Compare May 10, 2026 13:38
@jstojiljkovic

Copy link
Copy Markdown
Collaborator

If you could just resolve conflits and I will merge it, thanks 🙏

…-server

# Conflicts:
#	README.md
#	src/DependencyInjection/Configuration.php
#	src/DependencyInjection/OpenTelemetryExtension.php
#	tests/Functional/BundleBootTest.php
@srekcud

srekcud commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

My bad i forgot a push 🙈

@jstojiljkovic
jstojiljkovic merged commit 9d626da into tracewayapp:master May 12, 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