Skip to content

fix(kafka-python): record actual producer partition from send() future - #4858

Open
bourbonkk wants to merge 6 commits into
open-telemetry:mainfrom
bourbonkk:fix/kafka-python-partition-4625
Open

fix(kafka-python): record actual producer partition from send() future#4858
bourbonkk wants to merge 6 commits into
open-telemetry:mainfrom
bourbonkk:fix/kafka-python-partition-4625

Conversation

@bourbonkk

@bourbonkk bourbonkk commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #4625

The kafka-python producer instrumentation recorded messaging.kafka.partition from a value it computed before send(), by calling instance._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 FutureRecordMetadata returned by send() (_produce_future.topic_partition), which is the partition send() 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.

Note: an earlier revision of this PR took a narrower approach (only record the partition when deterministic). It was replaced with reading the real partition from the future — see commit history.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

  • Unit tests in tests/test_utils.py: partition read from the future (incl. 0), a check against a real kafka-python FutureRecordMetadata, graceful None when internals are unavailable, and _enrich_span recording/omitting the attribute.
  • Verified _produce_future.topic_partition exists across the supported range (kafka-python 2.0.3 … 3.0.8).

Does This PR Require a Core Repo Change?

  • No.

Checklist:

  • Followed the style guidelines of this project
  • Changelogs have been updated
  • Unit tests have been added
  • Documentation has been updated

@bourbonkk
bourbonkk requested a review from a team as a code owner July 23, 2026 06:25
@linux-foundation-easycla

linux-foundation-easycla Bot commented Jul 23, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

  • ✅ login: bourbonkk / name: allen (8823299)

…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>
@bourbonkk
bourbonkk force-pushed the fix/kafka-python-partition-4625 branch from 10cc6d8 to 8823299 Compare July 23, 2026 06:28
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>
@bourbonkk bourbonkk reopened this Jul 24, 2026
@bourbonkk bourbonkk changed the title fix(kafka-python): only record known destination partition in producer spans fix(kafka-python): record actual producer partition from send() future Jul 24, 2026
@bourbonkk

Copy link
Copy Markdown
Contributor Author

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):

  • New approach (this PR): the partition recorded on the span matched the actual delivered partition (read from future.get().partition) for all 40/40 messages.
  • Old approach (estimating via _partition() before send()): the recorded partition was wrong for 35/40 messages (~88%), e.g. estimate=9 → actual=3, estimate=0 → actual=8.

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>
Comment on lines -62 to -65
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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these functions dead code now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Incorrect partition in opentelemetry-instrumentation-kafka-python

3 participants