[Bug]: DD_TRACE_OTEL_ENABLED: OTel spans in sampling-rejected traces leave an unclosed empty-name ddtrace span (orphans; extra memory in FrankenPHP worker mode)
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 52/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Đặc tả rõ ràng
- Mức độ hoạt động
- Sôi nổi
- Công nghệ
- php
- Lĩnh vực
- backend, observability
Hướng nghiên cứu
Start with src/DDTrace/OpenTelemetry/SpanBuilder.php, especially startSpan() around the sampling decision and the drop branch; then read the cited context handling in src/DDTrace/OpenTelemetry/Context.php. Reproduce the rejected sampling case using the linked standalone script and check whether dropped spans remain open. Done means rejected traces have no empty-name orphan spans or state persisting across worker requests.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Bug report
With DD_TRACE_OTEL_ENABLED=true and a sampling rule that rejects the trace (e.g. DD_TRACE_SAMPLING_RULES=[{"sample_rate":0.05}]), every OpenTelemetry-API span started in a rejected trace leaves behind an unclosed ddtrace span with an empty name and resource, whose parent isn't in the trace. We expect the same with Adaptive Sampling, which uses sampling rules. Kept traces and 100%-sampled traces are clean.
In FrankenPHP worker mode the leaked state survives the request:
- Memory grows faster.
- In some setups, one trace per worker accumulates hundreds of these spans for the worker's lifetime.
Source (tag 1.26.1):
src/DDTrace/OpenTelemetry/SpanBuilder.php#L190and#L210:startSpan()opens a ddtrace span with\DDTrace\start_trace_span()before and independent of the sampling decision (shouldSample()at L200).#L242-L246: when the decision is neitherRECORD_AND_SAMPLEnorRECORD_ONLY, it returnsSpan::wrap($spanContext)before setting$span->resource = $this->spanName(L246). Nothing closes the ddtrace span opened above, so it stays open with an empty name.src/DDTrace/OpenTelemetry/Context.php#L228:self::storage()->attach($currentContext); // TODO: Handle Detach. In the repro the OTel context storage still held a context between requests.- Rejected (priority −1) traces produce unsampled OTel parent contexts. The trace flags come from the ddtrace
traceparent,Context.php#L186-L188. So the SDK's defaultParentBasedsampler drops each child span, which takes the drop branch above. The repro loggedstartSpan() returned OpenTelemetry\API\Trace\NonRecordingSpan, priority=-1.
Standalone reproduction: https://gist.github.com/Ardenexal/35129bc3bddd96386c6c063e635af496 (FrankenPHP worker script plus open-telemetry/sdk 1.14.0 from Packagist, no framework; bash run.sh):
- An OTel
hook()on an interface method starts and ends an internal span. The pre-hook doesspanBuilder()->setParent(Context::getCurrent())->startSpan()plusContext::storage()->attach(); the post-hook doesdetach()plusend(). 3 hooked calls per request. - 8 workers.
- Trace sink:
ddapm-test-agentv1.66.0 sharing the network namespace. - 3,000 requests at
ab -c 8per variant, each in a fresh container.
| Variant | Traces | One-root traces | Empty-name spans | Priorities | RSS per 500 requests |
|---|---|---|---|---|---|
| 5% rule, bridge on | 3,000 | 156 | 2,844 (every rejected trace has one, as an orphan root) | −1: 2,844, 2: 156 | +8.1 MiB |
| 100%, bridge on (control) | 3,000 | 3,000 | 0 | 1: 3,000 | +3.8 MiB |
5% rule, DD_TRACE_OTEL_ENABLED=false (control) |
3,000 | 3,000 | 0 | −1: 2,845, 2: 155 | +0.4 MiB |
RSS returned to about 126–128 MiB after a worker restart in every variant. Literal result line:
RESULT 1-rules5-otel/final (after worker restart): traces=3000 spans=6312 one_root_traces=156 multi_root_traces=2844 traces_gt100_spans=0 empty_name_spans=2844 (kept=0 rejected=2844 as_root=2844) priorities={'-1': 2844, '2': 156}
Per-worker accumulation (partially reproduced). Add a hooked call that runs after frankenphp_handle_request() returns (as Symfony's terminate() does in worker mode), with a ddtrace-native span inside it. With those, 8 traces (one per worker) accumulated 431–531 spans each, 214–264 of them empty-name, over 60.6–77.0 s. That variant also lost 929 of 3,000 request traces. In our app the same symptom reached 1,845–2,367 empty-name spans per worker trace, with memory +25 MiB per 500 requests against +10 at 100%.
Notes:
- The bridge grows RSS in worker mode even at 100% sampling (+3.8 against +0.4 MiB per 500 requests with it off). Rejection roughly doubles it.
OTEL_TRACES_SAMPLER=always_onmakes the orphans disappear, but it isn't a clean workaround. In rejected traces the OTel spans then carry priority 1 while their root carries −1, which we suspect a real Agent treats as separately kept chunks. That's an inference, not verified.- Observed with the test agent only; worker mode on vs off was not compared.
Expected: a span the sampler drops must not leave an open ddtrace span: close or discard it, or don't open it before sampling. Rejected traces shouldn't carry empty-name orphans, and nothing should persist across requests in worker mode.
PHP version
PHP 8.3.36 (cli) (built: Oct 21 2026 16:00:24) (ZTS). FrankenPHP v1.13.2, worker mode.
Tracer or profiler version
ddtrace 1.26.1 (ZTS build). The repro doesn't load the profiler.
Installed extensions
ddtrace (emulated opentelemetry) plus the official image's defaults. open-telemetry/sdk 1.14.0, open-telemetry/api 1.9.0, open-telemetry/context 1.5.0.
Output of phpinfo()
Not attached.
Upgrading from
New installation; previously the opentelemetry PECL extension without ddtrace.
- Ngôn ngữ chính
- PHP
- Star
- 558
- Fork
- 192
- Merge trung bình
- 2 ngày 3 giờ
- Pull request đã merge (30 ngày)
- 104
Chuẩn bị môi trường
- Có Dockerfile hoặc tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của DataDog/dd-trace-php
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
DataDog/dd-trace-php#4371 · 5 bình luận · 1 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
🐛 bug
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 42/100
DataDog/dd-trace-php#4192 · 5 bình luận · 3 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
🐛 bug
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 38/100
DataDog/dd-trace-php#4042 · 10 bình luận · 2 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
feature-request
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
DataDog/dd-trace-php#4031 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
🐛 bug cat:app-crash
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 35/100
DataDog/dd-trace-php#3975 · 4 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của DataDog/dd-trace-php
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
verbb/formie#3072 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 84/100
wp-graphql/wp-graphql-woocommerce#1026 ·
-
bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
taskforcesh/bullmq#5038 ·
Maintainer thường phản hồi trong vòng 1 ngày