Skip to content

feat(tracer): rollout local root span id pprof label in favor of trace id for appsec - #5114

Open
eliottness wants to merge 8 commits into
mainfrom
eliottness/pprof-trace-id-no-local-root
Open

feat(tracer): rollout local root span id pprof label in favor of trace id for appsec#5114
eliottness wants to merge 8 commits into
mainfrom
eliottness/pprof-trace-id-no-local-root

Conversation

@eliottness

@eliottness eliottness commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Combines Nick Ripley's #5087 with the trace-ID behavior from #5064, scoped to AppSec:

  • removes the unused local root span id pprof label
  • adds the full 128-bit trace id pprof label only when AppSec is enabled
  • code-hotspots-only spans retain span id and do not receive trace id
  • endpoint profiling retains trace endpoint and does not receive trace id
  • caches the hexadecimal trace ID once per trace

Motivation

AppSec needs a whole-trace correlation key in profiles. Profiling alone does not need the trace ID and must not pay its cardinality or encoding cost. #5087 demonstrates that local root span id is unused by the profiling backend/UI, so removing it preserves or improves profiling performance while AppSec selectively adds trace id.

The previous Benchmarking Platform run was for head 819a3eb46, before the AppSec-only gate. Head 0053e3ee1 removes trace-ID work from profiling-only benchmarks; its new Benchmarking Platform result is the decision criterion.

Validation:

  • go test -race -count=1 ./ddtrace/tracer ./internal/traceprof/...
  • go test -count=1 ./ddtrace/tracer/...
  • nested internal/traceprof/traceproftest TestEndpointsAndCodeHotspots
  • go vet ./ddtrace/tracer ./internal/traceprof/...
  • label gating tests assert trace ID absence for hotspots-only and endpoints-only, and presence for AppSec-only

Reviewer's Checklist

  • Changed code has unit tests for its functionality at or near 100% coverage.
  • System-Tests covering this feature have been added and enabled with the va.b.c-dev version tag.
  • There is a benchmark for any new code, or changes to existing code.
  • If this interacts with the agent in a new way, a system test has been added.
  • New code is free of linting errors.
  • New code does not break existing tests.
  • Add an appropriate team label so this PR gets put in the right place for the release notes.
  • All generated files are up to date.
  • No go.mod changes.

nsrip-dd and others added 4 commits August 3, 2026 16:39
We don't actually use these anywhere in our backend or UI. Stop adding
them (saving a bit of memory allocated per span) and remove the
associated tests.

(cherry picked from commit 3e671ce)
Emit the full 128-bit trace id (32-char lowercase hex) as a new "trace id"
pprof label so profiler samples can be correlated with the whole trace, not
just the local root span.

The "trace id" and "local root span id" labels are now emitted for code
hotspots and whenever AppSec is enabled, so AppSec can correlate security
events with traces/profiles. "span id" stays gated behind code hotspots and
"trace endpoint" behind endpoint profiling.

(cherry picked from commit ae25f6e)
The new pprof "trace id" label added a per-span hex.EncodeToString allocation
(+1 alloc/span on BenchmarkStartSpan and other span-creation benchmarks).

Every span in a trace shares the same trace id, so compute the 32-char hex once
and let child spans reuse the parent's cached string: hexEncodedCached memoizes
the hex on the not-yet-shared SpanContext during StartSpan, and newSpanContext
copies the parent's cached hex into the child (guarded by a full trace-id
equality check). The read path (HexEncoded, used by Inject) still never writes,
so the v2.8.0-rc.2 concurrent-Inject data race cannot reoccur.

benchstat (count=10): allocs/op 16 -> 15, B/op -1.84% on
BenchmarkStartSpan / BenchmarkStartSpanConcurrent.

(cherry picked from commit 679beae)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Config Audit

PACKAGE: contrib/cloud.google.com/go/pubsubtrace
  STATUS      CONFIG                                            CALL_SITES
  UNMIGRATED  DD_GOOGLE_CLOUD_PUBSUB_PROPAGATION_AS_SPAN_LINKS  1

PACKAGE: contrib/confluentinc/confluent-kafka-go/kafkatrace
  STATUS      CONFIG                            CALL_SITES
  UNMIGRATED  DD_TRACE_KAFKA_ANALYTICS_ENABLED  1

PACKAGE: ddtrace/opentelemetry/log
  STATUS      CONFIG                            CALL_SITES
  STILL_READ  DD_AGENT_HOST                     2
  STILL_READ  DD_ENV                            1
  STILL_READ  DD_SERVICE                        1
  STILL_READ  DD_TAGS                           1
  STILL_READ  DD_TRACE_AGENT_URL                2
  STILL_READ  DD_TRACE_REPORT_HOSTNAME          1
  STILL_READ  DD_VERSION                        1
  STILL_READ  OTEL_EXPORTER_OTLP_ENDPOINT       4
  STILL_READ  OTEL_EXPORTER_OTLP_HEADERS        2
  STILL_READ  OTEL_EXPORTER_OTLP_PROTOCOL       2
  UNMIGRATED  DD_HOSTNAME                       1
  UNMIGRATED  OTEL_BLRP_EXPORT_TIMEOUT          1
  UNMIGRATED  OTEL_BLRP_MAX_EXPORT_BATCH_SIZE   1
  UNMIGRATED  OTEL_BLRP_MAX_QUEUE_SIZE          1
  UNMIGRATED  OTEL_BLRP_SCHEDULE_DELAY          1
  UNMIGRATED  OTEL_EXPORTER_OTLP_LOGS_ENDPOINT  4
  UNMIGRATED  OTEL_EXPORTER_OTLP_LOGS_HEADERS   2
  UNMIGRATED  OTEL_EXPORTER_OTLP_LOGS_PROTOCOL  2
  UNMIGRATED  OTEL_EXPORTER_OTLP_LOGS_TIMEOUT   1
  UNMIGRATED  OTEL_EXPORTER_OTLP_TIMEOUT        1
  UNMIGRATED  OTEL_RESOURCE_ATTRIBUTES          1

PACKAGE: ddtrace/opentelemetry/metric
  STATUS      CONFIG                                             CALL_SITES
  STILL_READ  DD_AGENT_HOST                                      2
  STILL_READ  DD_ENV                                             1
  STILL_READ  DD_METRICS_OTEL_ENABLED                            1
  STILL_READ  DD_SERVICE                                         1
  STILL_READ  DD_TAGS                                            1
  STILL_READ  DD_TRACE_AGENT_URL                                 2
  STILL_READ  DD_TRACE_REPORT_HOSTNAME                           1
  STILL_READ  DD_VERSION                                         1
  STILL_READ  OTEL_EXPORTER_OTLP_ENDPOINT                        2
  STILL_READ  OTEL_EXPORTER_OTLP_HEADERS                         1
  STILL_READ  OTEL_EXPORTER_OTLP_METRICS_ENDPOINT                2
  STILL_READ  OTEL_EXPORTER_OTLP_METRICS_HEADERS                 1
  STILL_READ  OTEL_EXPORTER_OTLP_METRICS_PROTOCOL                2
  STILL_READ  OTEL_EXPORTER_OTLP_PROTOCOL                        2
  STILL_READ  OTEL_METRICS_EXPORTER                              1
  UNMIGRATED  DD_HOSTNAME                                        1
  UNMIGRATED  OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE  1
  UNMIGRATED  OTEL_EXPORTER_OTLP_TIMEOUT                         1
  UNMIGRATED  OTEL_RESOURCE_ATTRIBUTES                           1
  UNMIGRATED  OTEL_SERVICE_NAME                                  1

PACKAGE: ddtrace/tracer
  STATUS      CONFIG                                    CALL_SITES
  STILL_READ  DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT     1
  STILL_READ  DD_TRACE_PROPAGATION_EXTRACT_FIRST        1
  STILL_READ  DD_TRACE_PROPAGATION_STYLE_EXTRACT        1
  STILL_READ  DD_TRACE_PROPAGATION_STYLE_INJECT         1
  UNMIGRATED  DD_APM_TRACING_ENABLED                    1
  UNMIGRATED  DD_LLMOBS_AGENTLESS_ENABLED               1
  UNMIGRATED  DD_LLMOBS_ENABLED                         1
  UNMIGRATED  DD_LLMOBS_ML_APP                          1
  UNMIGRATED  DD_LLMOBS_PROJECT_NAME                    1
  UNMIGRATED  DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED  1
  UNMIGRATED  DD_TRACE_DEBUG_SEELOG_WORKAROUND          1
  UNMIGRATED  OTEL_TRACES_SAMPLER_ARG                   1

PACKAGE: instrumentation
  STATUS      CONFIG                                       CALL_SITES
  STILL_READ  DD_DATA_STREAMS_ENABLED                      1
  UNMIGRATED  DD_API_SECURITY_ENDPOINT_COLLECTION_ENABLED  1

PACKAGE: instrumentation/graphql
  STATUS      CONFIG                             CALL_SITES
  UNMIGRATED  DD_TRACE_GRAPHQL_ERROR_EXTENSIONS  1

PACKAGE: instrumentation/httptrace
  STATUS      CONFIG                                                 CALL_SITES
  UNMIGRATED  DD_GOOGLE_CLOUD_PUBSUB_PROPAGATION_AS_SPAN_LINKS       1
  UNMIGRATED  DD_TRACE_BAGGAGE_TAG_KEYS                              1
  UNMIGRATED  DD_TRACE_CLIENT_IP_ENABLED                             1
  UNMIGRATED  DD_TRACE_HTTP_SERVER_ERROR_STATUSES                    1
  UNMIGRATED  DD_TRACE_HTTP_URL_QUERY_STRING_ALLOWLIST               1
  UNMIGRATED  DD_TRACE_HTTP_URL_QUERY_STRING_ALLOWLIST_CLIENT        1
  UNMIGRATED  DD_TRACE_HTTP_URL_QUERY_STRING_ALLOWLIST_SERVER        1
  UNMIGRATED  DD_TRACE_HTTP_URL_QUERY_STRING_DISABLED                1
  UNMIGRATED  DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED               1
  UNMIGRATED  DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP               2
  UNMIGRATED  DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT  1
  UNMIGRATED  DD_TRACE_RESOURCE_RENAMING_ENABLED                     1

PACKAGE: instrumentation/internal/namingschema
  STATUS      CONFIG                                             CALL_SITES
  STILL_READ  DD_SERVICE                                         1
  STILL_READ  DD_TRACE_SPAN_ATTRIBUTE_SCHEMA                     1
  UNMIGRATED  DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED  1

