Skip to content

[SVLS-7168] Create inferred Span and Span links for GCP PubSub - #6415

Merged
nina9753 merged 57 commits into
nina.rei/SVLS-7168/gcp-pubsub-batch-pluginfrom
nina.rei/SVLS-7168/gcp-pubsub-synthetic-span
Dec 12, 2025
Merged

[SVLS-7168] Create inferred Span and Span links for GCP PubSub#6415
nina9753 merged 57 commits into
nina.rei/SVLS-7168/gcp-pubsub-batch-pluginfrom
nina.rei/SVLS-7168/gcp-pubsub-synthetic-span

Conversation

@nina9753

@nina9753 nina9753 commented Sep 10, 2025

Copy link
Copy Markdown
Contributor

What does this PR do?

This PR completes the distributed tracing story by adding bidirectional span linking between producer and consumer spans. It enables full trace visualization across the entire message lifecycle: producer → Pub/Sub topic → consumer. The implementation includes intelligent span linking that adapts based on whether messages originated from a single trace or multiple traces.

Consumer Span Linking (Pull Subscriptions)
Enhanced the consumer plugin to create span links back to the producer's batch span. When a consumer receives a message, it reconstructs the producer's batch span context from the metadata injected by the producer plugin. This context becomes a span link, creating a visible connection in the APM UI between the consumer operation and the batch that produced it.

Push Subscription Advanced Linking
Enhanced the push subscription plugin with conditional span linking based on trace ID comparison.
This also follows the industry standard OpenTelemetry concept, representing causal relationships between spans that aren't parent-child.

Web (framework) Plugin Integration Updated the web framework span creation logic to check AsyncLocalStorage for an active pubsub.delivery span before extracting context from HTTP headers. This ensures web framework spans (Express, Fastify, etc.) correctly parented to the delivery span when handling push subscription requests.

Enhanced Batch Metadata
All consumer spans (both pull and push) now include rich batch metadata:

  • Total messages in the batch
  • This message's position (0-indexed)
  • Human-readable description
  • Producer batch span trace and span IDs
  • These become searchable tags in the APM UI

Followup PR with tests can be found here #6414

Motivation

With the previous PRs we made it so we get proper trace context propagation from producer to consumer, but the relationship between the producer's batch span and each individual consumer span wasn't visible in the APM UI. This PR adds span links that let you:

  • See which messages were part of the same batch
  • Trace back from a consumer span to the producer's batch span
  • Understand message delivery patterns across services
  • Debug issues related to message ordering or batching

Plugin Checklist

Additional Notes

Follow-up PR to #6260
Additional information can be found in this doc

image image

@github-actions

github-actions Bot commented Sep 10, 2025

Copy link
Copy Markdown
Contributor

Overall package size

Self size: 13.67 MB
Deduped: 113.87 MB
No deduping: 128.89 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------| | @datadog/libdatadog | 0.7.0 | 35.02 MB | 35.02 MB | | @datadog/native-appsec | 10.3.0 | 20.73 MB | 20.74 MB | | @datadog/pprof | 5.12.0 | 11.19 MB | 11.57 MB | | @datadog/native-iast-taint-tracking | 4.1.0 | 9.01 MB | 9.02 MB | | @opentelemetry/resources | 1.30.1 | 557.67 kB | 7.71 MB | | @opentelemetry/core | 1.30.1 | 908.66 kB | 7.16 MB | | protobufjs | 7.5.4 | 2.95 MB | 5.83 MB | | @datadog/wasm-js-rewriter | 5.0.1 | 2.82 MB | 3.53 MB | | @datadog/native-metrics | 3.1.1 | 1.02 MB | 1.43 MB | | @opentelemetry/api-logs | 0.208.0 | 199.48 kB | 1.42 MB | | @opentelemetry/api | 1.9.0 | 1.22 MB | 1.22 MB | | jsonpath-plus | 10.3.0 | 617.18 kB | 1.08 MB | | import-in-the-middle | 1.15.0 | 127.66 kB | 856.24 kB | | lru-cache | 10.4.3 | 804.3 kB | 804.3 kB | | @datadog/openfeature-node-server | 0.2.0 | 118.51 kB | 437.19 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | source-map | 0.7.6 | 185.63 kB | 185.63 kB | | pprof-format | 2.2.1 | 163.06 kB | 163.06 kB | | @datadog/sketches-js | 2.1.1 | 109.9 kB | 109.9 kB | | @isaacs/ttlcache | 2.1.3 | 90.79 kB | 90.79 kB | | lodash.sortby | 4.7.0 | 75.76 kB | 75.76 kB | | ignore | 7.0.5 | 63.38 kB | 63.38 kB | | istanbul-lib-coverage | 3.2.2 | 34.37 kB | 34.37 kB | | rfdc | 1.4.1 | 27.15 kB | 27.15 kB | | dc-polyfill | 0.1.10 | 26.73 kB | 26.73 kB | | tlhunter-sorted-set | 0.1.0 | 24.94 kB | 24.94 kB | | shell-quote | 1.8.3 | 23.74 kB | 23.74 kB | | limiter | 1.1.5 | 23.17 kB | 23.17 kB | | retry | 0.13.1 | 18.85 kB | 18.85 kB | | semifies | 1.0.0 | 15.84 kB | 15.84 kB | | jest-docblock | 29.7.0 | 8.99 kB | 12.76 kB | | crypto-randomuuid | 1.0.0 | 11.18 kB | 11.18 kB | | ttl-set | 1.0.0 | 4.61 kB | 9.69 kB | | mutexify | 1.4.0 | 5.71 kB | 8.74 kB | | path-to-regexp | 0.1.12 | 6.6 kB | 6.6 kB | | module-details-from-path | 1.0.4 | 3.96 kB | 3.96 kB | | escape-string-regexp | 5.0.0 | 3.66 kB | 3.66 kB |

