UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

218 lines (180 loc) 7.45 kB
'use strict' const TracingPlugin = require('../../dd-trace/src/plugins/tracing') const SpanContext = require('../../dd-trace/src/opentracing/span_context') const id = require('../../dd-trace/src/id') const log = require('../../dd-trace/src/log') // WeakMap to track push receive spans by request const pushReceiveSpans = new WeakMap() class GoogleCloudPubsubPushSubscriptionPlugin extends TracingPlugin { static get id () { return 'google-cloud-pubsub-push-subscription' } constructor (...args) { super(...args) /** * PUSH SUBSCRIPTION: GCP sends HTTP POST requests to our service with message data in headers. * We intercept these requests to create a pubsub.push.receive span that wraps the HTTP request. * * Flow: Detect push request -> Extract trace context -> Create receive span -> Activate it * Hierarchy: pubsub.push.receive (parent) -> http.request (child) -> express.middleware... * * Plugin load order (http/index.js) ensures we subscribe before HttpServerPlugin. */ this.addSub('apm:http:server:request:start', (ctx) => { this.#handlePubSubRequest(ctx) }) this.addSub('apm:http:server:request:finish', ({ req }) => { this.#finishPushReceiveSpan(req) }) } #finishPushReceiveSpan (req) { const pushReceiveSpan = pushReceiveSpans.get(req) if (pushReceiveSpan && !pushReceiveSpan._duration) { pushReceiveSpan.finish() pushReceiveSpans.delete(req) } } #handlePubSubRequest (ctx) { const { req } = ctx const userAgent = req.headers['user-agent'] || '' if (req.method !== 'POST' || !userAgent.includes('APIs-Google')) { return false } if (req.headers['x-goog-pubsub-message-id']) { this.#createPushReceiveSpanAndActivate(ctx) return true } log.warn( '[PubSub] No x-goog-pubsub-* headers detected. pubsub.push.receive spans will not be created. ' + 'Add --push-no-wrapper-write-metadata to your subscription.' ) return false } #createPushReceiveSpanAndActivate (ctx) { const { req, res } = ctx const messageData = this.#parseMessage(req) if (!messageData) { return } const originalContext = this.#extractContext(messageData) const pubsubRequestContext = this.#reconstructPubSubContext(messageData.attrs) || originalContext const isSameTrace = pubsubRequestContext && originalContext?.toTraceId() === pubsubRequestContext.toTraceId() /** * Create receive span, choosing parent context: * - Same trace: use batch context (message is part of the batch trace) * - Different trace: use message context as parent, link to batch for observability * * this.enter() activates the span so the HTTP request span becomes its child. */ const pushReceiveSpan = this.#createPushReceiveSpan( messageData, isSameTrace ? pubsubRequestContext : originalContext, isSameTrace ? null : pubsubRequestContext ) if (!pushReceiveSpan) { return } this.enter(pushReceiveSpan, { req, res }) pushReceiveSpans.set(req, pushReceiveSpan) } #parseMessage (req) { const subscription = req.headers['x-goog-pubsub-subscription-name'] const message = { messageId: req.headers['x-goog-pubsub-message-id'], publishTime: req.headers['x-goog-pubsub-publish-time'] } const topicName = req.headers['pubsub.topic'] || 'push-subscription-topic' return { message, subscription, attrs: req.headers, topicName } } #extractContext (messageData) { return this.tracer.extract('text_map', messageData.attrs) } #reconstructPubSubContext (attrs) { /** * Reconstruct the batch publish span context from message attributes. * * When a batch is published, the producer injects: * - _dd.pubsub_request.trace_id: lower 64 bits of the batch span's trace ID (hex) * - _dd.pubsub_request.span_id: the batch span's span ID (hex) * - _dd.pubsub_request.p.tid: upper 64 bits of trace ID (hex, optional for 128-bit traces) * * This context represents the "pubsub.request" span on the producer side. * We use it to create span links, connecting each pubsub.push.receive span back to the original batch. */ const traceIdLower = attrs['_dd.pubsub_request.trace_id'] const spanId = attrs['_dd.pubsub_request.span_id'] const traceIdUpper = attrs['_dd.pubsub_request.p.tid'] if (!traceIdLower || !spanId) return null // Reconstruct full 128-bit trace ID (or pad 64-bit to 128-bit) const traceId128 = traceIdUpper ? traceIdUpper + traceIdLower : traceIdLower.padStart(32, '0') const traceId = id(traceId128, 16) const parentId = id(spanId, 16) const tags = {} if (traceIdUpper) tags['_dd.p.tid'] = traceIdUpper return new SpanContext({ traceId, spanId: parentId, tags }) } #createPushReceiveSpan (messageData, parentContext, linkContext) { const { message, subscription, topicName, attrs } = messageData const subscriptionName = subscription?.slice(subscription.lastIndexOf('/') + 1) ?? subscription const publishStartTime = attrs['x-dd-publish-start-time'] const startTime = publishStartTime ? Number.parseInt(publishStartTime, 10) : undefined // Get the base service name and construct the pubsub service override const baseService = this.tracer._service const serviceOverride = this.config.service ?? `${baseService}-pubsub` // Use this.startSpan() which automatically activates the span const span = this.startSpan('pubsub.push.receive', { childOf: parentContext, startTime, kind: 'consumer', service: serviceOverride, meta: { component: 'google-cloud-pubsub', 'pubsub.method': 'receive', 'pubsub.subscription': subscription, 'pubsub.message_id': message.messageId, 'pubsub.subscription_type': 'push', 'pubsub.topic': topicName, '_dd.base_service': baseService, '_dd.serviceoverride.type': 'integration', 'resource.name': `Push Subscription ${subscriptionName}` } }) if (!span) { return null } span._integrationName = 'google-cloud-pubsub' // Calculate delivery latency (queue time from publish to delivery) if (publishStartTime) { const deliveryDuration = Date.now() - Number(publishStartTime) span.setTag('pubsub.delivery_duration_ms', deliveryDuration) } this.#addBatchMetadata(span, attrs) if (linkContext) { if (span.addLink) { span.addLink(linkContext, {}) } else { span._links ??= [] span._links.push({ context: linkContext, attributes: {} }) } } return span } #addBatchMetadata (span, attrs) { const batchSizeStr = attrs['_dd.batch.size'] const batchIndexStr = attrs['_dd.batch.index'] if (!batchSizeStr || batchIndexStr === undefined) return const size = Number(batchSizeStr) const index = Number(batchIndexStr) span.setTag('pubsub.batch.message_count', size) span.setTag('pubsub.batch.message_index', index) span.setTag('pubsub.batch.description', `Message ${index + 1} of ${size}`) const requestTraceId = attrs['_dd.pubsub_request.trace_id'] if (requestTraceId) { span.setTag('pubsub.batch.request_trace_id', requestTraceId) } } } module.exports = GoogleCloudPubsubPushSubscriptionPlugin