Skip to content

Commit 4ee3b98

Browse files
committed
feat: add comprehensive span linking for consumer and push subscriptions
- Add _reconstructPubSubRequestContext in consumer for proper span reparenting - Reparent first message in batch to pubsub.request span - Add comprehensive batch metadata and correlation tags - Improve resource naming and service separation - Add delivery duration calculation - Update push subscription logging to warn level with better messages - Add missing headers warning for troubleshooting
1 parent 88b619b commit 4ee3b98

2 files changed

Lines changed: 118 additions & 16 deletions

File tree

packages/datadog-plugin-google-cloud-pubsub/src/consumer.js

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,130 @@
22

33
const { getMessageSize } = require('../../dd-trace/src/datastreams')
44
const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer')
5+
const SpanContext = require('../../dd-trace/src/opentracing/span_context')
6+
const id = require('../../dd-trace/src/id')
57

68
class GoogleCloudPubsubConsumerPlugin extends ConsumerPlugin {
79
static id = 'google-cloud-pubsub'
810
static operation = 'receive'
911

12+
// Reconstruct a SpanContext for the pubsub.request span
13+
// This creates proper Identifier objects that the encoder can serialize
14+
_reconstructPubSubRequestContext (attrs) {
15+
const traceIdLower = attrs['_dd.pubsub_request.trace_id']
16+
const spanId = attrs['_dd.pubsub_request.span_id']
17+
const traceIdUpper = attrs['_dd.p.tid']
18+
19+
if (!traceIdLower || !spanId) return null
20+
21+
try {
22+
const traceId128 = traceIdUpper ? traceIdUpper + traceIdLower : traceIdLower.padStart(32, '0')
23+
const traceId = id(traceId128, 16)
24+
const parentId = id(spanId, 16)
25+
26+
const tags = {}
27+
if (traceIdUpper) tags['_dd.p.tid'] = traceIdUpper
28+
29+
return new SpanContext({
30+
traceId,
31+
spanId: parentId,
32+
tags
33+
})
34+
} catch {
35+
return null
36+
}
37+
}
38+
1039
bindStart (ctx) {
1140
const { message } = ctx
1241
const subscription = message._subscriber._subscription
13-
const topic = subscription.metadata && subscription.metadata.topic
14-
const childOf = this.tracer.extract('text_map', message.attributes) || null
42+
// Get topic from metadata or message attributes (attributes more reliable for pull subscriptions)
43+
const topic = (subscription.metadata && subscription.metadata.topic) ||
44+
(message.attributes && message.attributes['pubsub.topic']) ||
45+
(message.attributes && message.attributes['gcloud.project_id'] ?
46+
`projects/${message.attributes['gcloud.project_id']}/topics/unknown` : null)
47+
48+
// Extract batch metadata from message attributes
49+
const batchRequestTraceId = message.attributes?.['_dd.pubsub_request.trace_id']
50+
const batchRequestSpanId = message.attributes?.['_dd.pubsub_request.span_id']
51+
const batchSize = message.attributes?.['_dd.batch.size']
52+
const batchIndex = message.attributes?.['_dd.batch.index']
53+
54+
// Extract the standard context (this gets us the full 128-bit trace ID, sampling priority, etc.)
55+
let childOf = this.tracer.extract('text_map', message.attributes) || null
56+
57+
// Only reparent to pubsub.request for the FIRST message in the batch (index 0)
58+
// Messages 2-N are in separate traces and should stay as children of their original parent
59+
const isFirstMessage = batchIndex === '0' || batchIndex === 0
60+
if (isFirstMessage && batchRequestSpanId) {
61+
// Reconstruct a proper SpanContext for the pubsub.request span
62+
// This ensures pubsub.receive becomes a child of pubsub.request (not triggerPubsub)
63+
const pubsubRequestContext = this._reconstructPubSubRequestContext(message.attributes)
64+
if (pubsubRequestContext) {
65+
childOf = pubsubRequestContext
66+
}
67+
}
1568

16-
// Create pubsub.delivery span
69+
// Extract topic name for better resource naming
70+
const topicName = topic ? topic.split('/').pop() : subscription.name.split('/').pop()
71+
// Create pubsub.receive span (note: operation name will be 'google-cloud-pubsub.receive')
72+
// Use a separate service name (like push subscriptions do) for better service map visibility
73+
const baseService = this.tracer._service || 'unknown'
74+
const serviceName = this.config.service || `${baseService}-pubsub`
75+
76+
// Build meta object with batch metadata if available
77+
const meta = {
78+
'gcloud.project_id': subscription.pubsub.projectId,
79+
'pubsub.topic': topic,
80+
'span.kind': 'consumer',
81+
'pubsub.delivery_method': 'pull',
82+
'pubsub.span_type': 'message_processing', // Easy filtering in Datadog
83+
'messaging.operation': 'receive' // Standard tag
84+
}
85+
86+
// Add batch metadata tags for correlation
87+
if (batchRequestTraceId) {
88+
meta['pubsub.batch.request_trace_id'] = batchRequestTraceId
89+
}
90+
if (batchRequestSpanId) {
91+
meta['pubsub.batch.request_span_id'] = batchRequestSpanId
92+
// Also add span link metadata
93+
meta['_dd.pubsub_request.trace_id'] = batchRequestTraceId
94+
meta['_dd.pubsub_request.span_id'] = batchRequestSpanId
95+
if (batchRequestTraceId && batchRequestSpanId) {
96+
meta['_dd.span_links'] = `${batchRequestTraceId}:${batchRequestSpanId}`
97+
}
98+
}
99+
100+
const metrics = {
101+
'pubsub.ack': 0
102+
}
103+
104+
// Add batch size and index if available
105+
if (batchSize) {
106+
metrics['pubsub.batch.message_count'] = Number.parseInt(batchSize, 10)
107+
metrics['pubsub.batch.size'] = Number.parseInt(batchSize, 10)
108+
}
109+
if (batchIndex !== undefined) {
110+
metrics['pubsub.batch.message_index'] = Number.parseInt(batchIndex, 10)
111+
metrics['pubsub.batch.index'] = Number.parseInt(batchIndex, 10)
112+
}
113+
114+
// Add batch description
115+
if (batchSize && batchIndex !== undefined) {
116+
const index = Number.parseInt(batchIndex, 10)
117+
const size = Number.parseInt(batchSize, 10)
118+
meta['pubsub.batch.description'] = `Message ${index + 1} of ${size}`
119+
}
120+
17121
const span = this.startSpan({
18122
childOf,
19-
resource: topic,
123+
resource: `Message from ${topicName}`, // More descriptive resource name
20124
type: 'worker',
21-
meta: {
22-
'gcloud.project_id': subscription.pubsub.projectId,
23-
'pubsub.topic': topic,
24-
'span.kind': 'consumer',
25-
operation: 'pubsub.delivery'
26-
},
27-
metrics: {
28-
'pubsub.ack': 0
29-
}
30-
}, ctx)
125+
service: serviceName, // Use integration-specific service name
126+
meta,
127+
metrics
128+
}, ctx)
31129

