UNPKG

@sentry/node

Version:

Sentry Node SDK using OpenTelemetry for performance instrumentation

439 lines (435 loc) 18.9 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const api = require('@opentelemetry/api'); const instrumentation = require('@opentelemetry/instrumentation'); const semanticConventions = require('@opentelemetry/semantic-conventions'); const internalTypes = require('./internal-types.js'); const propagator = require('./propagator.js'); const semconv = require('./semconv.js'); const core = require('@sentry/core'); const PACKAGE_NAME = "@sentry/instrumentation-kafkajs"; function prepareCounter(meter, value, attributes) { return (errorType) => { meter.add(value, { ...attributes, ...errorType ? { [semanticConventions.ATTR_ERROR_TYPE]: errorType } : {} }); }; } function prepareDurationHistogram(meter, value, attributes) { return (errorType) => { meter.record((Date.now() - value) / 1e3, { ...attributes, ...errorType ? { [semanticConventions.ATTR_ERROR_TYPE]: errorType } : {} }); }; } const HISTOGRAM_BUCKET_BOUNDARIES = [5e-3, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10]; class KafkaJsInstrumentation extends instrumentation.InstrumentationBase { constructor(config = {}) { super(PACKAGE_NAME, core.SDK_VERSION, config); } _updateMetricInstruments() { this._clientDuration = this.meter.createHistogram(semconv.METRIC_MESSAGING_CLIENT_OPERATION_DURATION, { advice: { explicitBucketBoundaries: HISTOGRAM_BUCKET_BOUNDARIES } }); this._sentMessages = this.meter.createCounter(semconv.METRIC_MESSAGING_CLIENT_SENT_MESSAGES); this._consumedMessages = this.meter.createCounter(semconv.METRIC_MESSAGING_CLIENT_CONSUMED_MESSAGES); this._processDuration = this.meter.createHistogram(semconv.METRIC_MESSAGING_PROCESS_DURATION, { advice: { explicitBucketBoundaries: HISTOGRAM_BUCKET_BOUNDARIES } }); } init() { const unpatch = (moduleExports) => { if (instrumentation.isWrapped(moduleExports?.Kafka?.prototype.producer)) { this._unwrap(moduleExports.Kafka.prototype, "producer"); } if (instrumentation.isWrapped(moduleExports?.Kafka?.prototype.consumer)) { this._unwrap(moduleExports.Kafka.prototype, "consumer"); } }; const module = new instrumentation.InstrumentationNodeModuleDefinition( "kafkajs", [">=0.3.0 <3"], (moduleExports) => { unpatch(moduleExports); this._wrap(moduleExports?.Kafka?.prototype, "producer", this._getProducerPatch()); this._wrap(moduleExports?.Kafka?.prototype, "consumer", this._getConsumerPatch()); return moduleExports; }, unpatch ); return module; } _getConsumerPatch() { const instrumentation$1 = this; return (original) => { return function consumer(...args) { const newConsumer = original.apply(this, args); if (instrumentation.isWrapped(newConsumer.run)) { instrumentation$1._unwrap(newConsumer, "run"); } instrumentation$1._wrap(newConsumer, "run", instrumentation$1._getConsumerRunPatch()); instrumentation$1._setKafkaEventListeners(newConsumer); return newConsumer; }; }; } _setKafkaEventListeners(kafkaObj) { if (kafkaObj[internalTypes.EVENT_LISTENERS_SET]) return; if (kafkaObj.events?.REQUEST) { kafkaObj.on(kafkaObj.events.REQUEST, this._recordClientDurationMetric.bind(this)); } kafkaObj[internalTypes.EVENT_LISTENERS_SET] = true; } _recordClientDurationMetric(event) { const [address = "", port = "0"] = event.payload.broker.split(":"); this._clientDuration.record(event.payload.duration / 1e3, { [semconv.ATTR_MESSAGING_SYSTEM]: semconv.MESSAGING_SYSTEM_VALUE_KAFKA, [semconv.ATTR_MESSAGING_OPERATION_NAME]: `${event.payload.apiName}`, [semanticConventions.ATTR_SERVER_ADDRESS]: address, [semanticConventions.ATTR_SERVER_PORT]: Number.parseInt(port, 10) }); } _getProducerPatch() { const instrumentation$1 = this; return (original) => { return function consumer(...args) { const newProducer = original.apply(this, args); if (instrumentation.isWrapped(newProducer.sendBatch)) { instrumentation$1._unwrap(newProducer, "sendBatch"); } instrumentation$1._wrap(newProducer, "sendBatch", instrumentation$1._getSendBatchPatch()); if (instrumentation.isWrapped(newProducer.send)) { instrumentation$1._unwrap(newProducer, "send"); } instrumentation$1._wrap(newProducer, "send", instrumentation$1._getSendPatch()); if (instrumentation.isWrapped(newProducer.transaction)) { instrumentation$1._unwrap(newProducer, "transaction"); } instrumentation$1._wrap(newProducer, "transaction", instrumentation$1._getProducerTransactionPatch()); instrumentation$1._setKafkaEventListeners(newProducer); return newProducer; }; }; } _getConsumerRunPatch() { const instrumentation$1 = this; return (original) => { return function run(...args) { const config = args[0]; if (config?.eachMessage) { if (instrumentation.isWrapped(config.eachMessage)) { instrumentation$1._unwrap(config, "eachMessage"); } instrumentation$1._wrap(config, "eachMessage", instrumentation$1._getConsumerEachMessagePatch()); } if (config?.eachBatch) { if (instrumentation.isWrapped(config.eachBatch)) { instrumentation$1._unwrap(config, "eachBatch"); } instrumentation$1._wrap(config, "eachBatch", instrumentation$1._getConsumerEachBatchPatch()); } return original.call(this, config); }; }; } _getConsumerEachMessagePatch() { const instrumentation = this; return (original) => { return function eachMessage(...args) { const payload = args[0]; const propagatedContext = api.propagation.extract( api.ROOT_CONTEXT, payload.message.headers, propagator.bufferTextMapGetter ); const span = instrumentation._startConsumerSpan({ topic: payload.topic, message: payload.message, operationType: semconv.MESSAGING_OPERATION_TYPE_VALUE_PROCESS, ctx: propagatedContext, attributes: { [semconv.ATTR_MESSAGING_DESTINATION_PARTITION_ID]: String(payload.partition) } }); const pendingMetrics = [ prepareDurationHistogram(instrumentation._processDuration, Date.now(), { [semconv.ATTR_MESSAGING_SYSTEM]: semconv.MESSAGING_SYSTEM_VALUE_KAFKA, [semconv.ATTR_MESSAGING_OPERATION_NAME]: "process", [semconv.ATTR_MESSAGING_DESTINATION_NAME]: payload.topic, [semconv.ATTR_MESSAGING_DESTINATION_PARTITION_ID]: String(payload.partition) }), prepareCounter(instrumentation._consumedMessages, 1, { [semconv.ATTR_MESSAGING_SYSTEM]: semconv.MESSAGING_SYSTEM_VALUE_KAFKA, [semconv.ATTR_MESSAGING_OPERATION_NAME]: "process", [semconv.ATTR_MESSAGING_DESTINATION_NAME]: payload.topic, [semconv.ATTR_MESSAGING_DESTINATION_PARTITION_ID]: String(payload.partition) }) ]; const eachMessagePromise = api.context.with(api.trace.setSpan(propagatedContext, span), () => { return original.apply(this, args); }); return instrumentation._endSpansOnPromise([span], pendingMetrics, eachMessagePromise); }; }; } _getConsumerEachBatchPatch() { return (original) => { const instrumentation = this; return function eachBatch(...args) { const payload = args[0]; const receivingSpan = instrumentation._startConsumerSpan({ topic: payload.batch.topic, message: void 0, operationType: semconv.MESSAGING_OPERATION_TYPE_VALUE_RECEIVE, ctx: api.ROOT_CONTEXT, attributes: { [semconv.ATTR_MESSAGING_BATCH_MESSAGE_COUNT]: payload.batch.messages.length, [semconv.ATTR_MESSAGING_DESTINATION_PARTITION_ID]: String(payload.batch.partition) } }); return api.context.with(api.trace.setSpan(api.context.active(), receivingSpan), () => { const startTime = Date.now(); const spans = []; const pendingMetrics = [ prepareCounter(instrumentation._consumedMessages, payload.batch.messages.length, { [semconv.ATTR_MESSAGING_SYSTEM]: semconv.MESSAGING_SYSTEM_VALUE_KAFKA, [semconv.ATTR_MESSAGING_OPERATION_NAME]: "process", [semconv.ATTR_MESSAGING_DESTINATION_NAME]: payload.batch.topic, [semconv.ATTR_MESSAGING_DESTINATION_PARTITION_ID]: String(payload.batch.partition) }) ]; payload.batch.messages.forEach((message) => { const propagatedContext = api.propagation.extract(api.ROOT_CONTEXT, message.headers, propagator.bufferTextMapGetter); const spanContext = api.trace.getSpan(propagatedContext)?.spanContext(); let origSpanLink; if (spanContext) { origSpanLink = { context: spanContext }; } spans.push( instrumentation._startConsumerSpan({ topic: payload.batch.topic, message, operationType: semconv.MESSAGING_OPERATION_TYPE_VALUE_PROCESS, link: origSpanLink, attributes: { [semconv.ATTR_MESSAGING_DESTINATION_PARTITION_ID]: String(payload.batch.partition) } }) ); pendingMetrics.push( prepareDurationHistogram(instrumentation._processDuration, startTime, { [semconv.ATTR_MESSAGING_SYSTEM]: semconv.MESSAGING_SYSTEM_VALUE_KAFKA, [semconv.ATTR_MESSAGING_OPERATION_NAME]: "process", [semconv.ATTR_MESSAGING_DESTINATION_NAME]: payload.batch.topic, [semconv.ATTR_MESSAGING_DESTINATION_PARTITION_ID]: String(payload.batch.partition) }) ); }); const batchMessagePromise = original.apply(this, args); spans.unshift(receivingSpan); return instrumentation._endSpansOnPromise(spans, pendingMetrics, batchMessagePromise); }); }; }; } _getProducerTransactionPatch() { const instrumentation = this; return (original) => { return function transaction(...args) { const transactionSpan = instrumentation.tracer.startSpan("transaction"); const transactionPromise = original.apply(this, args); transactionPromise.then((transaction2) => { const originalSend = transaction2.send; transaction2.send = function send(...args2) { return api.context.with(api.trace.setSpan(api.context.active(), transactionSpan), () => { const patched = instrumentation._getSendPatch()(originalSend); return patched.apply(this, args2).catch((err) => { transactionSpan.setStatus({ code: api.SpanStatusCode.ERROR, message: err?.message }); transactionSpan.recordException(err); throw err; }); }); }; const originalSendBatch = transaction2.sendBatch; transaction2.sendBatch = function sendBatch(...args2) { return api.context.with(api.trace.setSpan(api.context.active(), transactionSpan), () => { const patched = instrumentation._getSendBatchPatch()(originalSendBatch); return patched.apply(this, args2).catch((err) => { transactionSpan.setStatus({ code: api.SpanStatusCode.ERROR, message: err?.message }); transactionSpan.recordException(err); throw err; }); }); }; const originalCommit = transaction2.commit; transaction2.commit = function commit(...args2) { const originCommitPromise = originalCommit.apply(this, args2).then(() => { transactionSpan.setStatus({ code: api.SpanStatusCode.OK }); }); return instrumentation._endSpansOnPromise([transactionSpan], [], originCommitPromise); }; const originalAbort = transaction2.abort; transaction2.abort = function abort(...args2) { const originAbortPromise = originalAbort.apply(this, args2); return instrumentation._endSpansOnPromise([transactionSpan], [], originAbortPromise); }; }).catch((err) => { transactionSpan.setStatus({ code: api.SpanStatusCode.ERROR, message: err?.message }); transactionSpan.recordException(err); transactionSpan.end(); }); return transactionPromise; }; }; } _getSendBatchPatch() { const instrumentation = this; return (original) => { return function sendBatch(...args) { const batch = args[0]; const messages = batch.topicMessages || []; const spans = []; const pendingMetrics = []; messages.forEach((topicMessage) => { topicMessage.messages.forEach((message) => { spans.push(instrumentation._startProducerSpan(topicMessage.topic, message)); pendingMetrics.push( prepareCounter(instrumentation._sentMessages, 1, { [semconv.ATTR_MESSAGING_SYSTEM]: semconv.MESSAGING_SYSTEM_VALUE_KAFKA, [semconv.ATTR_MESSAGING_OPERATION_NAME]: "send", [semconv.ATTR_MESSAGING_DESTINATION_NAME]: topicMessage.topic, ...message.partition !== void 0 ? { [semconv.ATTR_MESSAGING_DESTINATION_PARTITION_ID]: String(message.partition) } : {} }) ); }); }); const origSendResult = original.apply(this, args); return instrumentation._endSpansOnPromise(spans, pendingMetrics, origSendResult); }; }; } _getSendPatch() { const instrumentation = this; return (original) => { return function send(...args) { const record = args[0]; const spans = record.messages.map((message) => { return instrumentation._startProducerSpan(record.topic, message); }); const pendingMetrics = record.messages.map( (m) => prepareCounter(instrumentation._sentMessages, 1, { [semconv.ATTR_MESSAGING_SYSTEM]: semconv.MESSAGING_SYSTEM_VALUE_KAFKA, [semconv.ATTR_MESSAGING_OPERATION_NAME]: "send", [semconv.ATTR_MESSAGING_DESTINATION_NAME]: record.topic, ...m.partition !== void 0 ? { [semconv.ATTR_MESSAGING_DESTINATION_PARTITION_ID]: String(m.partition) } : {} }) ); const origSendResult = original.apply(this, args); return instrumentation._endSpansOnPromise(spans, pendingMetrics, origSendResult); }; }; } _endSpansOnPromise(spans, pendingMetrics, sendPromise) { return Promise.resolve(sendPromise).then((result) => { pendingMetrics.forEach((m) => m()); return result; }).catch((reason) => { let errorMessage; let errorType = semanticConventions.ERROR_TYPE_VALUE_OTHER; if (typeof reason === "string" || reason === void 0) { errorMessage = reason; } else if (typeof reason === "object" && Object.prototype.hasOwnProperty.call(reason, "message")) { errorMessage = reason.message; errorType = reason.constructor.name; } pendingMetrics.forEach((m) => m(errorType)); spans.forEach((span) => { span.setAttribute(semanticConventions.ATTR_ERROR_TYPE, errorType); span.setStatus({ code: api.SpanStatusCode.ERROR, message: errorMessage }); }); throw reason; }).finally(() => { spans.forEach((span) => span.end()); }); } _startConsumerSpan({ topic, message, operationType, ctx, link, attributes }) { const operationName = operationType === semconv.MESSAGING_OPERATION_TYPE_VALUE_RECEIVE ? "poll" : operationType; const span = this.tracer.startSpan( `${operationName} ${topic}`, { kind: operationType === semconv.MESSAGING_OPERATION_TYPE_VALUE_RECEIVE ? api.SpanKind.CLIENT : api.SpanKind.CONSUMER, attributes: { ...attributes, [semconv.ATTR_MESSAGING_SYSTEM]: semconv.MESSAGING_SYSTEM_VALUE_KAFKA, [semconv.ATTR_MESSAGING_DESTINATION_NAME]: topic, [semconv.ATTR_MESSAGING_OPERATION_TYPE]: operationType, [semconv.ATTR_MESSAGING_OPERATION_NAME]: operationName, [semconv.ATTR_MESSAGING_KAFKA_MESSAGE_KEY]: message?.key ? String(message.key) : void 0, [semconv.ATTR_MESSAGING_KAFKA_MESSAGE_TOMBSTONE]: message?.key && message.value === null ? true : void 0, [semconv.ATTR_MESSAGING_KAFKA_OFFSET]: message?.offset }, links: link ? [link] : [] }, ctx ); const { consumerHook } = this.getConfig(); if (consumerHook && message) { instrumentation.safeExecuteInTheMiddle( () => consumerHook(span, { topic, message }), (e) => { if (e) this._diag.error("consumerHook error", e); }, true ); } return span; } _startProducerSpan(topic, message) { const span = this.tracer.startSpan(`send ${topic}`, { kind: api.SpanKind.PRODUCER, attributes: { [semconv.ATTR_MESSAGING_SYSTEM]: semconv.MESSAGING_SYSTEM_VALUE_KAFKA, [semconv.ATTR_MESSAGING_DESTINATION_NAME]: topic, [semconv.ATTR_MESSAGING_KAFKA_MESSAGE_KEY]: message.key ? String(message.key) : void 0, [semconv.ATTR_MESSAGING_KAFKA_MESSAGE_TOMBSTONE]: message.key && message.value === null ? true : void 0, [semconv.ATTR_MESSAGING_DESTINATION_PARTITION_ID]: message.partition !== void 0 ? String(message.partition) : void 0, [semconv.ATTR_MESSAGING_OPERATION_NAME]: "send", [semconv.ATTR_MESSAGING_OPERATION_TYPE]: semconv.MESSAGING_OPERATION_TYPE_VALUE_SEND } }); message.headers = message.headers ?? {}; api.propagation.inject(api.trace.setSpan(api.context.active(), span), message.headers); const { producerHook } = this.getConfig(); if (producerHook) { instrumentation.safeExecuteInTheMiddle( () => producerHook(span, { topic, message }), (e) => { if (e) this._diag.error("producerHook error", e); }, true ); } return span; } } exports.KafkaJsInstrumentation = KafkaJsInstrumentation; //# sourceMappingURL=instrumentation.js.map