Skip to content

fix(appsec): Go runtime _dd.appsec.enabled missing on aws.lambda#1213

Merged
purple4reina merged 4 commits intomainfrom
romain.marcadier/fix-appsec-go-enabled-tag
Apr 28, 2026
Merged

fix(appsec): Go runtime _dd.appsec.enabled missing on aws.lambda#1213
purple4reina merged 4 commits intomainfrom
romain.marcadier/fix-appsec-go-enabled-tag

Conversation

@RomainMuller
Copy link
Copy Markdown
Contributor

Problem

Two related bugs caused _dd.appsec.enabled and other AppSec tags to be absent from the aws.lambda invocation span for Go (and Java) runtimes when using extension-side App & API Protection.

The symptom was flaky integration tests: sometimes all AppSec tags were missing, and even when that race was won, _dd.appsec.enabled was consistently absent from aws.lambda while the inferred trigger span (aws.lambda.url, aws.httpapi, etc.) always had it.

Root causes

Bug 1 — race condition: AppSec context deleted by placeholder span

Go's tracer emits a placeholder aws.lambda span with resource = "dd-tracer-serverless-span" alongside its child spans in /v0.4/traces. AppSecProcessor::service_entry_span_mut was matching this placeholder (it has name = "aws.lambda" and request_id in meta). Depending on tokio scheduling, the placeholder could reach Processor::process_span after /runtime/invocation/response had set response_seen = true. In that case, process_span would tag the placeholder — harmless, since ChunkProcessor drops it — but then delete the AppSec context. When process_on_platform_runtime_done later sent the extension-built aws.lambda span via send_ctx_spans, the context was gone and no tags were applied.

Fix: filter placeholder spans out of service_entry_span_mut by excluding spans whose resource == INVOCATION_SPAN_RESOURCE ("dd-tracer-serverless-span"). These spans are always dropped before reaching the backend; tagging them is both pointless and harmful.

Bug 2 — _dd.appsec.enabled never pre-set on the invocation span

enrich_ctx_at_platform_done calls inferrer.complete_inferred_spans, which propagates _dd.appsec.enabled from the invocation span to the inferred trigger span via propagate_appsec. However, AppSec has not yet run on the invocation span at that point, so _dd.appsec.enabled is not in invocation_span.metrics. propagate_appsec falls back to the serverless_appsec_enabled config flag for the inferred span (so it always got the tag) but never sets it on the invocation span itself. If AppSec's context was unavailable at flush time for any reason, aws.lambda shipped without the tag. This also explains an existing // todo(duncanista): Add missing metric tags for ASM comment at that exact location.

Fix: pre-set _dd.appsec.enabled = 1.0 on the invocation span in enrich_ctx_at_platform_done when AAP is enabled, before calling complete_inferred_spans. This ensures the inferred span inherits from the actual metric value rather than the config fallback, and guarantees the tag is present even when the AppSec security context cannot be found at flush time.

Why Go/Java only

Only Go and Java use the placeholder span pattern. Python and Node emit their aws.lambda span directly in their tracer payload with resource = function_name, so service_entry_span_mut correctly identifies it, and the context-deletion race cannot happen.

Changes

File Change
bottlecap/src/traces/mod.rs Make INVOCATION_SPAN_RESOURCE pub(crate)
bottlecap/src/appsec/processor/mod.rs Filter placeholder spans in service_entry_span_mut
bottlecap/src/lifecycle/invocation/processor.rs Pre-set _dd.appsec.enabled on invocation span before complete_inferred_spans

Testing

  • Existing appsec_processor_test integration test passes.
  • The race condition is no longer reproducible in Go + Lambda URL integration tests.
  • _dd.appsec.enabled is now consistently present on the aws.lambda span.

Two related bugs caused the `_dd.appsec.enabled` metric and other AppSec
tags to be absent from the `aws.lambda` invocation span for Go (and Java)
runtimes when using extension-side App & API Protection enablement.

**Bug 1 — race condition (context deleted by placeholder span)**