32130
// Add message metadata
33131
if (message.id) {
@@ -85,7 +183,6 @@ class GoogleCloudPubsubConsumerPlugin extends ConsumerPlugin {
85183
}
86184

87185
super.finish()
88-
89186
return ctx.parentStore
90187
}
91188
}

packages/datadog-plugin-google-cloud-pubsub/src/pubsub-push-subscription.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@ class GoogleCloudPubsubPushSubscriptionPlugin extends TracingPlugin {
2929
// NOTE: Only unwrapped headers will work. Standard wrapped format requires
3030
// body parsing which hasn't happened yet at this point in the request lifecycle.
3131
if (req.headers['x-goog-pubsub-message-id']) {
32-
log.debug('[PubSub] Detected unwrapped Pub/Sub format (push subscription)')
32+
log.warn('[PubSub] Detected unwrapped Pub/Sub format (push subscription)')
33+
log.warn(`[PubSub] message-id: ${req.headers['x-goog-pubsub-message-id']}`)
3334
this._createDeliverySpanAndActivate({ req, res })
35+
return
3436
}
37+
38+
// No unwrapped Pub/Sub headers found - likely missing --push-no-wrapper-write-metadata
39+
log.warn('[PubSub] No x-goog-pubsub-* headers detected. pubsub.delivery spans will not be created. Add --push-no-wrapper-write-metadata to your subscription.')
3540
}
3641

3742
_createDeliverySpanAndActivate ({ req, res }) {

0 commit comments

Comments
 (0)