🤖 This report was automatically generated by heaviest-objects-in-the-universe

@pr-commenter

pr-commenter Bot commented Sep 10, 2025

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2025-12-12 06:20:53

Comparing candidate commit 805f38e in PR branch nina.rei/SVLS-7168/gcp-pubsub-synthetic-span with baseline commit 75dd311 in branch nina.rei/SVLS-7168/gcp-pubsub-batch-plugin.

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

scenario:plugin-http-server-querystring-obfuscation-24

  • 🟥 execution_time [+31.372ms; +34.640ms] or [+5.247%; +5.794%]

@codecov

codecov Bot commented Sep 10, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 56.64336% with 62 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.98%. Comparing base (75dd311) to head (805f38e).

Files with missing lines Patch % Lines
...atadog-instrumentations/src/google-cloud-pubsub.js 12.50% 35 Missing ⚠️
...oogle-cloud-pubsub/src/pubsub-push-subscription.js 7.14% 26 Missing ⚠️
...datadog-plugin-google-cloud-pubsub/src/consumer.js 98.59% 1 Missing ⚠️
Additional details and impacted files
@@                              Coverage Diff                               @@
##           nina.rei/SVLS-7168/gcp-pubsub-batch-plugin    #6415      +/-   ##
==============================================================================
- Coverage                                       84.14%   83.98%   -0.17%     
==============================================================================
  Files                                             517      516       -1     
  Lines                                           22264    22376     +112     
==============================================================================
+ Hits                                            18735    18793      +58     
- Misses                                           3529     3583      +54     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nina9753
nina9753 changed the base branch from nina.rei/SVLS-7168/gcp-push-pubsub-plugin-test to nina.rei/SVLS-7168/gcp-pubsub-batch-plugin October 29, 2025 16:59
@nina9753
nina9753 force-pushed the nina.rei/SVLS-7168/gcp-pubsub-synthetic-span branch from 3efb6be to 128343d Compare October 29, 2025 19:26
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Oct 29, 2025

Copy link
Copy Markdown

⚠️ Tests

⚠️ Warnings

❄️ 1 New flaky test detected

cypress@10.2.0 esm intelligent test runner can skip tests received by the intelligent test runner API and still reports code coverage from integration-tests/cypress/cypress.spec.js (Datadog) (Fix with Cursor)
Cannot read properties of undefined (reading 'content')

ℹ️ Info

🧪 All tests passed

This comment will be updated automatically if new data arrives.
* Fix with Cursor requires Datadog plugin ≥v2.17.0
🔗 Commit SHA: 805f38e | Docs | Datadog PR Page | Was this helpful? Give us feedback!

