Package
open-telemetry/opentelemetry-auto-symfony 1.4.0 (latest release)
Environment
- Symfony 5.4.45, PHP 8.2, php-fpm
open-telemetry/sdk 1.15.0, open-telemetry/api 1.10.0, ext-opentelemetry
- Exporter: OTLP http/protobuf → Grafana Alloy → Tempo
What happens
When a request fails with a 500 raised from a kernel.view listener, one HTTP request is
exported as two error spans in two different traces, and neither is complete:
| span |
kind |
name |
http.route |
http.response.status_code |
events |
| A |
SERVER |
DELETE |
absent |
absent |
exception |
| B |
INTERNAL |
DELETE api_customer_records_delete_item |
present |
500 |
none |
The two spans carry different trace_ids.
Counts match failures 1:1 — over 24h on two clusters: 2 failing requests produced 2 A-spans
and 2 B-spans; 1 failing request produced 1 and 1. Successful requests are unaffected: a
single SERVER span, correctly named {method} {route}.
Why it matters
- Span-derived RED metrics (
traces_spanmetrics_calls_total) bucket every 5xx under the
bare HTTP method, so server errors cannot be attributed to an endpoint.
- The span holding the exception is not the span holding the status code, and they are not
in the same trace, so no single trace shows a complete picture of the failure.
Where it appears to come from
In SymfonyInstrumentation::register() a scope is attached in the handle pre hook for
every request type, but detached in only two places:
handle post — returns early without detaching when $exception === null
(the normal path, including a request that produced a 500 response);
terminate post — pops exactly one scope, and that is where http.route and the
status code are stamped.
When the error is rendered through a sub-request, a second scope is attached and the stack
ends up unbalanced, so terminate appears to stamp the route and status onto the
sub-request span rather than the main SERVER span.
Caveat
The table above is measured; the explanation is inferred from reading
SymfonyInstrumentation.php — I have not stepped through the hook ordering to confirm it.
Happy to put together a minimal reproducer if that would help.
Package
open-telemetry/opentelemetry-auto-symfony1.4.0 (latest release)Environment
open-telemetry/sdk1.15.0,open-telemetry/api1.10.0, ext-opentelemetryWhat happens
When a request fails with a 500 raised from a
kernel.viewlistener, one HTTP request isexported as two error spans in two different traces, and neither is complete:
http.routehttp.response.status_codeDELETEexceptionDELETE api_customer_records_delete_itemThe two spans carry different
trace_ids.Counts match failures 1:1 — over 24h on two clusters: 2 failing requests produced 2 A-spans
and 2 B-spans; 1 failing request produced 1 and 1. Successful requests are unaffected: a
single SERVER span, correctly named
{method} {route}.Why it matters
traces_spanmetrics_calls_total) bucket every 5xx under thebare HTTP method, so server errors cannot be attributed to an endpoint.
in the same trace, so no single trace shows a complete picture of the failure.
Where it appears to come from
In
SymfonyInstrumentation::register()a scope is attached in thehandlepre hook forevery request type, but detached in only two places:
handlepost — returns early without detaching when$exception === null(the normal path, including a request that produced a 500 response);
terminatepost — pops exactly one scope, and that is wherehttp.routeand thestatus code are stamped.
When the error is rendered through a sub-request, a second scope is attached and the stack
ends up unbalanced, so
terminateappears to stamp the route and status onto thesub-request span rather than the main SERVER span.
Caveat
The table above is measured; the explanation is inferred from reading
SymfonyInstrumentation.php— I have not stepped through the hook ordering to confirm it.Happy to put together a minimal reproducer if that would help.