UNPKG

@google-cloud/pubsub

Version:

Cloud Pub/Sub Client Library for Node.js

648 lines 23.9 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.legacyExports = exports.PubsubEvents = exports.PubsubSpans = exports.legacyAttributeName = 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"); // 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 !== null && cachedTracer !== void 0 ? 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"; /** * Legacy: We found a trace provider, but the user also specified the old * manual enable flag; this will trigger the legacy attribute being included. * The modern propagation attribute will _also_ be included. */ OpenTelemetryLevel[OpenTelemetryLevel["Legacy"] = 1] = "Legacy"; /** * Modern: We will only inject/extract the modern propagation attribute. */ OpenTelemetryLevel[OpenTelemetryLevel["Modern"] = 2] = "Modern"; })(OpenTelemetryLevel || (exports.OpenTelemetryLevel = OpenTelemetryLevel = {})); // 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. * * Legacy OTel is no longer officially supported, but we don't want to * break anyone at a non-major. * * @private * @internal */ function isEnabled(publishSettings) { // If we're not enabled, skip everything. if (!globallyEnabled) { return OpenTelemetryLevel.None; } if (publishSettings === null || publishSettings === void 0 ? void 0 : publishSettings.enableOpenTelemetryTracing) { return OpenTelemetryLevel.Legacy; } // Enable modern support. return OpenTelemetryLevel.Modern; } /** * Implements common members for the TextMap getter and setter interfaces for Pub/Sub messages. * * @private * @internal */ class PubsubMessageGetSet { 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; PubsubMessageGetSet.keyPrefix = 'googclient_'; /** * Implements the TextMap getter interface for Pub/Sub messages. * * @private * @internal */ class PubsubMessageGet extends PubsubMessageGetSet { get(carrier, key) { var _a; return (_a = carrier === null || carrier === void 0 ? void 0 : carrier.attributes) === null || _a === void 0 ? void 0 : _a[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'; /** * The old legacy attribute name. * * @private * @internal */ exports.legacyAttributeName = 'googclient_OpenTelemetrySpanContext'; /** * 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 === null || results === void 0 ? void 0 : 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 === null || results === void 0 ? void 0 : 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 === null || spanContext === void 0 ? void 0 : 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) { var _a, _b, _c, _d; const destinationName = (_a = params.topicName) !== null && _a !== void 0 ? _a : params.subName; const destinationId = (_b = params.topicId) !== null && _b !== void 0 ? _b : 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/opentelemetry-specification/blob/v1.1.0/specification/trace/semantic_conventions/messaging.md ['messaging.system']: 'gcp_pubsub', ['messaging.destination.name']: destinationId !== null && destinationId !== void 0 ? destinationId : destinationName, ['gcp.project_id']: projectId, ['code.function']: caller !== null && caller !== void 0 ? caller : 'unknown', }; if (message) { if (message.calculatedSize) { spanAttributes['messaging.message.envelope.size'] = message.calculatedSize; } else { if ((_c = message.data) === null || _c === void 0 ? void 0 : _c.length) { spanAttributes['messaging.message.envelope.size'] = (_d = message.data) === null || _d === void 0 ? void 0 : _d.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; } } 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), }); 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) { var _a; if (!globallyEnabled) { return undefined; } const subInfo = getSubscriptionInfo(subName); const name = `${(_a = subInfo.subId) !== null && _a !== void 0 ? _a : subName} subscribe`; const attributes = this.createAttributes(subInfo, message, caller); 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) { var _a; if (!globallyEnabled) { return undefined; } const parent = (_a = message === null || message === void 0 ? void 0 : message.parentSpan) !== null && _a !== void 0 ? _a : parentSpan; if (parent) { return getTracer().startSpan(name, { kind: api_1.SpanKind.INTERNAL, attributes: attributes !== null && attributes !== void 0 ? 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); 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 === null || span === void 0 ? void 0 : 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) { var _a; if (!globallyEnabled) { return undefined; } const subInfo = getSubscriptionInfo(subName); const spanAttributes = PubsubSpans.createAttributes(subInfo, undefined, caller); const links = messageSpans .filter(m => m && isSampled(m)) .map(m => ({ context: m.spanContext() })) .filter(l => l.context); const span = getTracer().startSpan(`${(_a = subInfo.subId) !== null && _a !== void 0 ? _a : subInfo.subName} ack`, { kind: api_1.SpanKind.CONSUMER, attributes: spanAttributes, links, }, api_1.ROOT_CONTEXT); span === null || span === void 0 ? void 0 : 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) { var _a; if (!globallyEnabled) { return undefined; } const subInfo = getSubscriptionInfo(subName); const spanAttributes = PubsubSpans.createAttributes(subInfo, undefined, caller); const links = messageSpans .filter(m => m && isSampled(m)) .map(m => ({ context: m.spanContext() })) .filter(l => l.context); const span = getTracer().startSpan(`${(_a = subInfo.subId) !== null && _a !== void 0 ? _a : subInfo.subName} ${type}`, { kind: api_1.SpanKind.CONSUMER, attributes: spanAttributes, links, }, api_1.ROOT_CONTEXT); span === null || span === void 0 ? void 0 : 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 === null || span === void 0 ? void 0 : span.setAttribute('messaging.gcp_pubsub.message.ack_deadline_seconds', deadline.totalOf('second')); } if (isInitial !== undefined) { span === null || span === void 0 ? void 0 : 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) { var _a; const subInfo = getSubscriptionInfo(subName); return PubsubSpans.createChildSpan(`${(_a = subInfo.subId) !== null && _a !== void 0 ? _a : 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, enabled) { 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]; } // If we're in legacy mode, add that header as well. if (enabled === OpenTelemetryLevel.Legacy) { if (message.attributes[exports.legacyAttributeName]) { console.warn(`${exports.legacyAttributeName} key set as message attribute, but will be overridden.`); } message.attributes[exports.legacyAttributeName] = JSON.stringify(span.spanContext()); } // Always do propagation injection with the trace context. const context = api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, span.spanContext()); api_1.propagation.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.legacyAttributeName || 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, enabled) { var _a, _b; if (!globallyEnabled) { return undefined; } if (message.parentSpan) { return message.parentSpan; } const keys = Object.getOwnPropertyNames((_a = message.attributes) !== null && _a !== void 0 ? _a : {}); let context; if (enabled === OpenTelemetryLevel.Legacy) { // Only prefer the legacy attributes to no trace context attribute. if (keys.includes(exports.legacyAttributeName) && !keys.includes(exports.modernAttributeName)) { const legacyValue = (_b = message.attributes) === null || _b === void 0 ? void 0 : _b[exports.legacyAttributeName]; if (legacyValue) { const parentSpanContext = legacyValue ? JSON.parse(legacyValue) : undefined; if (parentSpanContext) { context = spanContextToContext(parentSpanContext); } } } } else { if (keys.includes(exports.modernAttributeName)) { context = api_1.propagation.extract(api_1.ROOT_CONTEXT, message, exports.pubsubGetter); } } const span = PubsubSpans.createReceiveSpan(message, subName, context, 'extractSpan'); message.parentSpan = span; return span; } // Since these were exported on the main Pub/Sub index in the previous // version, we have to export them until the next major. exports.legacyExports = { /** * @deprecated * Use the new telemetry functionality instead; see the updated OpenTelemetry * sample for an example. */ createSpan: function (spanName, kind, attributes, parent) { if (!globallyEnabled) { // This isn't great, but it's the fact of the situation. return undefined; } else { return getTracer().startSpan(spanName, { kind, attributes, }, parent ? api_1.trace.setSpanContext(api_1.context.active(), parent) : undefined); } }, }; //# sourceMappingURL=telemetry-tracing.js.map