@nina9753
nina9753 force-pushed the nina.rei/SVLS-7168/gcp-pubsub-batch-plugin branch from d7f0d5e to 3fdb180 Compare October 29, 2025 20:48
@nina9753
nina9753 force-pushed the nina.rei/SVLS-7168/gcp-pubsub-synthetic-span branch from 128343d to ee0b34e Compare October 29, 2025 20:53
@nina9753
nina9753 force-pushed the nina.rei/SVLS-7168/gcp-pubsub-batch-plugin branch from 3fdb180 to 6393626 Compare October 30, 2025 13:09
@nina9753
nina9753 force-pushed the nina.rei/SVLS-7168/gcp-pubsub-synthetic-span branch 2 times, most recently from 6ec410a to c8815fe Compare October 30, 2025 14:29
@nina9753
nina9753 force-pushed the nina.rei/SVLS-7168/gcp-pubsub-batch-plugin branch from d76ed3b to f638388 Compare November 4, 2025 21:44
@nina9753
nina9753 force-pushed the nina.rei/SVLS-7168/gcp-pubsub-synthetic-span branch from c8815fe to 929f281 Compare November 4, 2025 23:07
@nina9753
nina9753 force-pushed the nina.rei/SVLS-7168/gcp-pubsub-synthetic-span branch from 9b2c547 to 4ee3b98 Compare November 14, 2025 22:13
@nina9753
nina9753 force-pushed the nina.rei/SVLS-7168/gcp-pubsub-batch-plugin branch from f638388 to 8e3014c Compare November 17, 2025 15:36
- Collect span links from messages 2-N (first becomes parent)
- Extract parent context from first message trace context
- Create pubsub.request span with span links metadata
- Inject batch metadata into all messages (_dd.pubsub_request.*, _dd.batch.*)
- Add 128-bit trace ID support (_dd.p.tid)
- Add operation tag for batched vs single requests
- Add ack context map to preserve trace context across batched acknowledges
- Update producer to use batchSpan._startTime for accurate publish time
- Add explicit parent span support in client plugin
- Wrap Message.ack() to store context before batched gRPC acknowledge
- Update Subscription.emit to properly handle storage context
- Sync auto-load improvements from Branch 1
@nina9753
nina9753 force-pushed the nina.rei/SVLS-7168/gcp-pubsub-batch-plugin branch from 8e3014c to 10eb97c Compare November 18, 2025 17:42
@nina9753
nina9753 force-pushed the nina.rei/SVLS-7168/gcp-pubsub-synthetic-span branch from 1ad601a to 601a630 Compare November 18, 2025 22:49
@nina9753 nina9753 changed the title [SVLS-7168] Create inferred Span for GCP Push Subscriptions (including Cloud Events) [SVLS-7168] Create inferred Span for GCP Push Subscriptions Nov 19, 2025
@nina9753
nina9753 marked this pull request as ready for review November 19, 2025 19:17
@nina9753
nina9753 requested review from a team as code owners November 19, 2025 19:17

@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.

ℹ️ 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 packages/datadog-instrumentations/src/google-cloud-pubsub.js
Comment thread packages/datadog-plugin-google-cloud-pubsub/src/consumer.js Outdated
@nina9753
nina9753 force-pushed the nina.rei/SVLS-7168/gcp-pubsub-batch-plugin branch from ccc9ae5 to f7af73c Compare December 1, 2025 20:37
Comment thread packages/datadog-instrumentations/src/google-cloud-pubsub.js Outdated
const receiveFinishCh = channel('apm:google-cloud-pubsub:receive:finish')
const receiveErrorCh = channel('apm:google-cloud-pubsub:receive:error')

const ackContextMap = new Map()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maps like these which correlate to user-operations (such as HTTP requests) are scary because they can cause memory leaks whenever we don't clean up properly.

For that reason we try to use a WeakMap instead. Those hold a "weak reference" to the key and cause the value to be deleted automatically when the key is garbage collected.

However it only works when the key is an object and your Map is using a string as a key. We could theoretically do something where the request is the key but I don't think that is available later when the delete happens?

Can you explain more about the lifecycle of these ack IDs? Maybe we can figure out something that way.

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.

The lifecycle has a critical gap that prevents WeakMap usage:

