fix(kafka-python): record actual producer partition from send() future - #4858
fix(kafka-python): record actual producer partition from send() future#4858bourbonkk wants to merge 6 commits into
Conversation
|
|
…r spans When neither a key nor an explicit partition is provided, the DefaultPartitioner selects a partition at random. The instrumentation called `_partition()` to record it in the span, but the producer's `send()` calls `_partition()` again independently and may pick a different partition, so `messaging.kafka.partition` frequently did not match where the message was actually delivered. Record the partition only when it can be determined without changing where the message lands (explicit partition, or key-based hash which is deterministic) and omit the attribute for the random keyless case. Fixes open-telemetry#4625 Assisted-by: Claude Opus 4.8 Signed-off-by: allen <allen.k1m@kakaocorp.com>
10cc6d8 to
8823299
Compare
Instead of estimating the destination partition before send() (which, with the default partitioner and no key, runs a random choice that send() then redoes and diverges from), read the partition back from the FutureRecordMetadata returned by send() via _produce_future.topic_partition. This is accurate for explicit, key-based and random-partition cases alike, and removes the speculative _wait_on_metadata()/_serialize()/_partition() calls the instrumentation previously made. The producer span now wraps the send() call so the partition can be read from its result before the span ends. Fixes open-telemetry#4625 Assisted-by: Claude Opus 4.8 Signed-off-by: allen <allen.k1m@kakaocorp.com>
|
Verified against a live Kafka broker (single-node, topic with 12 partitions, 40 messages sent with no key and no explicit partition, so the DefaultPartitioner assigns randomly):
Also worth noting: the old code's _wait_on_metadata(topic, max_block_ms / 1000.0) call raises KafkaTimeoutError on kafka-python 2.3.2, since that argument is now milliseconds — so the pre-send estimation path was already broken on newer versions. This PR removes that call entirely. |
Address review nits: use `list[str]` and `int | None` for the annotations touched in this change. Assisted-by: Claude Opus 4.8 Signed-off-by: allen <allen.k1m@kakaocorp.com>
| topic = KafkaPropertiesExtractor.extract_send_topic(args, kwargs) | ||
| key = KafkaPropertiesExtractor.extract_send_key(args, kwargs) | ||
| value = KafkaPropertiesExtractor.extract_send_value(args, kwargs) | ||
| partition = KafkaPropertiesExtractor._extract_argument( |
There was a problem hiding this comment.
Are these functions dead code now?
There was a problem hiding this comment.
Partly. Breaking it down:
extract_send_key/extract_send_value— no callers left after this
change.extract_send_topic— still used at utils.py:146 for the span name and
messaging.destination.extract_send_headers— still used at utils.py:141 to inject the
propagation headers._extract_argument— still backs the two above.
I left the two unused ones alone since KafkaPropertiesExtractor and its
methods are public names, so dropping them is technically a breaking change,
and I wanted to keep this PR to the partition fix.
Happy to remove them here, or in a separate PR with a CHANGELOG entry —
which would you prefer?
Description
Fixes #4625
The kafka-python producer instrumentation recorded
messaging.kafka.partitionfrom a value it computed beforesend(), by callinginstance._partition()itself. With the default partitioner and no key,_partition()selects a partition at random;send()then calls_partition()again independently and picks a different partition, so the span attribute frequently did not match where the message was actually delivered.This reads the partition back from the
FutureRecordMetadatareturned bysend()(_produce_future.topic_partition), which is the partitionsend()actually assigned. It is accurate for all cases (explicit partition, key-based, and random keyless), and removes the speculative_wait_on_metadata()/_serialize()/_partition()calls the instrumentation previously made. If the attribute can't be read it is omitted rather than recording a wrong value.The producer span now wraps the
send()call so the partition can be read from its result before the span ends.Type of change
How Has This Been Tested?
tests/test_utils.py: partition read from the future (incl.0), a check against a real kafka-pythonFutureRecordMetadata, gracefulNonewhen internals are unavailable, and_enrich_spanrecording/omitting the attribute._produce_future.topic_partitionexists across the supported range (kafka-python 2.0.3 … 3.0.8).Does This PR Require a Core Repo Change?
Checklist: