fix(kafkajs): skip header injection on brokers below Produce v3#8390
Conversation
Overall package sizeSelf size: 5.88 MB Dependency sizes| name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.0.1 | 82.56 kB | 817.39 kB | | dc-polyfill | 0.1.11 | 25.74 kB | 25.74 kB |🤖 This report was automatically generated by heaviest-objects-in-the-universe |
🎉 All green!❄️ No new flaky tests detected 🎯 Code Coverage (details) 🔗 Commit SHA: 5a1d7ce | Docs | Datadog PR Page | Give us feedback! |
BenchmarksBenchmark execution time: 2026-05-18 19:20:59 Comparing candidate commit 5a1d7ce in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 1491 metrics, 102 unstable metrics. |
a1ae644 to
0d852ca
Compare
The Produce request didn't carry headers until v3 (Kafka 0.11), so injecting on older brokers either wastes work or trips `UNKNOWN_SERVER_ERROR` on a per-leader version mismatch in mixed-version clusters. Read `cluster.brokerPool.versions[0].maxVersion` once on first send and disable injection proactively when the broker negotiated <v3; the existing `KafkaJSProtocolError UNKNOWN` handler stays as the safety net for mixed-version clusters where the seed broker advertised v3+ but the leader couldn't parse the headers. `refreshHeaderSupport` is a `let` that swaps to module-level `noop` at both disable sites, so subsequent sends skip the helper call entirely. The reactive `disableHeaderInjection` moves from the module-level `disabledHeaderWeakSet` to a per-producer closure so the lifetime matches the producer it describes. Refs: #8253
681b6c4 to
9792e32
Compare
| // be <0.11; lying locally is enough to drive the proactive | ||
| // header-support check. | ||
| const cluster = clientToCluster.get(producer) | ||
| cluster.brokerPool.versions[0].maxVersion = 2 |
There was a problem hiding this comment.
Shouldn't we actually spin up a Docker container with an old version of Kafka to verify it works as expected instead of just mocking this value?
There was a problem hiding this comment.
I think the mock is better, since such old Kafka brokers are from 2017 :D
So having the mock here seems the best approach, no?
The Produce request didn't carry headers until v3 (Kafka 0.11), so injecting on older brokers either wastes work or trips `UNKNOWN_SERVER_ERROR` on a per-leader version mismatch in mixed-version clusters. Read `cluster.brokerPool.versions[0].maxVersion` once on first send and disable injection proactively when the broker negotiated <v3; the existing `KafkaJSProtocolError UNKNOWN` handler stays as the safety net for mixed-version clusters where the seed broker advertised v3+ but the leader couldn't parse the headers. `refreshHeaderSupport` is a `let` that swaps to module-level `noop` at both disable sites, so subsequent sends skip the helper call entirely. The reactive `disableHeaderInjection` moves from the module-level `disabledHeaderWeakSet` to a per-producer closure so the lifetime matches the producer it describes.
The integration spec in `packages/datadog-plugin-kafkajs/test/index.spec.js` needs Docker Kafka and always connects the producer before sending, so the slow-path branch through `cluster.refreshMetadataIfNecessary().then(...)` and its `refreshMetadataIfNecessary`-missing fallback never ran outside that one CI flag. This adds an instrumentation-level spec that drives the boundary with a faked cluster, pinning the resolve / reject / missing arms of the slow path, the proactive and reactive `refreshHeaderSupport` disable, and the `producerStartCh.hasSubscribers` fast skip without spinning a broker. Refs: #8390
The integration spec in `packages/datadog-plugin-kafkajs/test/index.spec.js` needs Docker Kafka and always connects the producer before sending, so the slow-path branch through `cluster.refreshMetadataIfNecessary().then(...)` and its `refreshMetadataIfNecessary`-missing fallback never ran outside that one CI flag. This adds an instrumentation-level spec that drives the boundary with a faked cluster, pinning the resolve / reject / missing arms of the slow path, the proactive and reactive `refreshHeaderSupport` disable, and the `producerStartCh.hasSubscribers` fast skip without spinning a broker. Refs: #8390
The Produce request didn't carry headers until v3 (Kafka 0.11), so injecting on older brokers either wastes work or trips `UNKNOWN_SERVER_ERROR` on a per-leader version mismatch in mixed-version clusters. Read `cluster.brokerPool.versions[0].maxVersion` once on first send and disable injection proactively when the broker negotiated <v3; the existing `KafkaJSProtocolError UNKNOWN` handler stays as the safety net for mixed-version clusters where the seed broker advertised v3+ but the leader couldn't parse the headers. `refreshHeaderSupport` is a `let` that swaps to module-level `noop` at both disable sites, so subsequent sends skip the helper call entirely. The reactive `disableHeaderInjection` moves from the module-level `disabledHeaderWeakSet` to a per-producer closure so the lifetime matches the producer it describes.
The integration spec in `packages/datadog-plugin-kafkajs/test/index.spec.js` needs Docker Kafka and always connects the producer before sending, so the slow-path branch through `cluster.refreshMetadataIfNecessary().then(...)` and its `refreshMetadataIfNecessary`-missing fallback never ran outside that one CI flag. This adds an instrumentation-level spec that drives the boundary with a faked cluster, pinning the resolve / reject / missing arms of the slow path, the proactive and reactive `refreshHeaderSupport` disable, and the `producerStartCh.hasSubscribers` fast skip without spinning a broker. Refs: #8390
* feat(kafkajs): instrument producer.sendBatch This adds tracing, header injection, and DSM checkpoints for the kafkajs producer.sendBatch API, which was previously uninstrumented: calls produced no spans and propagated no trace or DSM context. One kafka.produce span per topicMessages[] entry matches what producer.send already emits for the single-topic case and mirrors how kafkajs implements send internally (producer/messageProducer.js delegates send to a single-entry sendBatch). Per-entry granularity keeps DSM checkpoints correct on multi-topic batches. The broker response is one aggregated array covering every topic, so the plugin's finish filters by ctx.topic before tagging kafka.messages.offsets; otherwise each per-topic span would carry every other topic's offsets. Fixes: #1711 Refs: #8468 * test(kafkajs): cover producer-boundary slow path and disable arms The integration spec in `packages/datadog-plugin-kafkajs/test/index.spec.js` needs Docker Kafka and always connects the producer before sending, so the slow-path branch through `cluster.refreshMetadataIfNecessary().then(...)` and its `refreshMetadataIfNecessary`-missing fallback never ran outside that one CI flag. This adds an instrumentation-level spec that drives the boundary with a faked cluster, pinning the resolve / reject / missing arms of the slow path, the proactive and reactive `refreshHeaderSupport` disable, and the `producerStartCh.hasSubscribers` fast skip without spinning a broker. Refs: #8390 * ci(kafkajs): run the datadog-instrumentations spec in CI The instrumentation-level spec at `packages/datadog-instrumentations/test/kafkajs.spec.js` is matched by `test:instrumentations`'s `PLUGINS=kafkajs` glob, but no workflow runs it. Without a job, codecov sees no session for the producer-boundary slow path or the `refreshHeaderSupport` disable arms.
Summary
The Produce request didn't carry headers until v3 (Kafka 0.11), so injecting on older brokers either wastes work or trips
UNKNOWN_SERVER_ERRORon a per-leader version mismatch in mixed-version clusters.cluster.brokerPool.versions[0].maxVersiononce on first send and disable injection proactively when the broker negotiated <v3.KafkaJSProtocolError UNKNOWNhandler stays as the safety net for mixed-version clusters where the seed broker advertised v3+ but the leader couldn't parse the headers.refreshHeaderSupportis aletthat swaps to module-levelnoopat both disable sites, so subsequent sends skip the helper call entirely. The reactivedisableHeaderInjectionmoves from the module-leveldisabledHeaderWeakSetto a per-producer closure so the lifetime matches the producer it describes.Test plan
./node_modules/.bin/mocha packages/datadog-instrumentations/test/helpers/kafka.spec.js— unit tests forbrokerSupportsMessageHeaders(boundary at maxVersion 2 / 3 / 9 / undefined).PLUGINS=kafkajs SPEC=index npm run test:plugins— pins that downgrading the broker pool's negotiated Produce version to 2 trips the proactive disable path; the cloned messages reach the channel withoutheaders: {}seeding.Stacked on top of #8389.
Refs: #8253