// 1. MESSAGE RECEIVED: User gets a Message object
subscription.on('message', (message) => {
  // message.ackId = "abc123" (a string property)
  
  // 2. USER ACKS: We intercept message.ack() and store context
  message.ack()
  // → ackContextMap.set("abc123", context)  // Key is STRING, not object
  
  // 3. MESSAGE OBJECT GOES OUT OF SCOPE: User code finishes
  // The Message object may be GC'd here
  
  // 4. BATCHED ACKNOWLEDGE CALL: SDK batches acks into gRPC call (seconds later)
  pubsubClient.acknowledge({ ackIds: ["abc123", "def456", "ghi789"] })
  // → We only have STRING ackIds, NOT the original Message objects
  // → Need to look up context by string: ackContextMap.get("abc123")
})

Between steps 2 and 4, the Message object is gone, but we need to keep the context alive and retrieve it using only the string ackId. The acknowledge gRPC call creates a single pubsub.request span for the batch, and we need to ensure that span runs in the right async context. Even though N messages were acked, there's only 1 acknowledge API call. So, because the Message Object is not available during retrieval, we must use a Map. To prevent memory leaks, I added a TTL to handle manual cleanup, a graceful shutdown, and cases where acknowledgment never happens (network failure, crash, etc.).

Comment thread packages/datadog-instrumentations/src/google-cloud-pubsub.js Outdated

// Wrap Topic.publish (legacy API)
if (obj.Topic.prototype.publish) {
if (typeof obj.Topic.prototype.publish === 'function') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Did you find that sometimes obj.Topic.prototype.publish was a truthy value but wasn't a function?

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.

No, I have not found evidence that obj.Topic.prototype.publish is ever a truthy non-function value in practice.
The typeof obj.Topic.prototype.publish === 'function' check is following the pattern that's used throughout the dd-trace-js codebase for instrumentation you can see other examples in the same file that i did not add myself

const span = this.startSpan({
childOf,
resource: topic,
resource: `Message from ${topicName}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Like in the previous PR this one also looks like a breaking change.

@nina9753 nina9753 Dec 9, 2025

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.

There shouldn't be any users for this currently, but I can always revert to what is in prod. I wanted to make the spans more descriptive for the blog post and product release.

Comment thread packages/datadog-plugin-google-cloud-pubsub/src/producer.js
@nina9753
nina9753 requested a review from tlhunter December 9, 2025 17:26
Comment on lines +46 to +51
}, 60_000) // Run cleanup every 60 seconds

// Allow process to exit cleanly
if (ackContextCleanupInterval.unref) {
ackContextCleanupInterval.unref()
}

@tlhunter tlhunter Dec 10, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

.unref() will always be present (and it returns the timer instance which is nice for method chaining). But I'm pretty concerned about using a timer like this for cleanup since these types of solutions can often cause memory leaks. E.g. with a 60 second timer there could be a saw tooth memory increase with a 60 second period.

Suggested change
}, 60_000) // Run cleanup every 60 seconds
// Allow process to exit cleanly
if (ackContextCleanupInterval.unref) {
ackContextCleanupInterval.unref()
}
}, 60_000).unref() // Run cleanup every 60 seconds

)

* feat: add producer-side batch message handling with span linking

- Collect span links from messages 2-N (first becomes parent)
- Extract parent context from first message trace context
- Create pubsub.request span with span links metadata
- Inject batch metadata into all messages (_dd.pubsub_request.*, _dd.batch.*)
- Add 128-bit trace ID support (_dd.p.tid)
- Add operation tag for batched vs single requests

* feat: add span linking from delivery span to pubsub.request

- Add _reconstructPubSubContext to extract pubsub.request span ID from headers
- Add span link to original trace context if different from pubsub.request
- Supports same-trace parenting for better trace continuity

* feat: check for pubsub.delivery span in AsyncLocalStorage before extracting from headers

- HTTP plugin now checks if a delivery span is active in storage
- If found, uses delivery span as parent for http.request
- Ensures proper span hierarchy for push subscriptions

* feat: add span linking and batch metadata to pull-based consumer

- Extract pubsub.request span ID from message attributes
- Add span link correlation tags
- Calculate delivery duration from publish start time
- Add batch size and index tags for batched messages

* remove comments

* new test file

* Fix push subscription test module resolution for CI

* Implement full HTTP+Pub/Sub integration tests

* run linter

* test other frameworks

* Fix push subscription tests
@nina9753
nina9753 merged commit 87ecea4 into nina.rei/SVLS-7168/gcp-pubsub-batch-plugin Dec 12, 2025
782 of 784 checks passed
@nina9753
nina9753 deleted the nina.rei/SVLS-7168/gcp-pubsub-synthetic-span branch December 12, 2025 14:11
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.

4 participants