Skip to content

WIP: feat(tracer): align C span code with v1-compatible stub API (v2 groundwork) - #4046

Draft
Leiyks wants to merge 39 commits into
masterfrom
leiyks/dd-trace-php-v2
Draft

WIP: feat(tracer): align C span code with v1-compatible stub API (v2 groundwork)#4046
Leiyks wants to merge 39 commits into
masterfrom
leiyks/dd-trace-php-v2

Conversation

@Leiyks

@Leiyks Leiyks commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Migrate the tracer to the V1 Efficient Trace Payload protocol.

  • Sidecar path (PHP 8.3+): v1-only. Spans build directly into libdatadog's native v1 model via one dd_span_sink finalization body (no v04 intermediate): promoted env/version/component/span.kind, chunk-level 128-bit trace_id + sampling_mechanism, unified typed attributes (incl. meta_struct as bytes), native span_links/span_events.
  • Old-agent compatibility: when the agent /info doesn't advertise /v1.0/traces, the sidecar transcodes v1→v0.4 (existing libdatadog encoder). Isolated + removable — dropping v0.4 later = delete the /info check + the transcode call.
  • In-process background-sender (PHP ≤8.2): stays v0.4, transcoding v0.4→v1 on send behind the same removable check.
  • Userland introspection (dd_trace_serialize_closed_spans) returns the v1 shape uniformly on all versions; DD_TRACE_AGENT_PROTOCOL_VERSION gate removed.
  • Test harness: request-replayer gained a PHP v1 decoder + advertises /v1.0/traces; tests/ext expectations updated to v1 (603 passing).

Depends on libdatadog #2311. Follow-ups: native array/map AnyValue attributes (currently JSON-string), process_tags as a payload attribute, sidecar 404 fail-closed hardening.

@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Jul 17, 2026

Copy link
Copy Markdown

Pipelines  Tests

Unblock PR with BitsAI

⚠️ Warnings

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 730 Pipeline jobs failed

DataDog/apm-reliability/dd-trace-php | ASAN test_c: [8.0, arm64] — ❌ 17 tests failed · 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

❌ tmp/build_extension/tests/ext/request-replayer/serializer_wire_sidecar_v1.phpt (Sidecar sender + v1-capable agent serializes the v1 wire (/v1.0/traces, chunks)) from PHP.tmp.build_extension.tests.ext.request.replayer
001+ Fatal error: Uncaught Exception: wait for replay timeout in tmp/build_extension/tests/ext/includes/request_replayer.inc:59
002+ Stack trace:
003+ #0 tmp/build_extension/tests/ext/request-replayer/serializer_wire_sidecar_v1.php(32): RequestReplayer->waitForRequest(Object(Closure))
001- uri=/v1.0/traces
002- has_chunks=yes
003- span_name=root
004+ #1 {main}
005+   thrown in tmp/build_extension/tests/ext/includes/request_replayer.inc on line 59

↳ ❄️ tmp/build_extension/tests/ext/background-sender/agent_sampling_sidecar.phpt (The sidecar trace flusher sender informs about changes to the agent sample rate) from PHP.tmp.build_extension.tests.ext.background.sender

↳ ❄️ tmp/build_extension/tests/ext/ffe/remote_config_lifecycle.phpt (FFE Remote Config loads and removes UFC config) from PHP.tmp.build_extension.tests.ext.ffe

↳ and 14 more — View all
DataDog/apm-reliability/dd-trace-php | System Tests: [INTEGRATIONS] — ❌ 2 tests failed · 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

❌ tests.integrations.test_db_integrations_sql.Test_MySql.test_span_kind[apache-mod-8.0] from system_tests_suite
KeyError: 'span.kind'

self = <tests.integrations.test_db_integrations_sql.Test_MySql object at 0x7fd09f144350>
excluded_operations = ()

    def test_span_kind(self, excluded_operations: tuple[str, ...] = ()):
        """Describes the relationship between the Span, its parents, and its children in a Trace."""
    
        for _, span_meta in self.get_spans_meta(excluded_operations):
>           assert span_meta["span.kind"] == "client"
...
❌ tests.integrations.test_db_integrations_sql.Test_Postgres.test_span_kind[apache-mod-8.0] from system_tests_suite
KeyError: 'span.kind'

self = <tests.integrations.test_db_integrations_sql.Test_Postgres object at 0x7fd09f1469c0>
excluded_operations = ()

    def test_span_kind(self, excluded_operations: tuple[str, ...] = ()):
        """Describes the relationship between the Span, its parents, and its children in a Trace."""
    
        for _, span_meta in self.get_spans_meta(excluded_operations):
>           assert span_meta["span.kind"] == "client"
...
DataDog/apm-reliability/dd-trace-php | System Tests: [default] — ❌ 5 tests failed · 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

❌ tests.appsec.test_automated_payment_events.Test_Automated_Payment_Events_Stripe_Default_Rules.test_checkout_session[apache-mod-8.0] from system_tests_suite
KeyError: '_sampling_priority_v1'

self = <tests.appsec.test_automated_payment_events.Test_Automated_Payment_Events_Stripe_Default_Rules object at 0x7fd1f313eb70>

    def test_checkout_session(self):
        """R1"""
    
        def validator(span: DataDogLibrarySpan):
            assert span["meta"]["appsec.events.payments.creation.id"] == "cs_FAKE"
            assert span["metrics"]["appsec.events.payments.creation.amount_total"] == 950  # 100 * 10 * 0.9 + 50
...
❌ tests.appsec.test_automated_payment_events.Test_Automated_Payment_Events_Stripe_Default_Rules.test_payment_cancellation[apache-mod-8.0] from system_tests_suite
KeyError: '_sampling_priority_v1'

self = <tests.appsec.test_automated_payment_events.Test_Automated_Payment_Events_Stripe_Default_Rules object at 0x7fd1f313ede0>

    def test_payment_cancellation(self):
        """R5"""
    
        def validator(span: DataDogLibrarySpan):
            assert span["meta"]["appsec.events.payments.cancellation.id"] == "pi_FAKE"
            assert span["metrics"]["appsec.events.payments.cancellation.amount"] == 1337
...
❌ tests.appsec.test_automated_payment_events.Test_Automated_Payment_Events_Stripe_Default_Rules.test_payment_failure[apache-mod-8.0] from system_tests_suite
KeyError: '_sampling_priority_v1'

self = <tests.appsec.test_automated_payment_events.Test_Automated_Payment_Events_Stripe_Default_Rules object at 0x7fd1f313f740>

    def test_payment_failure(self):
        """R4"""
    
        def validator(span: DataDogLibrarySpan):
            assert span["meta"]["appsec.events.payments.failure.id"] == "pi_FAKE"
            assert span["metrics"]["appsec.events.payments.failure.amount"] == 1337
...
↳ and 2 more — View all

View all 730 failed jobs.

❄️ 1 New flaky test detected

tmp/build_extension/tests/ext/request-replayer/client_side_stats_dd_tags_env.phpt (Client-side span stats bucket by DD_TAGS env/version when DD_ENV/DD_VERSIO... from php.tmp.build_extension.tests.ext.request.replayer
001+ [ddtrace] [error] [5873] Failed synchronously flushing traces: Os { code: 104, kind: ConnectionReset, message: "Connection reset by peer" }
     env: staging
     version: 9.9.9-tags

View in Flaky Test Management

7 Test performance regressions detected

testScenario with data set "A simple GET request returning a string" from tests/Integrations/Symfony/V5_0.DDTrace\Tests\Integrations\Symfony\V5_0\CommonScenariosTest.DDTrace\Tests\Integrations\Symfony\V5_0\CommonScenariosTest::testScenario — 3.17s (+2.61s, +471%)   View in Datadog

tmp/build_extension/tests/ext/pcntl/pcntl_fork_thread_mode_orphan.phpt (Thread mode sidecar: orphaned child process promotes itself to master after parent exits) from PHP.tmp.build_extension.tests.ext.pcntl — 3.87s (+3.15s, +436%)   View in Datadog

tests.parametric.test_headers_tracecontext.Test_Headers_Tracecontext.test_tracestate_header_name_valid_casing[library_env0, parametric-php] from system_tests_suite — 3.85s (+3.11s, +422%)   View in Datadog

tests.parametric.test_headers_tracestate_dd.Test_Headers_Tracestate_DD.test_headers_tracestate_dd_propagate_propagatedtags_change_sampling_reset_dm[library_env0, parametric-php] from system_tests_suite — 3.65s (+3.1s, +558%)   View in Datadog

tests.parametric.test_headers_tracecontext.Test_Headers_Tracecontext.test_tracestate_header_name[library_env0, parametric-php] from system_tests_suite — 3.66s (+3.11s, +559%)   View in Datadog

View all

ℹ️ Info

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 87.72% (+27.09%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: eacc054 | Docs | View more details | Give us feedback!

@pr-commenter

pr-commenter Bot commented Jul 17, 2026

Copy link
Copy Markdown

Benchmarks [ tracer ]

Benchmark execution time: 2026-09-03 14:30:06

Comparing candidate commit eacc054 in PR branch leiyks/dd-trace-php-v2 with baseline commit 8d9060c in branch master.

Some scenarios are present only in baseline or only in candidate runs. If you didn't create or remove some scenarios in your branch, this maybe a sign of crashed benchmarks 💥💥💥
Check Gitlab CI job log to find if any benchmark has crashed.

Scenarios present only in baseline:

  • MessagePackSerializationBench/benchMessagePackSerialization
  • MessagePackSerializationBench/benchMessagePackSerialization-opcache

Found 1 performance improvements and 28 performance regressions! Performance is the same for 160 metrics, 1 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

scenario:ContextPropagationBench/benchExtractHeaders128Bit-opcache

  • 🟩 execution_time [-982.406ns; -948.261ns] or [-54.157%; -52.275%]

scenario:HookBench/benchHookOverheadTraceFunction

  • 🟥 mem_peak [+144.758KB; +144.761KB] or [+3.211%; +3.211%]

scenario:HookBench/benchHookOverheadTraceMethod

  • 🟥 mem_peak [+144.758KB; +144.762KB] or [+3.166%; +3.166%]

scenario:HookBench/benchHookOverheadTraceMethod-opcache

  • 🟥 mem_peak [+64.310KB; +64.314KB] or [+3.012%; +3.012%]

scenario:PDOBench/benchPDOOverhead

  • 🟥 execution_time [+20.170µs; +25.241µs] or [+6.272%; +7.848%]

scenario:PDOBench/benchPDOOverhead-opcache

  • 🟥 execution_time [+18.978µs; +22.677µs] or [+6.047%; +7.225%]

scenario:PDOBench/benchPDOOverheadWithDBM

  • 🟥 execution_time [+20.416µs; +24.799µs] or [+6.374%; +7.742%]

scenario:PDOBench/benchPDOOverheadWithDBM-opcache

  • 🟥 execution_time [+18.465µs; +22.049µs] or [+5.888%; +7.031%]

scenario:PHPRedisBench/benchRedisOverhead

  • 🟥 execution_time [+87.954µs; +101.157µs] or [+6.687%; +7.690%]

scenario:PHPRedisBench/benchRedisOverhead-opcache

  • 🟥 execution_time [+79.313µs; +100.416µs] or [+5.938%; +7.518%]

scenario:SamplingRuleMatchingBench/benchGlobMatching2-opcache

  • 🟥 execution_time [+86.326ns; +200.674ns] or [+3.180%; +7.392%]

scenario:SamplingRuleMatchingBench/benchRegexMatching1

  • 🟥 execution_time [+75.913ns; +174.287ns] or [+5.199%; +11.936%]

scenario:SamplingRuleMatchingBench/benchRegexMatching1-opcache

  • 🟥 execution_time [+44.252ns; +86.548ns] or [+2.793%; +5.462%]

scenario:SamplingRuleMatchingBench/benchRegexMatching3

  • 🟥 execution_time [+70.223ns; +142.577ns] or [+4.780%; +9.704%]

scenario:SamplingRuleMatchingBench/benchRegexMatching3-opcache

  • 🟥 execution_time [+42.301ns; +124.499ns] or [+2.656%; +7.816%]

scenario:SamplingRuleMatchingBench/benchRegexMatching4

  • 🟥 execution_time [+54.831ns; +114.169ns] or [+3.726%; +7.759%]

scenario:SamplingRuleMatchingBench/benchRegexMatching4-opcache

  • 🟥 execution_time [+118.202ns; +205.798ns] or [+7.362%; +12.818%]

scenario:SpanBench/benchDatadogAPI

  • 🟥 execution_time [+13.168µs; +15.327µs] or [+15.835%; +18.432%]

scenario:SpanBench/benchDatadogAPI-opcache

  • 🟥 execution_time [+14.980µs; +17.557µs] or [+6.765%; +7.929%]

scenario:SpanBench/benchOpenTelemetryAPI

  • 🟥 execution_time [+133.134µs; +174.308µs] or [+15.816%; +20.707%]

scenario:SpanBench/benchOpenTelemetryAPI-opcache

  • 🟥 execution_time [+120.550µs; +133.273µs] or [+14.648%; +16.194%]
  • 🟥 mem_peak [+1.530MB; +1.530MB] or [+3.523%; +3.523%]

scenario:SpanBench/benchOpenTelemetryInteroperability

  • 🟥 execution_time [+27.011µs; +32.767µs] or [+5.294%; +6.422%]
  • 🟥 mem_peak [+639.623KB; +639.631KB] or [+2.183%; +2.183%]

scenario:SpanBench/benchOpenTelemetryInteroperability-opcache

  • 🟥 execution_time [+21.554µs; +27.044µs] or [+4.421%; +5.546%]
  • 🟥 mem_peak [+640.981KB; +640.992KB] or [+2.431%; +2.431%]

scenario:TraceAnnotationsBench/benchTraceAnnotationOverhead-opcache

  • 🟥 mem_peak [+64.310KB; +64.315KB] or [+2.992%; +2.992%]

scenario:TraceFlushBench/benchFlushTrace

  • 🟥 execution_time [+21.266µs; +33.734µs] or [+5.746%; +9.115%]

scenario:TraceFlushBench/benchFlushTrace-opcache

  • 🟥 execution_time [+27.534µs; +38.666µs] or [+7.521%; +10.562%]

@Leiyks
Leiyks force-pushed the leiyks/dd-trace-php-v2 branch from 2531886 to b29cd49 Compare July 27, 2026 15:20
@Leiyks
Leiyks force-pushed the leiyks/dd-trace-php-v2 branch 3 times, most recently from 10c7edf to 84351df Compare August 6, 2026 12:39
@Leiyks
Leiyks force-pushed the leiyks/dd-trace-php-v2 branch from 84351df to 73c262e Compare August 24, 2026 15:26
Leiyks added 18 commits August 25, 2026 17:33
Signed-off-by: Alexandre Rulleau <alexandre.rulleau@datadoghq.com>
Signed-off-by: Alexandre Rulleau <alexandre.rulleau@datadoghq.com>
Bring the tracer C code into consistency with the already-rewritten
v1-compatible span stub (tracer/ddtrace.stub.php), keeping the v04 wire
format byte-identical (libdatadog v1 FFI is not yet available).

- Regenerate ddtrace_arginfo.h from the stub; reorder the
  ddtrace_span_properties / ddtrace_root_span_data / ddtrace_span_stack
  structs to match the new property declaration order (PHP property
  offsets are bound to these C struct fields).
- Drop SpanEvent/SpanLink JsonSerializable + jsonSerialize(); relocate
  that logic into serializer.c so the _dd.span_links / events meta blobs
  are produced with identical bytes.
- Make component/spanKind sourced from the new SpanData properties,
  translated back into meta (span.kind/component) at serialize time so
  the v04 wire is unchanged. Register the new SpanKind class.
- Remove dead code for stub-removed functions and the
  DD_TRACE_WARN_LEGACY_DD_TRACE config key; complete the userland
  integration-analytics removal (Integration.php + call sites); delete
  tests orphaned by removed functions and rewrite the dd_trace_reset
  helper tests without gutting their coverage.

Refs APMLP-1197.
…DD_TRACE_WARN_LEGACY_DD_TRACE

The config key DD_TRACE_WARN_LEGACY_DD_TRACE was removed from
tracer/configuration.h, but metadata/supported-configurations.json was not
regenerated, causing the 'Configuration Consistency' CI job to fail. Ran
tooling/generate-supported-configurations.sh to sync.
RootSpanData extends SpanData, which already declares $env and $version.
The v1-stub alignment accidentally re-declared them on RootSpanData (they
were never on RootSpanData on master, and the ddtrace_root_span_data C struct
has no separate env/version slots -- they are inherited SpanData slots). On
PHP < 8.1 this redundant child redeclaration corrupts the RootSpanData
property table: env/version are var_dump'd twice and propagatedTags loses its
default, breaking tests/ext/active_span.phpt and span_clone.phpt on 7.0-8.0
(they passed on 8.1+). Removing the redeclaration from the stub and arginfo
aligns stub=arginfo=C struct and restores consistent output across all
versions; env/version remain available on RootSpanData via inheritance.
…on PHP 7

The new `attributes` property (stub default `= []`) was not materialized at
span/stack creation. On PHP < 8.0 array-typed property defaults become null
(see the ZVAL_EMPTY_ARRAY shim in functions.c) and are only lazily turned into
arrays when touched; since `attributes` is never touched during a plain span
lifecycle it var_dump'd as NULL on 7.0-7.4 (array(0){} on 8.0+), breaking
tests/ext/active_span.phpt and span_clone.phpt. Force-materialize it in
ddtrace_init_span and dd_alloc_span_stack (guarded to PHP < 8.0) so it is a
consistent empty array on every supported version, matching its stub default.
The chore that dropped integration (App Analytics) auto-tagging removed
Integration::addTraceAnalyticsIfEnabled and the per-integration
DD_TRACE_<integration>_ANALYTICS_* config, so the _dd1.sr.eausr metric is no
longer auto-added to integration spans. Align the tests:

- Delete the 25 TraceSearchConfigTest.php files (Laravel, Lumen, Symfony,
  ZendFramework, Custom) whose sole subject was the removed per-integration
  trace-analytics config.
- Drop the per-integration analytics setup + _dd1.sr.eausr metric assertions
  from the mixed PDO and SQLSRV integration tests, preserving all other span
  coverage.
- Remove Curl's dedicated testTraceAnalytics method + its data provider
  (per-integration analytics config matrix) and the stale DD_CURL_ANALYTICS_ENABLED
  teardown-cleanup entries in Curl/Guzzle.

The user-facing App Analytics API (Span::setMetric(Tag::ANALYTICS_KEY) ->
TraceAnalyticsProcessor, Tag::ANALYTICS_KEY constant) is intentionally kept by
this branch, so its tests (SpanTest, TraceAnalyticsProcessorTest,
UserAvailableConstantsTest, OpenTelemetry, ext/test_special_attributes) are
left intact.
Comment-only cleanup of the C span-consistency changes: trim the
multi-line explanatory blocks in serializer.c, span.c,
handlers_httpstreams.c and tracer_telemetry.c down to concise 1-2 line
comments. No behavior change.
The user-facing App Analytics API is now a deprecated no-op: it remains
callable (Tag::ANALYTICS_KEY, TraceAnalyticsProcessor, DD_TRACE_ANALYTICS_ENABLED
stay defined) but no longer applies any behavior nor emits the _dd1.sr.eausr
metric in-process or on the wire.

- serializer.c: drop the DD_TRACE_ANALYTICS_ENABLED/web-analytics emission,
  stop converting the analytics.event meta key to the metric (still consumed),
  and skip _dd1.sr.eausr in the metrics serialization loop.
- TraceAnalyticsProcessor::normalizeAnalyticsValue is now an empty no-op;
  Tag::ANALYTICS_KEY, the processor, and the api stubs are marked @deprecated.
- Tests rewritten to assert the API is callable and emits no _dd1.sr.eausr.
Bump the libdatadog submodule to PR #2156's head (938c110), which brings in
the v1 sidecar span encoder/decoder (#2145, #2174) via its main base.

Regenerate components-rs/{common,sidecar}.h with cbindgen (exposes the new
ddog_sidecar_send_trace_v1_shm / _bytes entrypoints and the
ASM_RAW_RESPONSE_BODY remote-config capability) and mirror libdatadog's
consolidated [workspace.dependencies] into the root Cargo.toml so the
path-dependency crates resolve their { workspace = true } inheritance.

Wire format is unchanged: v04 remains the default send path.
… V1 wire

Add DD_TRACE_AGENT_PROTOCOL_VERSION (default "0.4"). When set to "1"/"1.0",
assemble ddog_TracerMetadataV1 and call ddog_send_traces_to_sidecar_v1;
otherwise keep the unchanged V0.4 sidecar send. Hard gate; no /info negotiation.
Regenerate components-rs/*.h and Cargo.lock against the rebased libdatadog
submodule (v1 send FFI branch merged with libdatadog main). Picks up the
RemoteConfig DEBUG product enum, the v1 FFI comment trims, the agent_info
doc-comment tightening, and the zrip/ring lockfile additions.
@Leiyks
Leiyks force-pushed the leiyks/dd-trace-php-v2 branch from 73c262e to 0d3d03d Compare August 25, 2026 15:50
@Leiyks
Leiyks marked this pull request as ready for review August 26, 2026 11:56
@Leiyks
Leiyks requested review from a team as code owners August 26, 2026 11:56
@Leiyks
Leiyks requested review from dd-oleksii and tabgok and removed request for a team August 26, 2026 11:56

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0d3d03d6f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tracer/serializer.c Outdated
Comment on lines +1094 to +1096
static bool dd_v1_native_span_enabled(void) {
zend_string *pv = get_global_DD_TRACE_AGENT_PROTOCOL_VERSION();
return zend_string_equals_literal(pv, "1") || zend_string_equals_literal(pv, "1.0");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Gate native fields on the negotiated V1 path

When the protocol setting is 1/1.0 but agent info is unavailable or does not advertise /v1.0/traces, this helper still moves links and events exclusively into the native V1 fields, while auto_flush.c:75-100 deliberately falls back to the V0.4 sender. The V0.4 encoder expects the JSON values in _dd.span_links and events, so those spans silently lose all links and events during startup, with older agents, and whenever the non-sidecar sender is used. Base this decision on the same negotiated use_v1 condition as the sender, rather than the configuration value alone.

Useful? React with 👍 / 👎.

Comment thread tracer/span_stats.c Outdated
Comment on lines +112 to +114
zval *meta_env = pre->meta ? zend_hash_str_find(pre->meta, ZEND_STRL("env")) : NULL;
if (meta_env) {
pre->env_deprecated = true;
LOG(DEPRECATED, "Using \"env\" in meta is deprecated. Instead specify the env property directly on the span.");
zend_string *str = datadog_convert_to_str(meta_env);
pre->env_deprecated = false;
zval *prop_env = &span->property_env;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve legacy env overrides during precomputation

When a span sets the supported legacy $span->meta['env'] override while its env property contains an inherited process environment, meta_env is now looked up but ignored. Consequently client-side stats are aggregated under the property environment via ddtrace_feed_span_to_concentrator() instead of the environment represented by the span, and the later property write in serializer.c:1914-1915 can replace the meta override on the wire. Restore the documented meta-first selection, even if it remains deprecated.

Useful? React with 👍 / 👎.

Comment thread tracer/span.c
Comment on lines +169 to +172
#if PHP_VERSION_ID < 80000
// PHP 7 array-typed properties default to null; materialize `attributes` to match its
// `= []` stub default (as on PHP 8).
ddtrace_property_array(&span->property_attributes);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Initialize attributes in the PHP 7 object creators

On PHP 7 this initialization only runs for spans allocated through the tracer's internal ddtrace_init_span() helper. Public direct construction such as new DDTrace\SpanData() or new DDTrace\RootSpanData() goes through the create_object callbacks in functions.c:244-269 instead, leaving the new attributes property as null because array defaults are deliberately rewritten to null on PHP 7. Directly constructed SpanStack objects have the same problem because their initialization was placed only in dd_alloc_span_stack(). Initialize these properties in the respective object creators so every construction path satisfies the declared array API.

Useful? React with 👍 / 👎.

Leiyks added 2 commits August 26, 2026 17:37
Stages 3+4 of the v1-native migration. The sidecar sender now always builds a
native libdatadog v1 TracerPayload via the new builder FFI and sends it with
ddog_send_traces_to_sidecar_v1; the sidecar negotiates v1-vs-v0.4 with the agent
and downgrades as needed, so the tracer-side DD_TRACE_AGENT_PROTOCOL_VERSION gate
is removed.

- serializer.c: convert each fully-built v0.4 span into the v1 builder (fields
  and meta/metrics/meta_struct via the v0.4 read getters, native links/events
  from the still-alive PHP span). Promoted fields (env/version/component/
  span.kind) use the dedicated setters and are excluded from the attribute map;
  chunk-level fields (sampling priority/origin/mechanism/128-bit trace-id/
  dropped) are routed to the chunk. Array/object attribute values, which have no
  native v1 attribute variant, are preserved as a JSON string. The v0.4 build
  stays byte-identical for the in-process sender and functions.c introspection.
- auto_flush.c: sidecar path always builds the v1 builder and sends v1; shrunk
  ddog_TracerMetadataV1 {hostname,env,app_version,runtime_id,git_commit_sha}. The
  in-process sender (PHP <= 8.2) stays on v0.4, unchanged.
- Remove the now-dead DD_TRACE_AGENT_PROTOCOL_VERSION config key and the
  ddog_agent_info_has_endpoint getter; regenerate datadog.h and
  supported-configurations.json.
- Bump libdatadog to the v1 send FFI (317c98d28) and regenerate cbindgen
  headers. Add a hand-written prototypes header (sidecar_v1_macro_ffi.h) for the
  macro-generated v1 setters that cbindgen cannot emit.
- Update the request-replayer span-event tests to assert native span events.
libdatadog now defines the v1 builder setters as explicit fns instead of
declarative-macro-generated ones, so cbindgen emits them into the generated
components-rs/sidecar.h. Bump the libdatadog submodule, regenerate sidecar.h
(now carries ddog_v1_set_span_* / ddog_v1_add_span_attr_* /
ddog_v1_add_event_attr_*), and remove the interim
components-rs/sidecar_v1_macro_ffi.h and its include in serializer.c.
@Leiyks
Leiyks marked this pull request as draft August 26, 2026 16:33
@Leiyks Leiyks changed the title feat(tracer): align C span code with v1-compatible stub API (v2 groundwork) WIP: feat(tracer): align C span code with v1-compatible stub API (v2 groundwork) Aug 26, 2026
Leiyks added 12 commits August 26, 2026 19:27
…ndgen

Bump the libdatadog submodule to 594d5c2ab (LIBDD_V1ONLY branch), which adds
ddog_serialize_trace_v1_into_charslice for the in-process (PHP <= 8.2) sender,
and regenerate components-rs/sidecar.h via cbindgen. The v1 builder/setter
symbols were already present from Stage 1; this only adds the serialize-to-bytes
entry point used by the coms.c in-process path.
Port span finalization to populate the native v1 TracerPayload builder
directly for the sidecar sender, with no v0.4 intermediate, and reflect
the v1 model in introspection. The in-process (<=8.2) background sender
keeps its v0.4 build behind an isolated, removable v0.4->v1 transcode
layer.

- serializer.c: route the single finalization body of
  ddtrace_serialize_span_to_rust_span through a dd_span_sink that targets
  either the v0.4 ddog_SpanBytes or the v1 builder chunk/span. Promoted
  fields (env/version/component/span.kind) go to dedicated v1 setters and
  chunk-level fields (_dd.origin/_dd.p.dm/_sampling_priority_v1) to the
  chunk, excluded from the attribute map; _dd.p.tid is carried by the
  chunk 128-bit trace id. Links/events are emitted natively on v1 (JSON-in
  -meta only on v0.4). The inferred->root merge uses the typed
  ddog_v1_transfer_span_attr. Removed dd_v1_convert_span and the removed
  ddog_v1_intern_string usage (setters now take CharSlice).
- exception_serialize.c: error.message/type/stack and exception-replay
  debug meta write through the sink (v1 span attributes on the sidecar
  path).
- functions.c/serializer.c: dd_trace_serialize_closed_spans reads the v1
  builder via the new getters on the sidecar path (dd_serialize_rust_v1_to
  _zval), reflecting the v1 model (promoted + chunk fields, typed
  attributes, native links/events); v0.4 introspection unchanged.
- auto_flush.c/coms.c/agent_info.c: removable v0.4->v1 bolt-on for the
  in-process sender: ddtrace_agent_supports_v1_traces() gates transcoding
  the v0.4 collection via ddog_serialize_trace_v04_as_v1_into_charslice and
  POSTing to /v1.0/traces; otherwise the existing v0.4 path to
  /v0.4/traces.
- libdatadog: bump to fef26d167 (mutable v1 span-attr FFI); regenerate
  cbindgen headers.
Bump libdatadog to 3fb12c2c8 (adds ddog_v1_span_debug_log for the V1
builder) and regenerate components-rs/sidecar.h.

On the v1 (sidecar) serialization path, emit the DD_TRACE_DEBUG
"Encoding span:" diagnostic via ddog_v1_span_debug_log(builder, chunk,
span) so the span-encoding log exists on both the v0.4 and v1 paths.
Freed with ddog_free_charslice, mirroring the v0.4 branch.
On the v1 (sidecar) path, meta_struct entries are written to the unified V1
attribute map as bytes attributes (ddog_v1_add_span_attr_bytes) and reach the
wire correctly, but dd_serialize_rust_v1_to_zval folded them into the generic
"attributes" array as opaque strings, so dd_trace_serialize_closed_spans no
longer exposed a "meta_struct" key (appsec data appeared missing on
introspection).

Route bytes-typed span attributes (the only source of bytes attrs on this path)
into a dedicated "meta_struct" key, mirroring the v0.4 reader, so meta_struct
is visible again with its raw msgpack bytes and meta_struct.phpt passes on both
the v0.4 and v1 paths unchanged.

Bumps libdatadog to include the v1 attribute-map pre-encode dedup fix.
…sandbox/exceptions, single-span_sampling, client_side_stats)

Relocate meta/metrics array access to the unified attributes map
introduced by the v1 introspection shape. attributes preserves
insertion order (not the old alphabetical meta ordering), which
required reordering safe_to_string_metadata's EXPECTF. Filtered
iteration to arg.* keys in safe_to_string_metrics/metadata and
accept-single-span, since attributes now also carries entrypoint-only
tags (_dd.tags.process) that used to live outside meta/metrics.
…root_span_url/http/referrer, extract_*)

Relocate meta -> attributes for root_span_url_*, root_span_http_*,
security-headers, referrer_extraction_*, and extract_server_values /
extract_ip_private_01 / ip_collection_03. span.kind moves to the
top-level int span_kind field.

Full-array meta dumps are replaced with targeted key/absence checks:
the unified attributes map now also carries entrypoint-only telemetry
(process_id, php.compilation.total_time_ms, php.memory.*) whose float
values are non-deterministic across runs, so dumping the whole map
would make these tests flaky. The targeted checks preserve each
test's original intent (URL/referrer/post-data extraction) without
asserting on unrelated telemetry noise.
…distributed_tracing)

Relocate meta -> attributes for the asm_standalone _dd.p.ts checks.

distributed_trace_overwrite_active_span needed more than a rename:
_dd.origin and the 128-bit trace id high bits are now promoted to
dedicated top-level fields (origin, trace_id_high) instead of living
in meta/attributes, and trace_id_high is now populated on every span
of a 128-bit trace (consistent with trace_id itself being duplicated
per span), not only the root. Also filter out the newly-visible
per-span telemetry noise (_dd.agent_psr, php.compilation/memory.*)
that the unified attributes map now carries.
…svc_*, limiter, debug-log format, misc)

Relocate meta/metrics -> attributes across the remaining tests
(dd_trace_serialize_header_to_meta, http_endpoint_resource_renaming_*,
svc_*, process_tags, git_metadata_injection_from_valid_files).

env/version are promoted to dedicated top-level fields (not nested in
attributes), fixed in ust_precedence_over_ddtags/ust_via_ddtags and
inherit_meta_from_parent. _dd.p.tid is fully replaced by the top-level
trace_id_high field (generate_128_bit_trace_id).

Rewrote the "Encoding span: Span { ... }" debug-log EXPECTF blocks to
the new v1 shape ("Encoding span: trace_id=... kind=... attributes=
{...} links=n events=n") and "Flushing trace" -> "Flushing v1 trace"
in close_spans_until, die_in_sandbox, span_on_close, force_flush_traces,
telemetry/broken_pipe.

limiter/002-limiter-reached and limiter/003-limiter-with-asm-standalone
initially looked like a real regression (_dd.limit_psr computed ~50x
too low): isset($span["attributes"]["_sampling_priority_v1"]) was
always false because that metric is now promoted to the top-level int
"sampling_priority" field, same relocation pattern as span_kind/env/
version/origin, so the test's sample-counting loop ran to its 1000-
iteration safety cap instead of stopping at 20 samples. Verified against
git merge-base (8d9060c): unmodified test passes there 5/5, and
fails deterministically 5/5 at this branch's HEAD with the old
metrics-key access - confirming the fix is the missed relocation, not
a product regression, once sampling_priority is read from the
top-level field.
…n view

Add a PHP v1 msgpack decoder (msgpack_v1_decoder.php) so the deferred PHPUnit
integration tests can assert the v1 wire. The decoder resolves the streaming
string table, maps integer proto field-number keys back to names, and decodes
typed AnyValue values, normalizing a v1 payload back to the canonical
{"chunks":[{"spans":[...]}]} v0.4 per-span shape TracerTestTrait reads:

- un-promotes span env/version/component and span.kind (int->string) into meta;
- reconstructs per-span trace_id (chunk 128-bit low 64 decimal) + meta._dd.p.tid
  (high 64 hex), meta._dd.origin, meta._dd.p.dm ("-N"), and
  metrics._sampling_priority_v1 from the chunk-level fields onto the local root;
- splits the unified attributes map into meta (String), metrics (Int/Double),
  meta_struct (Bytes); native span_links/span_events -> meta._dd.span_links /
  meta.events JSON (the prior v0.4 contract).

index.php dispatches /v1.0/traces to the new decoder and keeps the existing
v0.4 path for /v0.4/traces; /info now advertises /v1.0/traces by default so the
sidecar (8.3+) and in-process (<=8.2) senders negotiate v1, while
/set-agent-info overrides still win.
dd_trace_serialize_closed_spans() previously read the native V1 builder only
when the sidecar sender was active (PHP 8.3+) and the V0.4 model otherwise, so
its output shape depended on the active sender / PHP version. Introspection is
a debug view: always finalize spans into the in-memory V1 builder and read them
back via the V1 getters, regardless of which sender performs the wire flush.
The V1 builder needs no active sidecar, so this is version-independent; the
<=8.2 wire flush stays on the V0.4 background sender.
The meta["env"]/meta["version"] deprecation path was already removed from
ddtrace_precompute_span (env/version now come from the span property only), but
left behind: two unused meta lookups, the env_deprecated/version_deprecated
precomputed fields (permanently false), and the now always-true guards gating
them in the serializer. Remove all of it. Also drop the unused AsBytes import in
components-rs/agent_info.rs.
With introspection now uniformly V1-shaped, update the remaining tests/ext
.phpt expectations that still asserted the V0.4 shape. Bodies read the unified
attributes map (old meta + metrics merged); var_dump/JSON blocks gain top-level
trace_id_high and span_kind, rename meta->attributes with real v1 insertion
order, use native span_links/span_events arrays, and surface meta_struct under
its own key. _dd.p.tid meta is dropped (now top-level trace_id_high) and the
trace-flush debug log reads 'Flushing v1 trace'. The shared dd_dumper.inc /
fake_tracer.inc helpers read attributes (string-valued only, preserving the
historic meta-only view). tests/ext is fully green on the default config.
Comment thread tracer/serializer.c Outdated
Comment on lines +1097 to +1099
// write through a dd_span_sink. On the in-process (<=8.2) path the sink targets a V0.4 ddog_SpanBytes
// (also read back by functions.c introspection); on the sidecar path it targets the native V1 builder
// chunk/span directly, with no V0.4 intermediate. The V1 routing promotes env/version/component/

@bwoebi bwoebi Sep 1, 2026

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.

No, we want v1 only, and have libdatadog convert back from v1 to v0.4 if needed, after creating the native spans.
Completely get rid of any v04 stuff in serializer. That's for auto_flush.c.

Comment thread tracer/serializer.c Outdated
Comment on lines +1130 to +1142
if (DD_CS_EQ(key, "_dd.span_links") || DD_CS_EQ(key, "events")) { return true; }
if (DD_CS_EQ(key, "_dd.origin")) { ddog_v1_set_chunk_origin(b, s->chunk, value); return true; }
if (DD_CS_EQ(key, "_dd.p.dm")) {
// v0.4 form is "-N"; the mechanism is the trailing unsigned integer.
const char *p = value.ptr; size_t n = value.len;
if (n && *p == '-') { p++; n--; }
uint32_t mech = 0;
for (size_t i = 0; i < n; i++) { if (p[i] < '0' || p[i] > '9') { mech = 0; break; } mech = mech * 10 + (uint32_t)(p[i] - '0'); }
ddog_v1_set_chunk_sampling_mechanism(b, s->chunk, mech);
return true;
}
// 128-bit trace-id high half is carried by the chunk trace id, not a span attribute.
if (DD_CS_EQ(key, "_dd.p.tid")) { return true; }

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.

users aren't really supposed to touch that stuff at all. No conversion needed.

Comment thread tracer/serializer.c Outdated
Comment on lines +1125 to +1128
if (DD_CS_EQ(key, "env")) { ddog_v1_set_span_env(b, s->chunk, s->span, value); return true; }
if (DD_CS_EQ(key, "version")) { ddog_v1_set_span_version(b, s->chunk, s->span, value); return true; }
if (DD_CS_EQ(key, "component")) { ddog_v1_set_span_component(b, s->chunk, s->span, value); return true; }
if (DD_CS_EQ(key, "span.kind")) { ddog_v1_set_span_kind(b, s->chunk, s->span, dd_span_kind_meta_to_otel(value)); return true; }

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.

We should check the values on the meta of the PHP span directly, discard them from meta (via _find+_del) and have the actual serialization only look at taking the values from the span properties, bypassing meta fully, avoiding individual checks during iteration.

…n headers

Pull in libdatadog's replacement of the per-field SpanBytes mutator
FFI with direct TracerPayloadV1Builder access and the
ddog_downgrade_v1_builder_to_v04_traces API. Rewrite components-rs/
bytes.rs against the new builder-based FFI surface and regenerate
components-rs/datadog.h + sidecar.h via cbindgen.
Rework serializer.c/.h to fill the V1 TracerPayloadV1Builder through
the new direct-builder FFI (ddog_new_chunk/ddog_new_span/
ddog_span_set_*/ddog_add_span_attr_*) instead of the removed
per-field SpanBytes mutators, dropping ~550 lines of now-dead
hand-rolled v0.4 field plumbing. Adjust span.h/span_stats.{c,h} for
the fields the V1 builder now owns directly.
…r default

auto_flush.c/coms.{c,h}: the in-process (PHP <= 8.2) sender always
downgrades a V1 builder to v0.4 traces via
ddog_downgrade_v1_builder_to_v04_traces and frames each decoded trace
individually, matching the sidecar's own v1->v0.4 downgrade path.
generate-supported-configurations.sh: DD_SIDECAR_TRACE_SENDER_DEFAULT
is now unconditionally true (no more PHP-version gate), regenerate
metadata/supported-configurations.json to match.
DD_SIDECAR_TRACE_SENDER_DEFAULT is now true unconditionally instead
of only for PHP >= 8.3/_WIN32; the in-process coms.c sender remains
available via an explicit DD_TRACE_SIDECAR_TRACE_SENDER=0 opt-in.
Document the pcntl_fork() caveat for forking apps on <=8.2 that
relied on the old in-process default.
Update background-sender/agent_* and request-replayer span-event
.phpt expectations for the V1 payload shape. Add new coverage:
serializer_wire_{inprocess_v04,inprocess_v1,sidecar_v1} for the wire
format on each sender path, client_side_stats_dd_tags_env /
_inferred_span_sampling, and env/span-kind meta-fallback promotion
tests.
agent_info.h declares a bool-returning function but is the first include
in agent_info.c; PHP 7.x zend_types.h does not pull in <stdbool.h>, so
the tracer C failed to compile on 7.0-7.4 (Linux + Windows MSVC).

Also bumps the libdatadog submodule to the rustfmt-clean commit.
Trim verbose comment blocks added by the V1 trace-protocol pivot to the
house terse style (non-obvious WHY only); no code changes. Bump the
libdatadog submodule to the matching comment-only commit.
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.

2 participants