PACKAGE: internal
  STATUS      CONFIG                         CALL_SITES
  STILL_READ  DD_AGENT_HOST                  1
  STILL_READ  DD_TAGS                        1
  STILL_READ  DD_TRACE_AGENT_PORT            1
  STILL_READ  DD_TRACE_AGENT_URL             1
  UNMIGRATED  DD_EXTERNAL_ENV                1
  UNMIGRATED  DD_GIT_COMMIT_SHA              1
  UNMIGRATED  DD_GIT_REPOSITORY_URL          1
  UNMIGRATED  DD_TRACE_GIT_METADATA_ENABLED  1

PACKAGE: internal/appsec
  STATUS      CONFIG           CALL_SITES
  UNMIGRATED  DD_APPSEC_RULES  1

PACKAGE: internal/appsec/config
  STATUS      CONFIG                                                CALL_SITES
  UNMIGRATED  DD_API_SECURITY_DOWNSTREAM_BODY_ANALYSIS_SAMPLE_RATE  1
  UNMIGRATED  DD_API_SECURITY_ENABLED                               1
  UNMIGRATED  DD_API_SECURITY_MAX_DOWNSTREAM_REQUEST_BODY_ANALYSIS  1
  UNMIGRATED  DD_API_SECURITY_PROXY_SAMPLE_RATE                     1
  UNMIGRATED  DD_API_SECURITY_REQUEST_SAMPLE_RATE                   1
  UNMIGRATED  DD_API_SECURITY_SAMPLE_DELAY                          1
  UNMIGRATED  DD_APM_TRACING_ENABLED                                1
  UNMIGRATED  DD_APPSEC_AGENTIC_ONBOARDING                          1
  UNMIGRATED  DD_APPSEC_ENABLED                                     1
  UNMIGRATED  DD_APPSEC_MAX_STACK_TRACE_DEPTH                       1
  UNMIGRATED  DD_APPSEC_RASP_ENABLED                                1
  UNMIGRATED  DD_APPSEC_RULES                                       1
  UNMIGRATED  DD_APPSEC_SCA_ENABLED                                 1
  UNMIGRATED  DD_APPSEC_STACK_TRACE_ENABLED                         1
  UNMIGRATED  DD_APPSEC_TRACE_RATE_LIMIT                            1
  UNMIGRATED  DD_APPSEC_WAF_TIMEOUT                                 1

PACKAGE: internal/appsec/listener/httpsec
  STATUS      CONFIG                     CALL_SITES
  UNMIGRATED  DD_TRACE_CLIENT_IP_HEADER  1

PACKAGE: internal/bazel
  STATUS      CONFIG                                  CALL_SITES
  UNMIGRATED  DD_TEST_OPTIMIZATION_MANIFEST_FILE      1
  UNMIGRATED  DD_TEST_OPTIMIZATION_PAYLOADS_IN_FILES  1

PACKAGE: internal/civisibility/envconfig
  STATUS      CONFIG                   CALL_SITES
  STILL_READ  DD_CIVISIBILITY_ENABLED  1

PACKAGE: internal/civisibility/integrations
  STATUS      CONFIG                                               CALL_SITES
  STILL_READ  DD_SERVICE                                           1
  STILL_READ  DD_TRACE_DEBUG                                       1
  UNMIGRATED  DD_CIVISIBILITY_CODE_COVERAGE_REPORT_UPLOAD_ENABLED  1
  UNMIGRATED  DD_CIVISIBILITY_FLAKY_RETRY_COUNT                    1
  UNMIGRATED  DD_CIVISIBILITY_FLAKY_RETRY_ENABLED                  1
  UNMIGRATED  DD_CIVISIBILITY_GIT_UPLOAD_ENABLED                   1
  UNMIGRATED  DD_CIVISIBILITY_IMPACTED_TESTS_DETECTION_ENABLED     1
  UNMIGRATED  DD_CIVISIBILITY_SUBTEST_FEATURES_ENABLED             1
  UNMIGRATED  DD_CIVISIBILITY_TOTAL_FLAKY_RETRY_COUNT              1
  UNMIGRATED  DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES            1
  UNMIGRATED  DD_TEST_MANAGEMENT_ENABLED                           1

PACKAGE: internal/civisibility/integrations/gotesting
  STATUS      CONFIG                                                           CALL_SITES
  UNMIGRATED  DD_CIVISIBILITY_INTERNAL_PARALLEL_EARLY_FLAKE_DETECTION_ENABLED  1
  UNMIGRATED  DD_TEST_MANAGEMENT_ENABLED                                       1

PACKAGE: internal/civisibility/integrations/logs
  STATUS      CONFIG                        CALL_SITES
  UNMIGRATED  DD_CIVISIBILITY_LOGS_ENABLED  1

PACKAGE: internal/civisibility/utils
  STATUS      CONFIG                              CALL_SITES
  STILL_READ  DD_SERVICE                          1
  UNMIGRATED  DD_ACTION_EXECUTION_ID              1
  UNMIGRATED  DD_PIPELINE_EXECUTION_ID            1
  UNMIGRATED  DD_TEST_OPTIMIZATION_ENV_DATA_FILE  1
  UNMIGRATED  DD_TEST_SESSION_NAME                1

PACKAGE: internal/civisibility/utils/net
  STATUS      CONFIG                             CALL_SITES
  STILL_READ  DD_API_KEY                         1
  STILL_READ  DD_CIVISIBILITY_AGENTLESS_ENABLED  1
  STILL_READ  DD_CIVISIBILITY_AGENTLESS_URL      1
  STILL_READ  DD_ENV                             1
  STILL_READ  DD_SERVICE                         1
  STILL_READ  DD_SITE                            1
  STILL_READ  DD_TAGS                            1
  STILL_READ  DD_VERSION                         1
  UNMIGRATED  DD_CODE_COVERAGE_FLAGS             1

