UNPKG

@google-cloud/pubsub

Version:

Cloud Pub/Sub Client Library for Node.js

583 lines 20.2 kB
"use strict"; /*! * Copyright 2020-2024 Google LLC * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.PubsubEvents = exports.PubsubSpans = exports.modernAttributeName = exports.pubsubSetter = exports.pubsubGetter = exports.PubsubMessageSet = exports.PubsubMessageGet = exports.PubsubMessageGetSet = exports.OpenTelemetryLevel = void 0; exports.setGloballyEnabled = setGloballyEnabled; exports.isEnabled = isEnabled; exports.spanContextToContext = spanContextToContext; exports.getSubscriptionInfo = getSubscriptionInfo; exports.getTopicInfo = getTopicInfo; exports.injectSpan = injectSpan; exports.containsSpanContext = containsSpanContext; exports.extractSpan = extractSpan; const api_1 = require("@opentelemetry/api"); const core_1 = require("@opentelemetry/core"); // We need this to get the library version. // eslint-disable-next-line @typescript-eslint/no-var-requires const packageJson = require('../../package.json'); /** * Instantiates a Opentelemetry tracer for the library * * @private * @internal */ let cachedTracer; function getTracer() { const tracer = cachedTracer ?? api_1.trace.getTracer('@google-cloud/pubsub', packageJson.version); cachedTracer = tracer; return cachedTracer; } /** * Determination of the level of OTel support we're providing. * * @private * @internal */ var OpenTelemetryLevel; (function (OpenTelemetryLevel) { /** * None: OTel support is not enabled because we found no trace provider, or * the user has not enabled it. */ OpenTelemetryLevel[OpenTelemetryLevel["None"] = 0] = "None"; /** * Modern: We will only inject/extract the modern propagation attribute. */ OpenTelemetryLevel[OpenTelemetryLevel["Modern"] = 2] = "Modern"; })(OpenTelemetryLevel || (exports.OpenTelemetryLevel = OpenTelemetryLevel = {})); /** * The W3C trace context propagator, used for injecting/extracting trace context. * * @private * @internal */ const w3cTraceContextPropagator = new core_1.W3CTraceContextPropagator(); // True if user code elsewhere wants to enable OpenTelemetry support. let globallyEnabled = false; /** * Manually set the OpenTelemetry enabledness. * * @param enabled The enabled flag to use, to override any automated methods. * @private * @internal */ function setGloballyEnabled(enabled) { globallyEnabled = enabled; } /** * Tries to divine what sort of OpenTelemetry we're supporting. See the enum * for the meaning of the values, and other notes. * * @private * @internal */ function isEnabled() { return globallyEnabled ? OpenTelemetryLevel.Modern : OpenTelemetryLevel.None; } /** * Implements common members for the TextMap getter and setter interfaces for Pub/Sub messages. * * @private * @internal */ class PubsubMessageGetSet { static keyPrefix = 'googclient_'; keys(carrier) { return Object.getOwnPropertyNames(carrier.attributes) .filter(n => n.startsWith(PubsubMessageGetSet.keyPrefix)) .map(n => n.substring(PubsubMessageGetSet.keyPrefix.length)); } attributeName(key) { return `${PubsubMessageGetSet.keyPrefix}${key}`; } } exports.PubsubMessageGetSet = PubsubMessageGetSet; /** * Implements the TextMap getter interface for Pub/Sub messages. * * @private * @internal */ class PubsubMessageGet extends PubsubMessageGetSet { get(carrier, key) { return carrier?.attributes?.[this.attributeName(key)]; } } exports.PubsubMessageGet = PubsubMessageGet; /** * Implements the TextMap setter interface for Pub/Sub messages. * * @private * @internal */ class PubsubMessageSet extends PubsubMessageGetSet { set(carrier, key, value) { if (!carrier.attributes) { carrier.attributes = {}; } carrier.attributes[this.attributeName(key)] = value; } } exports.PubsubMessageSet = PubsubMessageSet; /** * The getter to use when calling extract() on a Pub/Sub message. * * @private * @internal */ exports.pubsubGetter = new PubsubMessageGet(); /** * The setter to use when calling inject() on a Pub/Sub message. * * @private * @internal */ exports.pubsubSetter = new PubsubMessageSet(); /** * Converts a SpanContext to a full Context, as needed. * * @private * @internal */ function spanContextToContext(parent) { return parent ? api_1.trace.setSpanContext(api_1.context.active(), parent) : undefined; } /** * The modern propagation attribute name. * * Technically this is determined by the OpenTelemetry library, but * in practice, it follows the W3C spec, so this should be the right * one. The only thing we're using it for, anyway, is emptying user * supplied attributes. * * @private * @internal */ exports.modernAttributeName = 'googclient_traceparent'; /** * Break down the subscription's full name into its project and ID. * * @private * @internal */ function getSubscriptionInfo(fullName) { const results = fullName.match(/projects\/([^/]+)\/subscriptions\/(.+)/); if (!results?.[0]) { return { subName: fullName, }; } return { subName: fullName, projectId: results[1], subId: results[2], }; } /** * Break down the subscription's full name into its project and ID. * * @private * @internal */ function getTopicInfo(fullName) { const results = fullName.match(/projects\/([^/]+)\/topics\/(.+)/); if (!results?.[0]) { return { topicName: fullName, }; } return { topicName: fullName, projectId: results[1], topicId: results[2], }; } // Determines if a trace is to be sampled. There doesn't appear to be a sanctioned // way to do this currently (isRecording does something different). // // Based on this: https://github.com/open-telemetry/opentelemetry-js/issues/4193 function isSampled(span) { const FLAG_MASK_SAMPLED = 0x1; const spanContext = span.spanContext(); const traceFlags = spanContext?.traceFlags; const sampled = !!(traceFlags && (traceFlags & FLAG_MASK_SAMPLED) === FLAG_MASK_SAMPLED); return sampled; } /** * Contains utility methods for creating spans. * * @private * @internal */ class PubsubSpans { static createAttributes(params, message, caller, operation) { const destinationName = params.topicName ?? params.subName; const destinationId = params.topicId ?? params.subId; const projectId = params.projectId; // Purposefully leaving this debug check here as a comment - this should // always be true, but we don't want to fail in prod if it's not. /*if ( (params.topicName && params.subName) || (!destinationName && !projectId && !destinationId) ) { throw new Error( 'One of topicName or subName must be specified, and must be fully qualified' ); }*/ const spanAttributes = { // Add Opentelemetry semantic convention attributes to the span, based on: // https://github.com/open-telemetry/semantic-conventions/blob/v1.24.0/docs/messaging/messaging-spans.md ['messaging.system']: 'gcp_pubsub', ['messaging.destination.name']: destinationId ?? destinationName, ['gcp.project_id']: projectId, ['code.function']: caller ?? 'unknown', }; if (message) { if (message.calculatedSize) { spanAttributes['messaging.message.envelope.size'] = message.calculatedSize; } else { if (message.data?.length) { spanAttributes['messaging.message.envelope.size'] = message.data?.length; } } if (message.orderingKey) { spanAttributes['messaging.gcp_pubsub.message.ordering_key'] = message.orderingKey; } if (message.isExactlyOnceDelivery) { spanAttributes['messaging.gcp_pubsub.message.exactly_once_delivery'] = message.isExactlyOnceDelivery; } if (message.ackId) { spanAttributes['messaging.gcp_pubsub.message.ack_id'] = message.ackId; } if (operation) { spanAttributes['messaging.operation'] = operation; } } return spanAttributes; } static createPublisherSpan(message, topicName, caller) { if (!globallyEnabled) { return undefined; } const topicInfo = getTopicInfo(topicName); const span = getTracer().startSpan(`${topicName} create`, { kind: api_1.SpanKind.PRODUCER, attributes: PubsubSpans.createAttributes(topicInfo, message, caller, 'create'), }); if (topicInfo.topicId) { span.updateName(`${topicInfo.topicId} create`); span.setAttribute('messaging.destination.name', topicInfo.topicId); } return span; } static updatePublisherTopicName(span, topicName) { const topicInfo = getTopicInfo(topicName); if (topicInfo.topicId) { span.updateName(`${topicInfo.topicId} create`); span.setAttribute('messaging.destination.name', topicInfo.topicId); } else { span.updateName(`${topicName} create`); } if (topicInfo.projectId) { span.setAttribute('gcp.project_id', topicInfo.projectId); } } static createReceiveSpan(message, subName, parent, caller) { if (!globallyEnabled) { return undefined; } const subInfo = getSubscriptionInfo(subName); const name = `${subInfo.subId ?? subName} subscribe`; const attributes = this.createAttributes(subInfo, message, caller, 'receive'); if (subInfo.subId) { attributes['messaging.destination.name'] = subInfo.subId; } if (api_1.context) { return getTracer().startSpan(name, { kind: api_1.SpanKind.CONSUMER, attributes, }, parent); } else { return getTracer().startSpan(name, { kind: api_1.SpanKind.CONSUMER, attributes, }); } } static createChildSpan(name, message, parentSpan, attributes) { if (!globallyEnabled) { return undefined; } const parent = message?.parentSpan ?? parentSpan; if (parent) { return getTracer().startSpan(name, { kind: api_1.SpanKind.INTERNAL, attributes: attributes ?? {}, }, spanContextToContext(parent.spanContext())); } else { return undefined; } } static createPublishFlowSpan(message) { return PubsubSpans.createChildSpan('publisher flow control', message); } static createPublishSchedulerSpan(message) { return PubsubSpans.createChildSpan('publisher batching', message); } static createPublishRpcSpan(messages, topicName, caller) { if (!globallyEnabled) { return undefined; } const spanAttributes = PubsubSpans.createAttributes(getTopicInfo(topicName), undefined, caller, 'create'); const links = messages .filter(m => m.parentSpan && isSampled(m.parentSpan)) .map(m => ({ context: m.parentSpan.spanContext() })) .filter(l => l.context); const span = getTracer().startSpan(`${topicName} send`, { kind: api_1.SpanKind.PRODUCER, attributes: spanAttributes, links, }, api_1.ROOT_CONTEXT); span?.setAttribute('messaging.batch.message_count', messages.length); if (span) { // Also attempt to link from message spans back to the publish RPC span. messages.forEach(m => { if (m.parentSpan && isSampled(m.parentSpan)) { m.parentSpan.addLink({ context: span.spanContext() }); } }); } return span; } static createAckRpcSpan(messageSpans, subName, caller) { if (!globallyEnabled) { return undefined; } const subInfo = getSubscriptionInfo(subName); const spanAttributes = PubsubSpans.createAttributes(subInfo, undefined, caller, 'receive'); const links = messageSpans .filter(m => m && isSampled(m)) .map(m => ({ context: m.spanContext() })) .filter(l => l.context); const span = getTracer().startSpan(`${subInfo.subId ?? subInfo.subName} ack`, { kind: api_1.SpanKind.CONSUMER, attributes: spanAttributes, links, }, api_1.ROOT_CONTEXT); span?.setAttribute('messaging.batch.message_count', messageSpans.length); if (span) { // Also attempt to link from the subscribe span(s) back to the publish RPC span. messageSpans.forEach(m => { if (m && isSampled(m)) { m.addLink({ context: span.spanContext() }); } }); } return span; } static createModackRpcSpan(messageSpans, subName, type, caller, deadline, isInitial) { if (!globallyEnabled) { return undefined; } const subInfo = getSubscriptionInfo(subName); const spanAttributes = PubsubSpans.createAttributes(subInfo, undefined, caller, 'receive'); const links = messageSpans .filter(m => m && isSampled(m)) .map(m => ({ context: m.spanContext() })) .filter(l => l.context); const span = getTracer().startSpan(`${subInfo.subId ?? subInfo.subName} ${type}`, { kind: api_1.SpanKind.CONSUMER, attributes: spanAttributes, links, }, api_1.ROOT_CONTEXT); span?.setAttribute('messaging.batch.message_count', messageSpans.length); if (span) { // Also attempt to link from the subscribe span(s) back to the publish RPC span. messageSpans.forEach(m => { if (m && isSampled(m)) { m.addLink({ context: span.spanContext() }); } }); } if (deadline) { span?.setAttribute('messaging.gcp_pubsub.message.ack_deadline_seconds', deadline.totalOf('second')); } if (isInitial !== undefined) { span?.setAttribute('messaging.gcp_pubsub.is_receipt_modack', isInitial); } return span; } static createReceiveFlowSpan(message) { return PubsubSpans.createChildSpan('subscriber concurrency control', message); } static createReceiveSchedulerSpan(message) { return PubsubSpans.createChildSpan('subscriber scheduler', message); } static createReceiveProcessSpan(message, subName) { const subInfo = getSubscriptionInfo(subName); return PubsubSpans.createChildSpan(`${subInfo.subId ?? subName} process`, message); } static setReceiveProcessResult(span, isAck) { span?.setAttribute('messaging.gcp_pubsub.result', isAck ? 'ack' : 'nack'); } } exports.PubsubSpans = PubsubSpans; /** * Creates and manipulates Pub/Sub-related events on spans. * * @private * @internal */ class PubsubEvents { static addEvent(text, message, attributes) { const parent = message.parentSpan; if (!parent) { return; } parent.addEvent(text, attributes); } static publishStart(message) { PubsubEvents.addEvent('publish start', message); } static publishEnd(message) { PubsubEvents.addEvent('publish end', message); } static ackStart(message) { PubsubEvents.addEvent('ack start', message); } static ackEnd(message) { PubsubEvents.addEvent('ack end', message); } static modackStart(message) { PubsubEvents.addEvent('modack start', message); } static modackEnd(message) { PubsubEvents.addEvent('modack end', message); } static nackStart(message) { PubsubEvents.addEvent('nack start', message); } static nackEnd(message) { PubsubEvents.addEvent('nack end', message); } static ackCalled(span) { span.addEvent('ack called'); } static nackCalled(span) { span.addEvent('nack called'); } static modAckCalled(span, deadline) { // User-called modAcks are never initial ones. span.addEvent('modack called', { 'messaging.gcp_pubsub.modack_deadline_seconds': `${deadline.totalOf('second')}`, 'messaging.gcp_pubsub.is_receipt_modack': 'false', }); } static modAckStart(message, deadline, isInitial) { PubsubEvents.addEvent('modack start', message, { 'messaging.gcp_pubsub.modack_deadline_seconds': `${deadline.totalOf('second')}`, 'messaging.gcp_pubsub.is_receipt_modack': isInitial ? 'true' : 'false', }); } static modAckEnd(message) { PubsubEvents.addEvent('modack end', message); } // Add this event any time the process is shut down before processing // of the message can complete. static shutdown(message) { PubsubEvents.addEvent('shutdown', message); } } exports.PubsubEvents = PubsubEvents; /** * Injects the trace context into a Pub/Sub message (or other object with * an 'attributes' object) for propagation. * * This is for the publish side. * * @private * @internal */ function injectSpan(span, message) { if (!globallyEnabled) { return; } if (!message.attributes) { message.attributes = {}; } if (message.attributes[exports.modernAttributeName]) { console.warn(`${exports.modernAttributeName} key set as message attribute, but will be overridden.`); delete message.attributes[exports.modernAttributeName]; } // Always do propagation injection with the trace context. const context = api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, span.spanContext()); w3cTraceContextPropagator.inject(context, message, exports.pubsubSetter); // Also put the direct reference to the Span object for while we're // passing it around in the client library. message.parentSpan = span; } /** * Returns true if this message potentially contains a span context. * * @private * @internal */ function containsSpanContext(message) { if (message.parentSpan) { return true; } if (!message.attributes) { return false; } const keys = Object.getOwnPropertyNames(message.attributes); return !!keys.find(n => n === exports.modernAttributeName); } /** * Extracts the trace context from a Pub/Sub message (or other object with * an 'attributes' object) from a propagation, for receive processing. If no * context was present, create a new parent span. * * This is for the receive side. * * @private * @internal */ function extractSpan(message, subName) { if (!globallyEnabled) { return undefined; } if (message.parentSpan) { return message.parentSpan; } const keys = Object.getOwnPropertyNames(message.attributes ?? {}); let context; if (keys.includes(exports.modernAttributeName)) { context = w3cTraceContextPropagator.extract(api_1.ROOT_CONTEXT, message, exports.pubsubGetter); } const span = PubsubSpans.createReceiveSpan(message, subName, context, 'extractSpan'); message.parentSpan = span; return span; } //# sourceMappingURL=telemetry-tracing.js.map