@instana/core
Version:
Core library for Instana's Node.js packages
392 lines (327 loc) • 11.8 kB
JavaScript
/*
* (c) Copyright IBM Corp. 2022
*/
;
const hook = require('../../../util/hook');
const tracingUtil = require('../../tracingUtil');
const { limitTraceId } = require('../../tracingHeaders');
const leftPad = require('../../leftPad');
const constants = require('../../constants');
const cls = require('../../cls');
const shimmer = require('../../shimmer');
const { getFunctionArguments } = require('../../../util/function_arguments');
let traceCorrelationEnabled = constants.kafkaTraceCorrelationDefault;
let isActive = false;
exports.init = function init(config) {
hook.onFileLoad(/\/node-rdkafka\/lib\/producer\.js/, instrumentProducer);
hook.onFileLoad(/\/node-rdkafka\/lib\/kafka-consumer-stream\.js/, instrumentConsumerAsStream);
hook.onModuleLoad('node-rdkafka', instrumentConsumer);
traceCorrelationEnabled = config.tracing.kafka.traceCorrelation;
};
exports.updateConfig = function updateConfig(config) {
traceCorrelationEnabled = config.tracing.kafka.traceCorrelation;
};
// The extraConfig is coming from the agent configs. You can set the kafka format in the agent.
exports.activate = function activate(extraConfig) {
if (extraConfig && extraConfig.tracing && extraConfig.tracing.kafka) {
if (extraConfig.tracing.kafka.traceCorrelation != null) {
traceCorrelationEnabled = extraConfig.tracing.kafka.traceCorrelation;
}
}
isActive = true;
};
exports.deactivate = function deactivate() {
isActive = false;
};
function instrumentProducer(ProducerClass) {
shimmer.wrap(ProducerClass.prototype, 'produce', shimProduce);
}
function instrumentConsumer(module) {
const className = 'KafkaConsumer';
const originalKafkaConsumer = module[className];
module[className] = function InstrumentedKafkaConsumer() {
const that = new originalKafkaConsumer(...arguments);
shimmer.wrap(that, 'emit', shimConsumerStreamEmit);
return that;
};
// We need to recover all static methods and attributes
// Currently, there is only one function: createReadStream
const keys = Object.keys(originalKafkaConsumer);
keys.forEach(key => {
module[className][key] = originalKafkaConsumer[key];
});
}
function instrumentConsumerAsStream(KafkaConsumerStream) {
shimmer.wrap(KafkaConsumerStream.prototype, 'emit', shimConsumerStreamEmit);
}
function shimProduce(originalProduce) {
return function () {
/**
* Arguments (ordered by their indexes):
* - topic: string,
* - partition: number,
* - message: Buffer,
* - key?: Kafka.MessageKey,
* - timestamp?: number,
* - opaque?: any,
* - headers?: Kafka.MessageHeader[]
*/
const originalArgs = getFunctionArguments(arguments);
return instrumentedProduce(this, originalProduce, originalArgs);
};
}
function shimConsumerStreamEmit(originalEmit) {
return function () {
/**
* Arguments (ordered by their indexes):
* - event name: string,
* - chunk: Object | any,
*/
const originalArgs = getFunctionArguments(arguments);
if (!isActive) {
return originalEmit.apply(this, originalArgs);
}
return instrumentedConsumerEmit(this, originalEmit, originalArgs);
};
}
function instrumentedProduce(ctx, originalProduce, originalArgs) {
const message = originalArgs[2];
if (!message) {
return originalProduce.apply(this, originalArgs);
}
const skipTracingResult = cls.skipExitTracing({ isActive, extendedResponse: true });
if (skipTracingResult.skip) {
if (skipTracingResult.suppressed) {
const headers = addTraceLevelSuppression(originalArgs[6]);
originalArgs[6] = headers;
}
return originalProduce.apply(ctx, originalArgs);
}
/**
* When `dr_cb` is set to true in the producer options during the creation step, we have the chance to get a message
* delivery callback, which makes our instrumentation more accurate in terms of message delivery success or failure.
* If this option is not set to true, the 'delivery-report' is not triggered.
* When this event is triggered, we get either an error or some pieces of the message back, which would indicate that
* the message was delivered successfully.
*/
const deliveryCb =
ctx._cb_configs &&
ctx._cb_configs.event &&
ctx._cb_configs.event.delivery_cb &&
typeof ctx._cb_configs.event.delivery_cb === 'function';
return cls.ns.runAndReturn(() => {
const topic = originalArgs[0];
const spanData = {
kafka: {
endpoints: topic,
operation: 'send'
}
};
const span = cls.startSpan({
spanName: 'kafka',
kind: constants.EXIT,
spanData
});
span.stack = tracingUtil.getStackTrace(instrumentedProduce, 1);
originalArgs[6] = setTraceHeaders({
headers: originalArgs[6],
span
});
if (deliveryCb) {
// TODO: Critical performance problems since 3.4.0
// https://github.com/Blizzard/node-rdkafka/issues/1128
// https://github.com/Blizzard/node-rdkafka/issues/1123#issuecomment-2855329479
ctx.once('delivery-report', function instanaDeliveryReportListener(err) {
span.d = Date.now() - span.ts;
if (err) {
span.ec = 1;
tracingUtil.setErrorDetails(span, err, 'kafka');
}
span.transmit();
});
}
try {
const result = originalProduce.apply(ctx, originalArgs);
if (!deliveryCb) {
span.d = Date.now() - span.ts;
span.transmit();
}
return result;
} catch (error) {
// e.g. cannot send message because format is byte
// "Message must be a buffer or null"
span.ec = 1;
tracingUtil.setErrorDetails(span, error, 'kafka');
if (!deliveryCb) {
span.d = Date.now() - span.ts;
span.transmit();
}
throw error;
}
});
}
function removeInstanaHeadersFromMessage(messageData) {
if (messageData.headers && messageData.headers.length) {
for (let i = messageData.headers.length - 1; i >= 0; i--) {
const headerObject = messageData.headers[i];
// There should be only one. That's how the API works. This can be tested, for instance, by sending a message
// via kafkajs to be recieved by rdkafka consumer. Each header will be a single object as an item in the
// messageData.headers array
const headerKey = Object.keys(headerObject)[0].toUpperCase();
if (constants.allInstanaKafkaHeaders.includes(headerKey)) {
messageData.headers.splice(i, 1);
}
}
}
}
function instrumentedConsumerEmit(ctx, originalEmit, originalArgs) {
let [event, eventData] = originalArgs;
/**
* If the stream batch is enabled, eventData will be an array. So in order to guarantee that array and non array cases
* are covered, we make sure that the data is always an array, and loop through them at all times.
*/
if (!Array.isArray(eventData)) {
eventData = [eventData];
}
if (!isActive) {
return originalEmit.apply(ctx, originalArgs);
}
const parentSpan = cls.getCurrentSpan();
if (parentSpan) {
return originalEmit.apply(ctx, originalArgs);
}
if (event !== 'data' && event !== 'error') {
return originalEmit.apply(ctx, originalArgs);
}
eventData.forEach(messageData => {
const instanaHeaders = (messageData.headers || []).filter(headerObject => {
const headerKey = Object.keys(headerObject)[0].toUpperCase();
return constants.allInstanaKafkaHeaders.includes(headerKey);
});
// flatten array to object
const instanaHeadersAsObject = {};
instanaHeaders.forEach(instanaHeader => {
const key = Object.keys(instanaHeader)[0].toUpperCase();
instanaHeadersAsObject[key] = instanaHeader[key];
});
let traceId;
let longTraceId;
let parentSpanId;
let level;
if (instanaHeaders.length) {
const {
level: _level,
traceId: _traceId,
longTraceId: _longTraceId,
parentSpanId: _parentSpanId
} = findInstanaHeaderValues(instanaHeadersAsObject);
traceId = _traceId;
longTraceId = _longTraceId;
parentSpanId = _parentSpanId;
level = _level;
removeInstanaHeadersFromMessage(messageData);
}
cls.ns.runAndReturn(function () {
if (level && level === '0') {
cls.setTracingLevel('0');
return originalEmit.apply(ctx, originalArgs);
}
const spanData = {
kafka: {
endpoints: messageData.topic || 'empty',
operation: 'consume'
}
};
const span = cls.startSpan({
spanName: 'kafka',
kind: constants.ENTRY,
traceId: traceId,
parentSpanId: parentSpanId,
spanData
});
if (longTraceId) {
span.lt = longTraceId;
}
span.stack = tracingUtil.getStackTrace(instrumentedConsumerEmit, 1);
// CASE: stream consumer receives error e.g. cannot connect to kafka
if (event === 'error') {
delete messageData.headers;
span.ec = 1;
tracingUtil.setErrorDetails(span, messageData, 'kafka');
}
setImmediate(() => {
span.d = Date.now() - span.ts;
span.transmit();
});
return originalEmit.apply(ctx, originalArgs);
});
});
}
function addTraceContextHeader(headers, span) {
if (!traceCorrelationEnabled) {
return headers;
}
if (headers == null) {
headers = [
// Maintenance note (128-bit-trace-ids): We can remove the left-pad call here once we have switched to 128 bit
// trace IDs. We already left-pad to the trace ID length (currently 16) in cls.js, when continuing the trace from
// an upstream tracer.
{ [constants.kafkaTraceIdHeaderName]: leftPad(span.t, 32) },
{ [constants.kafkaSpanIdHeaderName]: span.s },
{ [constants.kafkaTraceLevelHeaderName]: '1' }
];
} else if (headers && Array.isArray(headers)) {
// Maintenance note (128-bit-trace-ids): We can remove the left-pad call here once we have switched to 128 bit trace
// IDs, see above.
headers.push({ [constants.kafkaTraceIdHeaderName]: leftPad(span.t, 32) });
headers.push({ [constants.kafkaSpanIdHeaderName]: span.s });
headers.push({ [constants.kafkaTraceLevelHeaderName]: '1' });
}
return headers;
}
function addTraceLevelSuppression(headers) {
if (!traceCorrelationEnabled) {
return headers;
}
if (headers == null) {
headers = [
{
[constants.kafkaTraceLevelHeaderName]: '0'
}
];
} else if (headers && Array.isArray(headers)) {
headers.push({ [constants.kafkaTraceLevelHeaderName]: '0' });
}
return headers;
}
function findInstanaHeaderValues(instanaHeadersAsObject) {
let traceId;
let longTraceId;
let parentSpanId;
let level;
// Since v4, only 'string' format is supported.
if (instanaHeadersAsObject[constants.kafkaTraceIdHeaderName]) {
traceId = String(instanaHeadersAsObject[constants.kafkaTraceIdHeaderName]);
if (traceId) {
const limited = limitTraceId({ traceId });
traceId = limited.traceId;
longTraceId = limited.longTraceId;
}
}
if (instanaHeadersAsObject[constants.kafkaSpanIdHeaderName]) {
parentSpanId = String(instanaHeadersAsObject[constants.kafkaSpanIdHeaderName]);
}
if (instanaHeadersAsObject[constants.kafkaTraceLevelHeaderName]) {
level = String(instanaHeadersAsObject[constants.kafkaTraceLevelHeaderName]);
}
return { level, traceId, longTraceId, parentSpanId };
}
function setTraceHeaders({ headers, span }) {
if (span.shouldSuppressDownstream) {
// Suppress trace propagation to downstream services.
return addTraceLevelSuppression(headers);
} else {
// Otherwise, inject the trace context into the headers for propagation.
return addTraceContextHeader(headers, span);
}
}