Skip to content

feat(confluent-kafka): patch AIOProducer and AIOConsumer - #5181

Open
Heisenbrick wants to merge 32 commits into
open-telemetry:mainfrom
Heisenbrick:issue-5115
Open

Heisenbrick wants to merge 32 commits into
open-telemetry:mainfrom
Heisenbrick:issue-5115

Conversation

@Heisenbrick

Copy link
Copy Markdown

Description

This PR adds asynchronous instrumentation support for AIOProducer and AIOConsumer in opentelemetry-instrumentation-confluent-kafka.

Previously, the instrumentation only patched the synchronous Producer and Consumer classes. When using asynchronous Kafka clients (confluent_kafka.aio), no spans were generated and distributed trace contexts were not propagated across message boundaries.

This change:

  • Safely imports AIOProducer and AIOConsumer (with fallback/graceful handling if .aio is not available in older confluent-kafka versions).
  • Implements AutoInstrumentedAIOProducer and AutoInstrumentedAIOConsumer to wrap async calls.
  • Adds wrap_aio_produce, wrap_aio_poll, and wrap_aio_close to start spans and inject/extract trace headers.
  • Adds automated unit tests covering async produce and async poll.

Fixes #5115

Type of change

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

  • Unit test test_aio_produce: verifies async produce generates a PRODUCER span and injects trace headers into the Kafka message.
  • Unit test test_aio_poll: verifies async poll receives message and creates the CONSUMER process span.

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

@linux-foundation-easycla

linux-foundation-easycla Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

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

  • ✅ login: Heisenbrick / name: Heisenbrick (e560815)

@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Pull request dashboard status

Waiting on the author · refreshed 2026-11-11 14:18 UTC

Respond to 6 review items (e.g. link a commit, explain why not, ask a follow-up):

  • Inline threads: 1, 2, 3, 4, 5, 6
Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Should this be with reviewers? Comment /dashboard route:reviewers to route it to them.
  • Anything wrong — including the routing? Report it with what you expected; it helps us improve the dashboard.

@Heisenbrick
Heisenbrick requested a review from a team as a code owner October 17, 2026 22:36
@xrmx

xrmx commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

@Heisenbrick please fix the failing jobs

@xrmx xrmx moved this to Reviewed PRs that need fixes in Python PR digest Oct 28, 2026
- Remove trailing whitespace in __init__.py (ruff W291)
- Fix blank line counts: E302 (2 before top-level class) and
  E303 (too many blank lines inside class body)
- Add blank line before _uninstrument after wrapt block (E301)
- Remove extra blank line before MockedAIOProducer in tests/utils.py
- Fix changelog fragment line-ending (LF, not CRLF)
- Add AutoInstrumentedAIOProducer/AIOConsumer to docs/nitpick-exceptions.ini
  so Sphinx --nitpicky does not fail on unresolved class references
@Heisenbrick

Copy link
Copy Markdown
Author

@xrmx Thank you for taking a look! I have pushed a fix commit addressing the lint, formatting, and documentation checks:

  • Removed trailing whitespace and adjusted blank line counts to satisfy ruff (E302/E303).
  • Normalized the line ending on the changelog fragment.
  • Added the new classes to docs/nitpick-exceptions.ini to resolve Sphinx build warnings.

Please let me know if anything else needs adjustments!

- Replace public AIOProducer and AIOConsumer on module and restore on uninstrument
- Do not force kwargs['headers'] = [] in wrap_aio_produce if headers is None, avoiding NotImplementedError
- Add test_aio_instrument_and_uninstrument and update test_aio_produce
@Heisenbrick

Copy link
Copy Markdown
Author

@JacksonWeber Thank you for the insightful review and catching these points! I have pushed a new commit addressing both items:

  • Public Class Replacement: _instrument() now patches AIOProducer and AIOConsumer on the confluent_kafka.aio module with AutoInstrumentedAIOProducer and AutoInstrumentedAIOConsumer, and _uninstrument() restores the original classes cleanly. I also added test_aio_instrument_and_uninstrument to verify this lifecycle.
  • Header Injection (NotImplementedError): Updated wrap_aio_produce() so that it no longer injects an empty headers list when headers are absent, avoiding the NotImplementedError raised by confluent-kafka. Trace context is now injected only when headers are explicitly supplied by the caller. I have also updated test_aio_produce to verify this behavior.

All unit tests and linters are passing locally. Please let me know if any further adjustments are needed!

@Heisenbrick

Copy link
Copy Markdown
Author

Hi @JacksonWeber, thank you for the feedback!

I have addressed your review comment — wrap_aio_poll is now a pass-through since AIOConsumer already delegates to an inner Consumer that is instrumented via AutoInstrumentedConsumer, so adding a second span layer at the AIO level was producing duplicates. I have also removed _current_consume_span from AutoInstrumentedAIOConsumer since span lifecycle is now fully managed by the inner Consumer layer.

Additionally, I have updated test_aio_poll to mirror the real AIOConsumer → Consumer structure — MockedAIOConsumer now wraps an inner MockConsumer whose poll is instrumented via wrap_poll, and the test asserts that exactly one process span is created with no duplicates from the AIO wrapper.

The CI failures from the previous commit (license header position, pylint W1113/C0328, and CRLF line endings) have also been fixed in this push.

@Heisenbrick

Copy link
Copy Markdown
Author

@xrmx

I have addressed your review comment — wrap_aio_poll is now a pass-through since AIOConsumer already delegates to an inner Consumer that is instrumented via AutoInstrumentedConsumer, so adding a second span layer at the AIO level was producing duplicates. I have also removed _current_consume_span from AutoInstrumentedAIOConsumer since span lifecycle is now fully managed by the inner Consumer layer.

Additionally, I have updated test_aio_poll to mirror the real AIOConsumer → Consumer structure — MockedAIOConsumer now wraps an inner MockConsumer whose poll is instrumented via wrap_poll, and the test asserts that exactly one process span is created with no duplicates from the AIO wrapper.

The CI failures from the previous commit (license header position, pylint W1113/C0328, and CRLF line endings) have also been fixed in this push.

@opentelemetry-pr-dashboard

This comment has been minimized.

@Heisenbrick

Copy link
Copy Markdown
Author

@emdneto @JacksonWeber — I've pushed an update in commit 43c2693 addressing the async context feedback.

wrap_aio_poll now re-attaches the process span to the caller's async task context after the inner Consumer.poll() returns from the ThreadPoolExecutor, rather than leaving it confined to the worker thread's context copy. The thread-level token is cleared beforehand to prevent cross-thread detach errors, and wrap_aio_close cleans up the async context token on close.

MockedAIOConsumer now uses a real ThreadPoolExecutor via loop.run_in_executor() to match the actual AIOConsumer behaviour, and test_aio_poll asserts trace.get_current_span().name == "test-topic process" from the caller's async task — confirming context is correctly propagated out of the worker thread.

Happy to make any further changes if needed. Thanks for the reviews!

@Heisenbrick

Copy link
Copy Markdown
Author

@emdneto @JacksonWeber — pushed one more fix in commit 8967bfc to resolve the CI / lint / instrumentation-confluent-kafka failure.

Pylint was flagging four issues in the AIO-related code:

W0406 (import-self) on import confluent_kafka and from confluent_kafka import ... — false positive caused by pylint confusing the instrumentation module's directory name with the installed confluent_kafka package. Suppressed with inline # pylint: disable=import-self.
E0611 (no-name-in-module) on the confluent_kafka.aio / confluent_kafka.experimental.aio try/except import block — expected at static analysis time since the submodule availability depends on the installed version. Suppressed with inline # pylint: disable=no-name-in-module.
W1113 (keyword-arg-before-vararg) on AutoInstrumentedAIOProducer.produce — the same pattern already suppressed on the sync produce methods. Added the matching disable comment.
W0621 (redefined-outer-name) in test_aio_poll — MockedMessage was being imported twice (once at module level, once inside the function). Removed the redundant inner import since it was already available from the top-level import.

@Heisenbrick

Copy link
Copy Markdown
Author

@JacksonWeber @emdneto — I have updated the branch with the fixes for review feedback and CI checks:

  1. Async Context Management: Managed the span context in the async task within wrap_aio_poll / wrap_aio_close and added an executor-backed test verifying trace.get_current_span() in the async task.
  2. Linting: Resolved all Pylint issues (now rated 10.00/10).
  3. Documentation: Added the external AIOProducer and AIOConsumer base class references to docs/nitpick-exceptions.ini to keep Sphinx nitpick checks passing with -W.

All 19 tests, linters, and documentation builds are verified green locally. Whenever you have a chance, could you please approve the workflow run? Thank you!

