dd-trace
Version:
Datadog APM tracing client for JavaScript
267 lines (232 loc) • 8.09 kB
JavaScript
const shimmer = require('../../datadog-shimmer')
const log = require('../../dd-trace/src/log')
const {
channel,
addHook,
} = require('./helpers/instrument')
const producerStartCh = channel('apm:kafkajs:produce:start')
const producerCommitCh = channel('apm:kafkajs:produce:commit')
const producerFinishCh = channel('apm:kafkajs:produce:finish')
const producerErrorCh = channel('apm:kafkajs:produce:error')
const consumerStartCh = channel('apm:kafkajs:consume:start')
const consumerCommitCh = channel('apm:kafkajs:consume:commit')
const consumerFinishCh = channel('apm:kafkajs:consume:finish')
const consumerErrorCh = channel('apm:kafkajs:consume:error')
const batchConsumerStartCh = channel('apm:kafkajs:consume-batch:start')
const batchConsumerFinishCh = channel('apm:kafkajs:consume-batch:finish')
const batchConsumerErrorCh = channel('apm:kafkajs:consume-batch:error')
const disabledHeaderWeakSet = new WeakSet()
addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKafka) => {
class Kafka extends BaseKafka {
constructor (options) {
super(options)
this._brokers = (options.brokers && typeof options.brokers !== 'function')
? options.brokers.join(',')
: undefined
}
}
shimmer.wrap(Kafka.prototype, 'producer', createProducer => function () {
const producer = createProducer.apply(this, arguments)
const send = producer.send
const bootstrapServers = this._brokers
const kafkaClusterIdPromise = getKafkaClusterId(this)
producer.send = function () {
const wrappedSend = (clusterId) => {
const { topic, messages = [] } = arguments[0]
const ctx = {
bootstrapServers,
clusterId,
disableHeaderInjection: disabledHeaderWeakSet.has(producer),
messages,
topic,
}
for (const message of messages) {
if (message !== null && typeof message === 'object' && !ctx.disableHeaderInjection) {
message.headers = message.headers || {}
}
}
return producerStartCh.runStores(ctx, () => {
try {
const result = send.apply(this, arguments)
result.then(
(res) => {
ctx.result = res
producerFinishCh.publish(ctx)
producerCommitCh.publish(ctx)
},
(err) => {
ctx.error = err
if (err) {
// Fixes bug where we would inject message headers for kafka brokers that don't support headers
// (version <0.11). On the error, we disable header injection.
// Unfortunately the error name / type is not more specific.
// This approach is implemented by other tracers as well.
if (err.name === 'KafkaJSProtocolError' && err.type === 'UNKNOWN') {
disabledHeaderWeakSet.add(producer)
log.error(
// eslint-disable-next-line @stylistic/max-len
'Kafka Broker responded with UNKNOWN_SERVER_ERROR (-1). Please look at broker logs for more information. Tracer message header injection for Kafka is disabled.'
)
}
producerErrorCh.publish(err)
}
producerFinishCh.publish(ctx)
})
return result
} catch (e) {
ctx.error = e
producerErrorCh.publish(ctx)
producerFinishCh.publish(ctx)
throw e
}
})
}
if (isPromise(kafkaClusterIdPromise)) {
// promise is not resolved
return kafkaClusterIdPromise.then((clusterId) => {
return wrappedSend(clusterId)
})
}
// promise is already resolved
return wrappedSend(kafkaClusterIdPromise)
}
return producer
})
shimmer.wrap(Kafka.prototype, 'consumer', createConsumer => function () {
if (!consumerStartCh.hasSubscribers) {
return createConsumer.apply(this, arguments)
}
const kafkaClusterIdPromise = getKafkaClusterId(this)
let resolvedClusterId = null
const eachMessageExtractor = (args, clusterId) => {
const { topic, partition, message } = args[0]
return { topic, partition, message, groupId, clusterId }
}
const eachBatchExtractor = (args, clusterId) => {
const { batch } = args[0]
const { topic, partition, messages } = batch
return { topic, partition, messages, groupId, clusterId }
}
const consumer = createConsumer.apply(this, arguments)
consumer.on(consumer.events.COMMIT_OFFSETS, (event) => {
const { payload: { groupId: commitGroupId, topics } } = event
const commitList = []
for (const { topic, partitions } of topics) {
for (const { partition, offset } of partitions) {
commitList.push({
groupId: commitGroupId,
partition,
offset,
topic,
clusterId: resolvedClusterId,
})
}
}
consumerCommitCh.publish(commitList)
})
const run = consumer.run
const groupId = arguments[0].groupId
consumer.run = function ({ eachMessage, eachBatch, ...runArgs }) {
const wrapConsume = (clusterId) => {
// In kafkajs COMMIT_OFFSETS always happens in the context of one synchronous run
// So this will always reference a correct cluster id
resolvedClusterId = clusterId
return run({
eachMessage: wrappedCallback(
eachMessage,
consumerStartCh,
consumerFinishCh,
consumerErrorCh,
eachMessageExtractor,
clusterId
),
eachBatch: wrappedCallback(
eachBatch,
batchConsumerStartCh,
batchConsumerFinishCh,
batchConsumerErrorCh,
eachBatchExtractor,
clusterId
),
...runArgs,
})
}
if (isPromise(kafkaClusterIdPromise)) {
// promise is not resolved
return kafkaClusterIdPromise.then((clusterId) => {
return wrapConsume(clusterId)
})
}
// promise is already resolved
return wrapConsume(kafkaClusterIdPromise)
}
return consumer
})
return Kafka
})
const wrappedCallback = (fn, startCh, finishCh, errorCh, extractArgs, clusterId) => {
return typeof fn === 'function'
? function (...args) {
const extractedArgs = extractArgs(args, clusterId)
const ctx = {
extractedArgs,
}
return startCh.runStores(ctx, () => {
try {
const result = fn.apply(this, args)
if (result && typeof result.then === 'function') {
result.then(
(res) => {
ctx.result = res
finishCh.publish(ctx)
},
(err) => {
ctx.error = err
if (err) {
errorCh.publish(ctx)
}
finishCh.publish(ctx)
})
} else {
finishCh.publish(ctx)
}
return result
} catch (e) {
ctx.error = e
errorCh.publish(ctx)
finishCh.publish(ctx)
throw e
}
})
}
: fn
}
const getKafkaClusterId = (kafka) => {
if (kafka._ddKafkaClusterId) {
return kafka._ddKafkaClusterId
}
if (!kafka.admin) {
return null
}
const admin = kafka.admin()
if (!admin.describeCluster) {
return null
}
return admin.connect()
.then(() => {
return admin.describeCluster()
})
.then((clusterInfo) => {
const clusterId = clusterInfo?.clusterId
kafka._ddKafkaClusterId = clusterId
admin.disconnect()
return clusterId
})
.catch((error) => {
throw error
})
}
function isPromise (obj) {
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'
}