You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When end-to-end produce latency is high, an application-level timer
wrapped around send_and_wait() cannot tell whether the cause is network
RTT, replication wait on the broker side, producer-side accumulator
back-pressure, retries, or linger_ms batching. Each of these has a
different remediation path (cluster topology, broker tuning, producer
config, application code) and a different operational owner. Without
visibility into the producer's internal stages, every investigation
starts from scratch with guesswork or ad-hoc instrumentation.
The Java client exposes ~25 producer metrics that segment this latency
directly — record-queue-time-avg/max, request-latency-avg/max, bufferpool-wait-time-total, record-retry-rate, and so on — and they
are the first thing operators reach for. aiokafka exposes none, which
forces every team running it in production to write the same custom
timing wrappers and still end up unable to distinguish the same causes.
This proposal aims to close that gap with a minimal, pluggable API — not
by re-introducing the heavyweight Sensor/Stat/Histogram framework
that was removed in #1117.
Prior art in this repo
Add metrics #38 (open since 2016) — "Add metrics". Original intent was to reuse the
python-kafka Sensor framework, but it was never completed.
A single new module aiokafka/metrics.py defines a Protocol with sync
callback methods. The producer invokes these at known transition points in
the message lifecycle. No aggregation, no sampling, no sliding windows
implemented inside aiokafka — the user plugs in their preferred backend
(prometheus_client, OpenTelemetry, statsd, in-memory test double, etc.).
fromtypingimportProtocol, runtime_checkable@runtime_checkableclassProducerMetricsCollector(Protocol):
"""Receive lifecycle events from AIOKafkaProducer. All methods are synchronous and called from the hot path; implementations should be fast and non-blocking. If asynchronous work is required, defer it via an internal queue. All time-valued arguments are in **seconds** (consistent with ``time.monotonic()`` and the Prometheus naming convention). """defon_batch_drained(
self,
topic: str,
queue_time_seconds: float,
batch_size_bytes: int,
record_count: int,
) ->None:
"""Called when a batch leaves the accumulator and is handed to the sender. ``queue_time_seconds`` measures time from batch creation to drain — equivalent to Java client's ``record-queue-time``."""defon_batch_done(
self,
topic: str,
request_latency_seconds: float,
record_count: int,
) ->None:
"""Called when the broker acknowledges the batch. ``request_latency`` is measured from drain (handoff to sender) to ack — equivalent to Java client's ``request-latency``."""defon_batch_failure(
self,
topic: str,
exception: BaseException,
record_count: int,
) ->None:
"""Called when a batch ultimately fails (after retries)."""defon_buffer_wait(
self,
topic: str,
wait_seconds: float,
) ->None:
"""Called when ``add_message`` had to wait for accumulator space — equivalent to Java client's ``bufferpool-wait-time``. Direct back-pressure signal."""classNullMetricsCollector:
"""Default no-op implementation. Zero overhead when nothing is plugged in."""defon_batch_drained(self, *args, **kwargs): passdefon_batch_done(self, *args, **kwargs): passdefon_batch_failure(self, *args, **kwargs): passdefon_buffer_wait(self, *args, **kwargs): pass
No new dependencies. Users who want Prometheus plug in prometheus_client.Histogram; users who want OTel plug in OTel.
aiokafka stays dependency-clean.
Better fit for Python ecosystem.prometheus_client, OpenTelemetry
and statsd all already provide aggregation/percentiles natively — far
better than the Java Avg/Max sliding-window stats, which lose
information.
Unblocks Add compability with OpenTelemetry #862 (OpenTelemetry). With these hooks, OTel
auto-instrumentation can be written as a third-party package without any
further changes to aiokafka.
Minimal hot-path cost. With NullMetricsCollector, each hook is a
single attribute lookup + method call returning None. ~50 ns in CPython.
Hooks fire per-batch, not per-record.
Out of scope for this issue
To keep the PR reviewable, the first PR will include only the
producer-side metrics listed above. Follow-ups can add:
OTel-specific helper package (likely belongs in a separate repo)
Scope of the first PR
New module aiokafka/metrics.py (~80 lines: Protocol + NullCollector + docstrings)
Constructor parameter metrics_collector on AIOKafkaProducer
Hook calls in MessageBatch.drain_ready, MessageBatch.done, MessageBatch.failure, MessageAccumulator.add_message
New _drained_at field on MessageBatch
tests/test_metrics.py with a fake collector verifying:
events fire on normal send
on_buffer_wait fires when accumulator is full
on_batch_failure fires when delivery fails
NullMetricsCollector does not introduce overhead (smoke test)
docs/metrics.rst with API reference + Prometheus example
examples/prometheus_metrics.py
CHANGES.rst entry under 0.15.0
Total expected diff: ~300 lines added, ~10 lines modified in existing files.
Questions for maintainers before I start coding
Are you open to this direction in principle?
Naming: I went with Java-aligned terminology (queue_time, request_latency).
Any preference for renaming?
Should topic be passed always, or only when an opt-in enable_per_topic_metrics=True flag is set? Java client emits per-topic
metrics by default; passing topic per-callback adds zero cost but
downstream aggregation cardinality is the user's choice.
Motivation
When end-to-end produce latency is high, an application-level timer
wrapped around
send_and_wait()cannot tell whether the cause is networkRTT, replication wait on the broker side, producer-side accumulator
back-pressure, retries, or
linger_msbatching. Each of these has adifferent remediation path (cluster topology, broker tuning, producer
config, application code) and a different operational owner. Without
visibility into the producer's internal stages, every investigation
starts from scratch with guesswork or ad-hoc instrumentation.
The Java client exposes ~25 producer metrics that segment this latency
directly —
record-queue-time-avg/max,request-latency-avg/max,bufferpool-wait-time-total,record-retry-rate, and so on — and theyare the first thing operators reach for. aiokafka exposes none, which
forces every team running it in production to write the same custom
timing wrappers and still end up unable to distinguish the same causes.
This proposal aims to close that gap with a minimal, pluggable API — not
by re-introducing the heavyweight
Sensor/Stat/Histogramframeworkthat was removed in #1117.
Prior art in this repo
python-kafka
Sensorframework, but it was never completed.by the lack of hook points.
metrics code. This proposal deliberately does not bring any of it back.
Proposal: callback-based
ProducerMetricsCollectorprotocolA single new module
aiokafka/metrics.pydefines a Protocol with synccallback methods. The producer invokes these at known transition points in
the message lifecycle. No aggregation, no sampling, no sliding windows
implemented inside aiokafka — the user plugs in their preferred backend
(
prometheus_client, OpenTelemetry, statsd, in-memory test double, etc.).Wired into the producer via constructor:
Hook locations
on_batch_drainedmessage_accumulator.py:288(MessageBatch.drain_ready)time.monotonic() - self._ctime(_ctimealready exists on line 144)on_batch_donemessage_accumulator.py:204(MessageBatch.done)time.monotonic() - self._drained_at(new field set indrain_ready)on_batch_failuremessage_accumulator.py:248(MessageBatch.failure)on_buffer_waitmessage_accumulator.py:425-429(add_message, aroundawait batch.wait_drain)wait_drainOne new
floatfield onMessageBatch(_drained_at). All other timingscome from data already tracked by the producer.
Mapping to Java client metrics
Users typically aggregate the raw events into Prometheus
Histograms orsimilar. The mapping to Java metric names is:
record-queue-time-avg/maxon_batch_drained.queue_time_secondsrequest-latency-avg/maxon_batch_done.request_latency_secondsbatch-size-avg/maxon_batch_drained.batch_size_bytesrecords-per-request-avgon_batch_drained.record_countrecord-send-rate/totalon_batch_done.record_countrecord-error-rate/totalon_batch_failurebufferpool-wait-time-ns-totalon_buffer_wait.wait_secondsWhy callbacks rather than an embedded Sensor framework
of dead aggregation code. This proposal adds ~150 lines of callback
plumbing and no aggregation code.
prometheus_client.Histogram; users who want OTel plug in OTel.aiokafka stays dependency-clean.
prometheus_client, OpenTelemetryand statsd all already provide aggregation/percentiles natively — far
better than the Java
Avg/Maxsliding-window stats, which loseinformation.
auto-instrumentation can be written as a third-party package without any
further changes to aiokafka.
docs/metrics.rstwith a referenceexamples/prometheus_metrics.py.NullMetricsCollector, each hook is asingle attribute lookup + method call returning
None. ~50 ns in CPython.Hooks fire per-batch, not per-record.
Out of scope for this issue
To keep the PR reviewable, the first PR will include only the
producer-side metrics listed above. Follow-ups can add:
topicis already passed)fetch-latency, lag, commit time)Scope of the first PR
aiokafka/metrics.py(~80 lines: Protocol + NullCollector + docstrings)metrics_collectoronAIOKafkaProducerMessageBatch.drain_ready,MessageBatch.done,MessageBatch.failure,MessageAccumulator.add_message_drained_atfield onMessageBatchtests/test_metrics.pywith a fake collector verifying:on_buffer_waitfires when accumulator is fullon_batch_failurefires when delivery failsNullMetricsCollectordoes not introduce overhead (smoke test)docs/metrics.rstwith API reference + Prometheus exampleexamples/prometheus_metrics.pyCHANGES.rstentry under 0.15.0Total expected diff: ~300 lines added, ~10 lines modified in existing files.
Questions for maintainers before I start coding
queue_time,request_latency).Any preference for renaming?
topicbe passed always, or only when an opt-inenable_per_topic_metrics=Trueflag is set? Java client emits per-topicmetrics by default; passing
topicper-callback adds zero cost butdownstream aggregation cardinality is the user's choice.
"the old framework was dead code"?