dd-trace
Version:
Datadog APM tracing client for JavaScript
277 lines (244 loc) • 8.9 kB
JavaScript
const shimmer = require('../../datadog-shimmer')
const log = require('../../dd-trace/src/log')
const {
channel,
addHook,
} = require('./helpers/instrument')
const {
brokerSupportsMessageHeaders,
clientToCluster,
cloneMessages,
} = require('./helpers/kafka')
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 noop = () => {}
addHook({ name: 'kafkajs', file: 'src/producer/index.js', versions: ['>=1.4'] }, (createProducer) =>
shimmer.wrapFunction(createProducer, original => function wrappedCreateProducer (params) {
const producer = original(params)
if (params?.cluster) {
clientToCluster.set(producer, params.cluster)
}
return producer
})
)
addHook({ name: 'kafkajs', file: 'src/consumer/index.js', versions: ['>=1.4'] }, (createConsumer) =>
shimmer.wrapFunction(createConsumer, original => function wrappedCreateConsumer (params) {
const consumer = original(params)
if (params?.cluster) {
clientToCluster.set(consumer, params.cluster)
}
return consumer
})
)
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 originalSend = producer.send
const bootstrapServers = this._brokers
const cluster = clientToCluster.get(producer)
let disableHeaderInjection = false
let refreshHeaderSupport = () => {
if (!brokerSupportsMessageHeaders(cluster?.brokerPool)) {
disableHeaderInjection = true
refreshHeaderSupport = noop
log.info('kafkajs broker negotiated Produce <v3; tracer header injection disabled.')
}
}
producer.send = function (...args) {
if (!producerStartCh.hasSubscribers) {
return originalSend.apply(this, args)
}
// Fast path: kafkajs has fetched metadata, so versions and clusterId
// are already on the broker pool.
const metadata = cluster?.brokerPool?.metadata
if (metadata) {
refreshHeaderSupport()
return runSend.call(this, args, metadata.clusterId)
}
// Slow path, taken at most once per producer connect cycle. Prime the
// metadata fetch kafkajs's send would do internally a few stack frames
// later. `sharedPromiseTo` collapses our call and kafkajs's call into a
// single round trip, so total latency is unchanged.
if (typeof cluster?.refreshMetadataIfNecessary !== 'function') {
return runSend.call(this, args)
}
return cluster.refreshMetadataIfNecessary().then(
() => {
refreshHeaderSupport()
return runSend.call(this, args, cluster.brokerPool?.metadata?.clusterId)
},
() => runSend.call(this, args)
)
}
function runSend (args, clusterId) {
const arg0 = args[0]
const topic = arg0?.topic
const inputMessages = Array.isArray(arg0?.messages) ? arg0.messages : []
// Hand kafkajs and the plugin a shallow clone so injection writes to
// tracer-owned objects instead of the caller's. With injection
// disabled the clone must not seed `headers: {}` either: brokers that
// reject any header field cannot recover otherwise.
let messages = inputMessages
if (inputMessages.length > 0) {
messages = cloneMessages(inputMessages, !disableHeaderInjection)
args[0] = { ...arg0, messages }
}
const ctx = {
bootstrapServers,
clusterId,
disableHeaderInjection,
messages,
topic,
}
return producerStartCh.runStores(ctx, () => {
try {
const result = originalSend.apply(this, args)
result.then(
(res) => {
ctx.result = res
producerFinishCh.publish(ctx)
producerCommitCh.publish(ctx)
},
(error) => {
ctx.error = error
if (error) {
// Safety net for mixed-version clusters where the seed
// broker advertised Produce v3+ but the leader we shipped to
// could not parse the headers, surfacing as
// KafkaJSProtocolError UNKNOWN (server error code -1).
if (error.name === 'KafkaJSProtocolError' && error.type === 'UNKNOWN') {
disableHeaderInjection = true
refreshHeaderSupport = noop
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(error)
}
producerFinishCh.publish(ctx)
}
)
return result
} catch (error) {
ctx.error = error
producerErrorCh.publish(ctx)
producerFinishCh.publish(ctx)
throw error
}
})
}
return producer
})
shimmer.wrap(Kafka.prototype, 'consumer', createConsumer => function (...args) {
if (!consumerStartCh.hasSubscribers) {
return createConsumer.apply(this, args)
}
const consumer = createConsumer.apply(this, arguments)
const cluster = clientToCluster.get(consumer)
const groupId = arguments[0].groupId
const readClusterId = () => cluster?.brokerPool?.metadata?.clusterId
const eachMessageExtractor = (args) => {
const { topic, partition, message } = args[0]
return { topic, partition, message, groupId, clusterId: readClusterId() }
}
const eachBatchExtractor = (args) => {
const { batch } = args[0]
const { topic, partition, messages } = batch
return { topic, partition, messages, groupId, clusterId: readClusterId() }
}
consumer.on(consumer.events.COMMIT_OFFSETS, (event) => {
const { payload: { groupId: commitGroupId, topics } } = event
const clusterId = readClusterId()
const commitList = []
for (const { topic, partitions } of topics) {
for (const { partition, offset } of partitions) {
commitList.push({
groupId: commitGroupId,
partition,
offset,
topic,
clusterId,
})
}
}
consumerCommitCh.publish(commitList)
})
const run = consumer.run
consumer.run = function ({ eachMessage, eachBatch, ...runArgs }) {
return run({
eachMessage: wrappedCallback(
eachMessage,
consumerStartCh,
consumerFinishCh,
consumerErrorCh,
eachMessageExtractor
),
eachBatch: wrappedCallback(
eachBatch,
batchConsumerStartCh,
batchConsumerFinishCh,
batchConsumerErrorCh,
eachBatchExtractor
),
...runArgs,
})
}
return consumer
})
return Kafka
})
const wrappedCallback = (fn, startCh, finishCh, errorCh, extractArgs) => {
if (typeof fn !== 'function') return fn
return function (...args) {
const ctx = {
extractedArgs: extractArgs(args),
}
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)
},
(error) => {
ctx.error = error
if (error) {
errorCh.publish(ctx)
}
finishCh.publish(ctx)
}
)
} else {
finishCh.publish(ctx)
}
return result
} catch (error) {
ctx.error = error
errorCh.publish(ctx)
finishCh.publish(ctx)
throw error
}
})
}
}