dd-trace
Version:
Datadog APM tracing client for JavaScript
156 lines (140 loc) • 5.35 kB
JavaScript
const ProducerPlugin = require('../../dd-trace/src/plugins/producer')
const { DsmPathwayCodec, getMessageSize } = require('../../dd-trace/src/datastreams')
const BOOTSTRAP_SERVERS_KEY = 'messaging.kafka.bootstrap.servers'
const MESSAGING_DESTINATION_KEY = 'messaging.destination.name'
class KafkajsProducerPlugin extends ProducerPlugin {
static id = 'kafkajs'
static operation = 'produce'
static peerServicePrecursors = [BOOTSTRAP_SERVERS_KEY]
constructor () {
super(...arguments)
this.addSub(`apm:${this.constructor.id}:produce:commit`, message => this.commit(message))
}
/**
* Transform individual commit details sent by kafkajs' event reporter
* into actionable backlog items for DSM
*
* @typedef {object} ProducerBacklog
* @property {number} type
* @property {string} topic
* @property {number} partition
* @property {number} offset
*/
/**
*
* @typedef {object} ProducerResponseItem
* @property {string} topic
* @property {number} partition
* @property {import('kafkajs/utils/long').Long} [offset]
* @property {import('kafkajs/utils/long').Long} [baseOffset]
*
* @param {ProducerResponseItem} response
* @returns {ProducerBacklog}
*/
transformProduceResponse (response, clusterId) {
// In produce protocol >=v3, the offset key changes from `offset` to `baseOffset`
const { topicName: topic, partition, offset, baseOffset } = response
const offsetAsLong = offset || baseOffset
const backlog = {
type: 'kafka_produce',
partition,
offset: offsetAsLong ? Number(offsetAsLong) : undefined,
topic,
}
if (clusterId) {
backlog.kafka_cluster_id = clusterId
}
return backlog
}
/**
*
* @param {{ result: ProducerResponseItem[] }} ctx
* @returns {void}
*/
commit (ctx) {
const commitList = ctx.result
const clusterId = ctx.clusterId
if (!this.config.dsmEnabled) return
if (!commitList || !Array.isArray(commitList)) return
for (const rawCommit of commitList) {
const commit = this.transformProduceResponse(rawCommit, clusterId)
this.tracer.setOffset(commit)
}
}
start (ctx) {
if (!this.config.dsmEnabled) return
const { topic, messages, clusterId, disableHeaderInjection, currentStore: { span } } = ctx
for (const message of messages) {
if (message !== null && typeof message === 'object') {
const payloadSize = getMessageSize(message)
const edgeTags = ['direction:out', `topic:${topic}`, 'type:kafka']
if (clusterId) {
edgeTags.push(`kafka_cluster_id:${clusterId}`)
}
const dataStreamsContext = this.tracer.setCheckpoint(edgeTags, span, payloadSize)
if (!disableHeaderInjection) {
DsmPathwayCodec.encode(dataStreamsContext, message.headers)
}
}
}
}
finish (ctx) {
const span = ctx?.currentStore?.span
const result = ctx?.result
if (span && Array.isArray(result) && result.length > 0) {
// The broker response is one entry per (topic, partition). Each entry
// carries a `baseOffset` — the offset assigned to the first record sent
// to that partition. We don't know per-partition record counts from the
// response, only the starting offset.
const offsets = []
for (const entry of result) {
const offsetAsLong = entry.offset ?? entry.baseOffset
if (entry.partition === undefined || offsetAsLong === undefined) continue
// Kafka offsets are 64-bit; coercing to Number loses precision past
// 2^53. Keep them as strings so the tag matches the exact offset on
// long-lived/high-throughput topics.
offsets.push({ partition: entry.partition, start_offset: String(offsetAsLong) })
}
if (offsets.length > 0) {
offsets.sort((a, b) => a.partition - b.partition)
span.setTag('kafka.messages.offsets', JSON.stringify(offsets))
}
// Single-message send: the one entry's partition/offset describes the
// exact record. Also expose them as flat tags for easy filtering.
if (offsets.length === 1 && ctx.messages?.length === 1) {
span.setTag('kafka.partition', offsets[0].partition)
// Set as a string meta tag (not a metric) to preserve full 64-bit precision.
span.setTag('kafka.message.offset', offsets[0].start_offset)
}
}
super.finish(ctx)
}
bindStart (ctx) {
const { topic, messages, bootstrapServers, clusterId, disableHeaderInjection } = ctx
const span = this.startSpan({
resource: topic,
meta: {
component: this.constructor.id,
'kafka.topic': topic,
'kafka.cluster_id': clusterId,
[MESSAGING_DESTINATION_KEY]: topic,
},
metrics: {
'kafka.batch_size': messages.length,
},
}, ctx)
if (bootstrapServers) {
span.setTag(BOOTSTRAP_SERVERS_KEY, bootstrapServers)
}
for (const message of messages) {
// message headers are not supported for kafka broker versions <0.11
if (message !== null && typeof message === 'object' && !disableHeaderInjection) {
message.headers ??= {}
this.tracer.inject(span, 'text_map', message.headers)
}
}
return ctx.currentStore
}
}
module.exports = KafkajsProducerPlugin