PACKAGE: internal/civisibility/utils/telemetry
  STATUS      CONFIG                                         CALL_SITES
  UNMIGRATED  DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER  1

PACKAGE: internal/globalconfig
  STATUS      CONFIG                           CALL_SITES
  UNMIGRATED  DD_INSTRUMENTATION_INSTALL_ID    1
  UNMIGRATED  DD_INSTRUMENTATION_INSTALL_TIME  1
  UNMIGRATED  DD_INSTRUMENTATION_INSTALL_TYPE  1

PACKAGE: internal/hostname
  STATUS      CONFIG       CALL_SITES
  UNMIGRATED  DD_HOSTNAME  1

PACKAGE: internal/namingschema
  STATUS      CONFIG                                             CALL_SITES
  STILL_READ  DD_SERVICE                                         1
  STILL_READ  DD_TRACE_SPAN_ATTRIBUTE_SCHEMA                     1
  UNMIGRATED  DD_TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED  1

PACKAGE: internal/processtags
  STATUS      CONFIG                                          CALL_SITES
  UNMIGRATED  DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED  1

PACKAGE: internal/remoteconfig
  STATUS      CONFIG                                  CALL_SITES
  STILL_READ  DD_ENV                                  1
  UNMIGRATED  DD_RC_TUF_ROOT                          1
  UNMIGRATED  DD_REMOTE_CONFIGURATION_ENABLED         1
  UNMIGRATED  DD_REMOTE_CONFIG_POLL_INTERVAL_SECONDS  1

PACKAGE: internal/telemetry
  STATUS      CONFIG                                             CALL_SITES
  STILL_READ  DD_API_KEY                                         1
  STILL_READ  DD_SITE                                            1
  UNMIGRATED  DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT  1
  UNMIGRATED  DD_INSTRUMENTATION_TELEMETRY_ENABLED               1
  UNMIGRATED  DD_TELEMETRY_DEBUG                                 1
  UNMIGRATED  DD_TELEMETRY_DEPENDENCY_COLLECTION_ENABLED         1
  UNMIGRATED  DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL           1
  UNMIGRATED  DD_TELEMETRY_HEARTBEAT_INTERVAL                    1
  UNMIGRATED  DD_TELEMETRY_LOG_COLLECTION_ENABLED                1
  UNMIGRATED  DD_TELEMETRY_METRICS_ENABLED                       1

PACKAGE: openfeature
  STATUS      CONFIG                                                     CALL_SITES
  STILL_READ  DD_ENV                                                     2
  STILL_READ  DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED                  1
  STILL_READ  DD_SERVICE                                                 2
  STILL_READ  DD_VERSION                                                 2
  UNMIGRATED  DD_EXPERIMENTAL_FLAGGING_PROVIDER_SPAN_ENRICHMENT_ENABLED  1
  UNMIGRATED  DD_FLAGGING_EVALUATION_COUNTS_ENABLED                      1

PACKAGE: profiler
  STATUS      CONFIG                                    CALL_SITES
  STILL_READ  DD_API_KEY                                1
  STILL_READ  DD_ENV                                    1
  STILL_READ  DD_SERVICE                                1
  STILL_READ  DD_SITE                                   1
  STILL_READ  DD_TAGS                                   1
  STILL_READ  DD_TRACE_STARTUP_LOGS                     1
  STILL_READ  DD_VERSION                                1
  UNMIGRATED  DD_PROFILING_AGENTLESS                    1
  UNMIGRATED  DD_PROFILING_DEBUG_COMPRESSION_SETTINGS   1
  UNMIGRATED  DD_PROFILING_DELTA                        1
  UNMIGRATED  DD_PROFILING_ENABLED                      3
  UNMIGRATED  DD_PROFILING_ENDPOINT_COUNT_ENABLED       1
  UNMIGRATED  DD_PROFILING_EXECUTION_TRACE_ENABLED      1
  UNMIGRATED  DD_PROFILING_EXECUTION_TRACE_LIMIT_BYTES  1
  UNMIGRATED  DD_PROFILING_EXECUTION_TRACE_PERIOD       1
  UNMIGRATED  DD_PROFILING_FLUSH_ON_EXIT                1
  UNMIGRATED  DD_PROFILING_OUTPUT_DIR                   1
  UNMIGRATED  DD_PROFILING_UPLOAD_TIMEOUT               1
  UNMIGRATED  DD_PROFILING_URL                          1

@datadog-prod-us1-5