Go's tracer emits a placeholder `aws.lambda` span with `resource =
"dd-tracer-serverless-span"` in its `/v0.4/traces` flush. AppSec's
`service_entry_span_mut` was matching this placeholder (same name,
`request_id` in meta). Depending on tokio scheduling, the placeholder
could arrive and be processed by AppSec _after_ `/runtime/invocation/
response` had already set `response_seen = true`. In that case,
`Processor::process_span` would tag the placeholder (harmless, it gets
dropped by `ChunkProcessor`) and then _delete the AppSec context_ (the
"finalized" branch). When `process_on_platform_runtime_done` later sent
the extension-built `aws.lambda` span via `send_ctx_spans`, the context
was gone and no tags were applied.

Fix: filter placeholder spans out of `service_entry_span_mut` by
excluding spans whose `resource == INVOCATION_SPAN_RESOURCE`. These
spans are always dropped before reaching the backend, so tagging them is
both pointless and harmful.

**Bug 2 — `_dd.appsec.enabled` never pre-set on the invocation span**

`enrich_ctx_at_platform_done` calls `inferrer.complete_inferred_spans`
which propagates `_dd.appsec.enabled` from the invocation span to the
inferred trigger span (e.g. `aws.lambda.url`). However, AppSec has not
yet run on the invocation span at that point, so the metric is not in
`invocation_span.metrics`. `propagate_appsec` therefore falls back to
the `serverless_appsec_enabled` config flag for the _inferred_ span
(which always got the tag) but never sets it on the _invocation_ span
itself. If the AppSec context could not be found at flush time for any
reason, `aws.lambda` shipped without `_dd.appsec.enabled`.

Fix: in `enrich_ctx_at_platform_done`, pre-set `_dd.appsec.enabled =
1.0` on the invocation span when AAP is enabled, before calling
`complete_inferred_spans`. This resolves the pre-existing TODO comment
(`// todo(duncanista): Add missing metric tags for ASM`), ensures the
inferred span inherits the value from the actual metric rather than the
config fallback, and makes the tag present even when the AppSec context
is unavailable.

Both issues are specific to Go (and Java) because only those runtimes
use the placeholder span pattern. Python and Node emit the `aws.lambda`
span directly in their tracer payload, which is not filtered and is not
subject to the context-deletion race.

JJ-Change-Id: xltnyl
Copilot AI review requested due to automatic review settings April 27, 2026 15:43
@RomainMuller RomainMuller requested review from a team as code owners April 27, 2026 15:43
@RomainMuller RomainMuller requested a review from duncanista April 27, 2026 15:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes missing/flaky AppSec tags (notably _dd.appsec.enabled) on the aws.lambda invocation span for Go/Java runtimes when using extension-side App & API Protection, by avoiding placeholder-span races and ensuring the metric is present early enough for propagation.

Changes:

  • Expose INVOCATION_SPAN_RESOURCE within the crate so non-traces modules can reliably identify placeholder invocation spans.
  • Update AppSec span selection to ignore Go/Java placeholder aws.lambda spans (resource == INVOCATION_SPAN_RESOURCE) to prevent premature AppSec context deletion.
  • Pre-set _dd.appsec.enabled = 1.0 on the invocation span when AAP is enabled, before inferred-span completion/propagation.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
bottlecap/src/traces/mod.rs Makes the placeholder invocation span resource constant available crate-wide.
bottlecap/src/appsec/processor/mod.rs Filters out placeholder aws.lambda spans when selecting the service-entry span for AppSec tagging.
bottlecap/src/lifecycle/invocation/processor.rs Ensures _dd.appsec.enabled is present on the invocation span prior to inferred span propagation.

Comment thread bottlecap/src/appsec/processor/mod.rs
Comment thread bottlecap/src/lifecycle/invocation/processor.rs
@purple4reina
Copy link
Copy Markdown
Contributor

@purple4reina
Copy link
Copy Markdown
Contributor

Running the e2e tests here: gitlab.ddbuild.io/DataDog/serverless-e2e-tests/-/pipelines/110011644

lambda-features/tests/test_spans.py::test_appsec_attack_detection[golang-appsecextension] XPASS [ 94%]

The test was flaky though, sometimes passing, sometimes not, but in this case it passed.

Add two unit tests for `Processor::service_entry_span_mut`:
- verifies the non-placeholder span is returned when both a
  placeholder and a real `aws.lambda` span are present
- verifies `None` is returned when only a placeholder span exists

These guard against regressions in the span-selection logic
that caused AppSec context to be prematurely deleted on Go/Java
runtimes, leaving the real invocation span untagged.

JJ-Change-Id: vlkyxo
Add three unit tests for `enrich_ctx_at_platform_done`:
- verifies `_dd.appsec.enabled = 1.0` is set on the invocation
  span when `serverless_appsec_enabled` is true
- verifies the metric is absent when AAP is disabled
- verifies an already-present value is not overwritten by the
  `or_insert` baseline

JJ-Change-Id: myopvz
- `cargo fmt`: reformat `service_entry_span_mut` predicate and
  `setup_appsec` helper
- wrap `AppSec` in backticks in doc comment to satisfy
  `clippy::doc_markdown`
- replace `.get(k).is_none()` with `!contains_key(k)` in
  `enrich_ctx_does_not_set_appsec_enabled_when_aap_disabled`

JJ-Change-Id: xnknop
@purple4reina purple4reina merged commit 47d70b3 into main Apr 28, 2026
55 of 59 checks passed
@purple4reina purple4reina deleted the romain.marcadier/fix-appsec-go-enabled-tag branch April 28, 2026 18:35
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.

4 participants