@aabmass
aabmass requested a balanced review from Copilot and removed request for JacksonWeber November 9, 2026 21:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The async consumer leaves ended process spans attached to reused executor workers, corrupting context for subsequent polls, while batch-consume context handling and producer trace propagation also remain incomplete.

5 open issues
What changed in this PR

Adds asynchronous Confluent Kafka instrumentation for AIOProducer and AIOConsumer, extending the package’s existing synchronous client support toward issue #5115.

Changes:

  • Adds optional async-client imports, instrumented subclasses, and wrapper registration and removal.
  • Adds producer spans and consumer caller-task context handling, with async mocks and tests.
  • Updates documentation reference exceptions and adds a changelog entry.
File Description
instrumentation/​opentelemetry-instrumentation-confluent-kafka/​tests/​utils.py Adds async producer and executor-backed consumer mocks.
instrumentation/​opentelemetry-instrumentation-confluent-kafka/​tests/​test_instrumentation.py Tests async produce, poll, and instrumentation registration.
instrumentation/​opentelemetry-instrumentation-confluent-kafka/​src/​opentelemetry/​instrumentation/​confluent_kafka/​__init__.py Adds optional async-client patching and telemetry wrappers.
docs/​nitpick-exceptions.ini Adds reference exceptions for async client classes.
.changelog/​5181.added Announces async Confluent Kafka support.

Review effort: Balanced

class AutoInstrumentedAIOProducer(AIOProducer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.config = _capture_config(args, kwargs)
Comment on lines +430 to +434
wrapt.wrap_function_wrapper(
AutoInstrumentedAIOConsumer,
"poll",
_inner_wrap_aio_poll,
)
Comment on lines +564 to +565
headers = KafkaPropertiesExtractor.extract_produce_headers(args, kwargs)
if headers is not None:
Comment on lines +668 to +671
def instrumented_inner_poll(timeout=None):
return ConfluentKafkaInstrumentor.wrap_poll(raw_poll, inner, tracer, (timeout,), {})

inner.poll = instrumented_inner_poll
@Heisenbrick

Copy link
Copy Markdown
Author

@aabmass @JacksonWeber — I have pushed commit 29a8ecb addressing all items raised in the review:

  1. Worker Context Isolation: In AutoInstrumentedAIOConsumer, the inner consumer is marked _is_aio = True. wrap_poll and wrap_consume now skip attaching context to the thread when running under an async consumer (if not getattr(instance, "_is_aio", False)), managing the context lifecycle entirely in the async layer (wrap_aio_poll / wrap_aio_consume). Added an executor assertion in test_aio_poll verifying that worker thread contexts remain unpolluted.
  2. Config Kwargs: _capture_config now explicitly inspects producer_conf and consumer_conf kwargs in addition to conf, ensuring server.address and server.port attributes are properly extracted. Covered in test_aio_config_kwargs.
  3. Async Batch Consume: Implemented consume() on AutoInstrumentedAIOConsumer with matching wrap_aio_consume() instrumentation, uninstrumentation unwrapping, and test coverage in test_aio_consume.
  4. Producer Header Propagation: Documented why default header injection is skipped when headers are not provided (since confluent-kafka's AIOProducer.produce() raises NotImplementedError if headers are injected).
  5. Testing: All 25 unit tests pass locally, Pylint is 10.00/10, and Ruff is clean.

Could you please approve and trigger the workflow run when you have a moment? Thank you!

from collections.abc import Collection

import confluent_kafka
import confluent_kafka # pylint: disable=import-self

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Confused on why is now needed

from confluent_kafka.aio import AIOConsumer, AIOProducer # pylint: disable=no-name-in-module
except ImportError:
try:
import confluent_kafka.experimental.aio as _aio_module # pylint: disable=no-name-in-module

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we really need to try the experimental imports if some version has already the stable one? I mean it sounds legit to add support for something that is considered stable and not experimental



# Instrument asynchronous AIOProducer and AIOConsumer if available in confluent-kafka
if AIOProducer is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Given the import is shared can we use a shared if _aio_module instead?

This branch has not been deployed

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

Labels

None yet

Projects

Status: Reviewed PRs that need fixes

Development

Successfully merging this pull request may close these issues.

opentelemetry-instrumentation-confluent-kafka: Patch AIOProducer/AIOConsumer

5 participants