datadog-prod-us1-5 Bot commented Aug 3, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 87.50%
Overall Coverage: 63.16% (+11.31%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: c17535a | Docs | Datadog PR Page | Give us feedback!

@pr-commenter

pr-commenter Bot commented Aug 3, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-08-04 14:12:17

Comparing candidate commit c17535a in PR branch eliottness/pprof-trace-id-no-local-root with baseline commit 20addd7 in branch main.

Found 24 performance improvements and 4 performance regressions! Performance is the same for 298 metrics, 0 unstable metrics, 1 flaky benchmarks without significant changes.

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:BenchmarkOTLPProtoSize/1000spans

  • 🟥 execution_time [+17.911µs; +19.092µs] or [+5.683%; +6.057%]

scenario:BenchmarkOTLPProtoSize/100spans

  • 🟥 execution_time [+1.788µs; +1.900µs] or [+5.684%; +6.040%]

scenario:BenchmarkOTLPProtoSize/10spans

  • 🟥 execution_time [+201.408ns; +213.392ns] or [+6.328%; +6.705%]

scenario:BenchmarkOTLPProtoSize/1span

  • 🟥 execution_time [+25.366ns; +29.274ns] or [+6.604%; +7.622%]

scenario:BenchmarkOTelApiWithCustomTags/datadog_otel_api

  • 🟩 allocated_mem [-62 bytes; -53 bytes] or [-2.480%; -2.105%]
  • 🟩 allocations [-1; -1] or [-3.846%; -3.846%]

scenario:BenchmarkOTelApiWithCustomTags/otel_api

  • 🟩 allocations [-1; -1] or [-2.439%; -2.439%]

scenario:BenchmarkPartialFlushing/Disabled

  • 🟩 allocated_mem [-9.031MB; -8.370MB] or [-4.724%; -4.379%]
  • 🟩 allocations [-100015; -100005] or [-7.689%; -7.688%]
  • 🟩 avgHeapInUse(Mb) [-4.799MB; -1.481MB] or [-8.892%; -2.744%]

scenario:BenchmarkPartialFlushing/Enabled

  • 🟩 allocated_mem [-11.643MB; -9.091MB] or [-6.388%; -4.988%]
  • 🟩 allocations [-100022; -100008] or [-7.676%; -7.675%]

scenario:BenchmarkSingleSpanRetention/no-rules

  • 🟩 allocated_mem [-8.856KB; -8.854KB] or [-5.428%; -5.427%]
  • 🟩 allocations [-101; -101] or [-7.634%; -7.634%]
  • 🟩 execution_time [-11.055µs; -7.504µs] or [-3.495%; -2.372%]

scenario:BenchmarkSingleSpanRetention/with-rules/match-all

  • 🟩 allocated_mem [-8.889KB; -8.863KB] or [-5.444%; -5.428%]
  • 🟩 allocations [-101; -101] or [-7.634%; -7.634%]

scenario:BenchmarkSingleSpanRetention/with-rules/match-half

  • 🟩 allocated_mem [-8.895KB; -8.856KB] or [-5.447%; -5.424%]
  • 🟩 allocations [-101; -101] or [-7.634%; -7.634%]

scenario:BenchmarkStartSpan

  • 🟩 allocated_mem [-88 bytes; -88 bytes] or [-5.490%; -5.490%]
  • 🟩 allocations [-1; -1] or [-6.667%; -6.667%]
  • 🟩 execution_time [-111.542ns; -79.258ns] or [-5.825%; -4.139%]

scenario:BenchmarkStartSpanConfig/scenario_WithStartSpanConfig

  • 🟩 allocated_mem [-56 bytes; -56 bytes] or [-2.756%; -2.756%]
  • 🟩 allocations [-1; -1] or [-5.882%; -5.882%]

scenario:BenchmarkStartSpanConfig/scenario_none

  • 🟩 allocated_mem [-56 bytes; -56 bytes] or [-2.365%; -2.365%]
  • 🟩 allocations [-1; -1] or [-5.263%; -5.263%]

scenario:BenchmarkTracerAddSpans

  • 🟩 allocated_mem [-56 bytes; -56 bytes] or [-2.318%; -2.318%]
  • 🟩 allocations [-1; -1] or [-4.762%; -4.762%]

Known flaky benchmarks

These benchmarks are marked as flaky and will not trigger a failure. Modify FLAKY_BENCHMARKS_REGEX to control which benchmarks are marked as flaky.

Known flaky benchmarks without significant changes:

  • scenario:BenchmarkOTLPTraceWriterFlush

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@eliottness eliottness changed the title feat(tracer): replace local root profiler label with trace ID feat(tracer): add AppSec trace ID pprof label Aug 3, 2026
@eliottness

Copy link
Copy Markdown
Contributor Author

keep in mind the benchmarks are like this because appsec and profiling is enabled, there is no changes for APM only customers and APM+appsec or APM+Profling will both only see half the performance impact

@eliottness
eliottness marked this pull request as ready for review August 4, 2026 08:50
@eliottness
eliottness requested review from a team as code owners August 4, 2026 08:50
@eliottness

Copy link
Copy Markdown
Contributor Author

@copilot review

@eliottness

Copy link
Copy Markdown
Contributor Author

@codex review

Copilot AI commented Aug 4, 2026

Copy link
Copy Markdown

@copilot review

The changes look correct and well-structured. Here's my review:

ddtrace/tracer/tracer.go

  • The appsecCorrelation gate is clean: appsec.Enabled() is called once and reused for both the early-return check and the label append, avoiding redundant calls.
  • Label ordering (alphabetical) is preserved per the go1.24 optimization comment.
  • The len(labels) > 0 guard at the end correctly handles the edge case where AppSec is enabled but neither hotspots nor endpoints are set — traceprof.TraceID is the only label, and it still flows through correctly.
  • One minor observation: when only AppSec is enabled (no profiling features), pprofCtxRestore is still set, which means goroutine label cleanup happens on span finish. This is the correct behavior — the trace id label must be cleared when the span ends.

ddtrace/tracer/tracer_test.go

  • Test comments are updated to reflect the new behavior.
  • The hotspots sub-test now asserts traceprof.TraceID is absent, which correctly validates the separation of profiling from AppSec labels.
  • The removed traceID variable and its regexp check are appropriate since the hotspots path no longer emits it.

CI: All workflow runs on this branch are passing (Code Freeze, Static Checks, CodeQL, Pull Request Tests, System Tests, etc.).

No issues found — the implementation correctly gates the trace id label behind appsec.Enabled() without affecting profiling-only paths.

@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: 0053e3ee16

ℹ️ 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 ddtrace/tracer/spancontext.go Outdated
Comment on lines +134 to +136
func (t *traceID) hexEncodedCached() string {
if t.hexEncoded == "" {
t.cacheHex()

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 Cache the trace ID before invoking custom samplers

When AppSec is enabled and a custom Sampler.Sample publishes the supplied span or context to another goroutine, StartSpan has already invoked that callback at tracer.go:1024 before reaching this write via applyPPROFLabels at tracer.go:1031. A concurrent TraceID() or Inject() then reads hexEncoded while cacheHex mutates it, producing a reproducible data race under go test -race; populate the cache before invoking the sampler callback or keep this path read-only.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 84722c2 — good catch, this was a real race.

You are right about the ordering: StartSpan calls t.sample(span) (tracer.go:1024) before applyPPROFLabels (tracer.go:1031), so a custom Sampler that publishes the span makes any later write to its SpanContext racy.

Rather than caching earlier inside the label path, the write now happens in newSpanContext (tracer.go:949), which runs before both the sampler and the label path, while the context is still private to the starting goroutine. applyPPROFLabels is now a pure read via HexEncoded(), which returns the cache when set and otherwise encodes locally without storing. hexEncodedCached() is deleted.

Added TestApplyPPROFLabelsTraceIDNoCacheWriteAfterPublish, which reproduces exactly your scenario: a custom Sampler publishes every span to a reader goroutine that calls TraceID() while StartSpan is still running. It fails on the previous commit under -race:

WARNING: DATA RACE
  ...tracer.(*traceID).cacheHex()
  ...tracer.(*traceID).hexEncodedCached()

and passes now. Full go test -race ./ddtrace/tracer ./internal/traceprof/... is green.

Comment thread ddtrace/tracer/spancontext.go Outdated
Comment on lines +135 to +137
if t.hexEncoded == "" {
t.cacheHex()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There is a TOCTU race here; unless we can absolutely guarantee that hexEncodedCached() may only be called by one specific goroutine (which isn't enforced at the moment; and multiple spans may be started concurrently from the same extracted context, for example).

Apparently, there is a similar race around trace.root in similar circumstances.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Both confirmed. The TOCTOU is fixed in 84722c2; the trace.root one is real but pre-existing, so I would like to do it separately.

1. hexEncodedCached() TOCTOU — fixed. You were right that nothing enforced single-goroutine access. hexEncodedCached() is deleted. The cache is now finalized in newSpanContext (called at tracer.go:949), which is before t.sample (:1024) and applyPPROFLabels (:1031), so the context is provably still private to the starting goroutine. The label path only reads, via HexEncoded(). Regression test TestApplyPPROFLabelsTraceIDNoCacheWriteAfterPublish fails on the parent commit under -race and passes now.

2. trace.root — confirmed, and it reproduces on main unchanged. newSpanContext does an unsynchronized check-then-set:

if context.trace.root == nil {
    context.trace.root = span
}

Concurrent children of one extracted context all race there, and span.Root() can then hand applyPPROFLabels a span another goroutine is still initializing from the pool. I reproduced it on a clean origin/main worktree (20addd7ce, no changes from this PR) with 32 concurrent children of one extracted context:

WARNING: DATA RACE
Write at ... by goroutine 51:
  tracer.newSpanContext()  spancontext.go:334
Previous read at ... by goroutine 56:
  tracer.spanStart()       tracer.go:937

Since it is orthogonal to the label change and sits on the span-start hot path, I would rather not fold it into this PR: the fix wants trace.root published once under the trace lock and read atomically, which touches every trace.root reader and needs its own Benchmarking Platform run. Happy to open that PR right after this one — or to fold it in here if you prefer it shipped together.

For that reason the test I added deliberately covers the hex path only (TestNewSpanContextTraceIDHexInheritsParentReadOnly, asserting the child copy never rewrites the shared parent). Any concurrent shared-parent test still trips the trace.root race regardless of profiler settings, so it would have been red for a reason unrelated to this PR.

Comment thread ddtrace/tracer/spancontext.go Outdated
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@eliottness

Copy link
Copy Markdown
Contributor Author

Note on the four BenchmarkOTLPProtoSize regressions: they are a benchmark-layout artifact, not runtime work added by this PR.

BenchmarkOTLPProtoSize builds and converts all spans in setup and only times proto.Size(tracesData) on the finished protobuf tree. Nothing this PR touches is on that timed path, and the protobuf input is byte-identical.

What does change is where the linker places the protobuf sizing code. Comparing the baseline and candidate test binaries, the sizing functions are identical instructions at shifted addresses (0x110 apart), which moves proto.Size off its 64-byte alignment:

baseline   1003c3340 T google.golang.org/protobuf/proto.Size
candidate  1003c3450 T google.golang.org/protobuf/proto.Size

Two supporting results:

  • Building each commit of this branch as its own binary and interleaving 10 runs each, no commit introduces the slowdown; every revision is neutral or faster than main locally.
  • Rebuilding the same source with different -ldflags=-randlayout seeds moves the same benchmark by a comparable margin with zero source change.

The per-span delta (+18-28 ns, no allocation change) is consistent with the protobuf per-span sizing loop straddling instruction-cache lines differently.

Suggested follow-up, outside this PR: run the Benchmarking Platform stability workflow for BenchmarkOTLPProtoSize and either add it to FLAKY_BENCHMARKS_REGEX or move it into a minimal package so unrelated tracer changes cannot relocate the protobuf code. BenchmarkOTLPTraceWriterFlush is already flagged flaky for related reasons.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@eliottness

Copy link
Copy Markdown
Contributor Author

Ran a five-lane review over this branch (coverage, concurrency, memory, package consistency, dead-code/comment audit). Fixed everything in scope in 3b135bc1d; recording the pre-existing findings here so they are not lost.

Fixed

  1. pprof label writes happened after the span was published. applyPPROFLabels wrote pprofCtxActive/pprofCtxRestore — both declared +checklocks:muafter t.sample(span) had already handed the span to a custom Sampler. Same class as the TOCTOU already fixed, one layer up. Labels are now applied before the sampler, and the +checklocksignore rationale is truthful. Endpoint counting is unaffected: GlobalEndpointCounter().Inc was never conditioned on the sampling decision.

  2. AppSec-only spans could acquire a trace endpoint label. The resource-override relabel in Span.setTagLocked was gated on pprofCtxActive != nil. Now that AppSec populates that field, a later SetTag(ext.ResourceName, ...) added an endpoint label even with endpoint profiling disabled. Now gated on the endpoint feature itself. Locked by TestSetResourceNameDoesNotLeakEndpointLabel, verified to fail without the guard.

  3. WithProfilerCodeHotspots godoc still promised local root span id.

  4. Empty label set left restoration armed — endpoint profiling with a PII-unsafe resource could emit no labels while pprofCtxRestore stayed set.

  5. Coverage and comment rot: full AppSec x hotspots x endpoints matrix with explicit absence assertions (including the removed legacy key), trace-ID wire format pinned against known IDs (32 chars, lowercase, zero-padded, stable across a trace), the sampler race test made deterministic and now asserting the cache is finalized at sampler time, and stale references to the deleted hexEncodedCached removed.

Benchmarks under the CI configuration (Go 1.25.9, DD_ENV/DD_SERVICE set, 10 paired runs) show no regression: allocations improve 4.8-7.7% and no timing lane is statistically significant.

Pre-existing, deliberately not in this PR

  • appsec.Enabled() reads activeAppSec.started under mu, but remote-config activation writes it without that mutex (internal/appsec/appsec.go). Independent of this PR, though this PR does add reads on the span-start path. Worth an atomic or consistent locking fix in the AppSec package.
  • Pooled spans retain SpanContext.spanSnapshot.pprofCtx, so an idle pooled span can pin a request context chain. clear() intentionally keeps s.context, so this is a lifecycle decision rather than a leak to patch here.
  • Finish skips label restoration if tracing is disabled mid-span, and cross-goroutine Finish cannot restore the starter goroutine labels.
  • trace.root check-then-set race (already discussed in the review thread above; reproduces on unmodified main).

@nsrip-dd nsrip-dd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm inclined to approve this if it unblocks an important feature for appsec, but I want to make sure we clearly document what this is for and what we're paying for it. So, I have a few clarifying points/questions, and perhaps you could update the PR description so we have a record of this?

  • In the motivation, you say "AppSec needs a whole-trace correlation key in profiles". To be clear, it doesn't actually matter if the new label in the CPU/goroutine profile, right? The thing that's reading these labels is eBPF code that knows how to find goroutine labels. Nothing new is touching the profiles that the client library uploads.
    • So we could, for example, strip them from the CPU profile before we upload it.
  • I also want to call out that this makes CPU profiles bigger. We care about this because upload/intake bandwidth costs us and our customers money. I have about 600 CPU profiles at hand. I ran a quick test replacing the local root span ID labels with trace ID labels, taking the local root span ID and padding out to 128 bits, hex-encoded. I did random padding and all zeros. With random padding, CPU profiles on average were 10% bigger, with a p95 increase of 35%, after zstd compression. With zero padding the average and p95 are about half of that. I haven't yet done the math on overall payload size increase, but CPU profiles tend to be the largest things besides execution traces, so I would anticipate a modest but non-zero size increase.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@eliottness

Copy link
Copy Markdown
Contributor Author

Thanks — both points are fair, and the second one changes the cost calculus. I investigated the strip idea; findings below, and I will fold the size cost into the PR description.

1. Who reads the label

Your eBPF hypothesis holds up as far as I can verify from open sources, but I cannot fully close it from this repo.

  • Supporting it: the draft Agent change in [CWS] Go pprof-label span-context reader + legacy TLS removal datadog-agent#53988 ships span_go.h, which walks TLS to runtime.g to runtime.m to the current goroutine and copies raw pprof label key/value pairs into an eBPF ring for userspace parsing. That is a live-memory read, not uploaded-profile parsing.
  • What I cannot prove here: that no intake or backend workflow consumes trace id from the uploaded pprof. That needs an explicit confirmation from the AppSec/CWS side. If someone can confirm it, stripping is safe.

Worth separating out: trace endpoint and endpoint counting are independent. Counts go through traceprof.GlobalEndpointCounter(), are reset per period in profiler.go, and are uploaded as endpoint_counts — so stripping label data would not affect the unit-of-work feature. span id and trace endpoint are both read from uploaded profiles by the existing correlation and endpoint-scoping features, so neither should be touched.

2. Stripping is possible, but not free

I checked whether we could piggyback on an existing decode. We cannot, for CPU specifically:

  • CPU Collect calls StopCPUProfile and then only recompresses — gzip in, zstd out, streaming, no pprof parse (profiler/profile.go:118-122, profiler/compression.go:190).
  • fastdelta does already decode and re-encode, and a filter there would be nearly free — but CPU never reaches it. CPUProfile declares no DeltaValues, and delta is gated on len(t.DeltaValues) > 0 (profiler/profile.go:383); only heap, mutex, and block configure it.

So the cheapest correct option is a sanitizer at collection time, right after StopCPUProfile, feeding a noCompression -> configured output compressor. That adds one full protobuf decode/encode per CPU profile. One subtlety: it has to drop the string-table entries too, not just the sample label fields, otherwise the high-cardinality IDs stay in the payload and most of the saving evaporates.

Affected profile types are CPU, goroutine, and the experimental goroutine-leak profile. Heap, block, and mutex serialize samples without goroutine labels, and the execution trace is a separate go.trace payload, so those need no change.

3. Alternatives

  • Zero-padding: cheaper as you measured, but it is not a valid 128-bit ID once the upper bits are non-zero, which they are for native 128-bit generation. Breaks full-trace correlation.
  • Compact encoding (unpadded base64url, 22 chars vs 32): ~31% fewer raw value bytes, but needs a coordinated format change across eBPF, userspace, and backend.
  • Sampled traces only: would reopen the publication race this PR just fixed, since labels are deliberately applied before t.sample. Also loses correlation for security-relevant unsampled traces.
  • Local-root only: does not help size, because children inherit the root trace ID into their pprof context, so samples carry it either way.

Recommendation: keep the label live for eBPF and strip it from CPU, goroutine, and goroutine-leak payloads at collection time — conditional on someone confirming no backend consumer. That is roughly 40-80 production lines using google/pprof/profile, or 150-250 with a lower-allocation pproflite implementation. I would do it as a separate profiler PR rather than growing this one, and gate it so span id and trace endpoint are provably untouched.

Happy to take that on. Do you want the strip before this merges, or as an immediate follow-up?

@nsrip-dd

nsrip-dd commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Your eBPF hypothesis holds up as far as I can verify from open sources, but I cannot fully close it from this repo.

This was a question for you @eliottness not an LLM. Is it right that this label is consumed by eBPF code running in/observing a process, and that nothing on the AppSec side is going to look at the aggregated CPU profiles that this client library uploads? The agent PR referenced there seems to confirm that, but I'd like to know definitively.

Regarding stripping the label, yeah, I can do that as a followup if we need to.

@eliottness

Copy link
Copy Markdown
Contributor Author

@nsrip-dd Help me I am loosing control of my AI

I can confirm we only use pprof labels as a mean-to-an-end here to have custom data readable from an eBPF probe

@eliottness eliottness changed the title feat(tracer): add AppSec trace ID pprof label feat(tracer): rollout local root span id pprof label in favor of trace id for appsec Aug 11, 2026

@mtoffl01 mtoffl01 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

just some nits 😄

Comment on lines +309 to +320
if parent != nil && context.traceID.value == parent.traceID.value {
context.traceID.hexEncoded = parent.traceID.hexEncoded
}
// AppSec correlates security events with profiles through the "trace id"
// pprof label, which needs the hex form. Finalize the cache here, while the
// context is still private to this goroutine: StartSpan hands the span to
// the sampler (and any custom Sampler may publish it) before it applies the
// pprof labels, so a lazy write from that later point would race with
// readers such as TraceID and Inject.
if context.traceID.hexEncoded == "" && appsec.Enabled() {
context.traceID.cacheHex()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can these statements be combined? they both look at context.traceID.hexEncoded. It seems context.traceID.hexEncoded is always "" if the span doesn't have a parent, so you can probably do some if/else-ing here?

Comment on lines +3670 to +3672
// Disable AppSec so locally started spans keep a cold hex cache; with it
// enabled, newSpanContext finalizes the cache and the "cold cache" subtest
// below could no longer exercise the non-caching HexEncoded read path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps I'm not the target audience for this, but I don't know what this comment means 😆 . Can you make it clearer/simpler?

Comment thread ddtrace/tracer/tracer.go
Comment on lines +1022 to +1025
// Apply the pprof labels before t.sample: a custom Sampler receives the span
// and may publish it to another goroutine, after which writing span fields
// here would race with that goroutine (e.g. SetTag or Finish).
t.applyPPROFLabels(span.pprofCtxRestore, span, cSnap)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a more robust way to ensure pprof labels are always added to the span before sampling?

Comment thread ddtrace/tracer/tracer.go
// +checklocksignore — Initialization time, called from StartSpan before the span
// is handed to the sampler, so it is not yet shared with other goroutines.
func (t *tracer) applyPPROFLabels(ctx gocontext.Context, span *Span, snap internalconfig.SpanStartSnapshot) {
// "trace id" is AppSec-only. Profiling features retain their own labels

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"trace id" is AppSec-only

Is confusing to me. "trace id" to me means "ID of the current trace" but I assume you mean the "trace id" label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants