-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-13945: add bytes/records consumed and produced metrics #12235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
afd325b
6ab8c54
6a4eb80
f69fa86
70da1e2
c89d21b
edba70f
7a4dad3
0761f0d
5ed377d
a03f926
b8d8f0c
9a0cfd1
0df6c10
a8681d1
31d10f8
7dea1ae
5745fe9
511b5bc
a5756c1
314be89
65a7ce4
3e09f01
d39b249
6814000
7e67567
9823764
49fa512
a408b95
a0e2f08
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,8 @@ | |
| import org.apache.kafka.streams.processor.TaskId; | ||
| import org.apache.kafka.streams.processor.internals.metrics.StreamsMetricsImpl; | ||
| import org.apache.kafka.streams.processor.internals.metrics.TaskMetrics; | ||
| import org.apache.kafka.streams.processor.internals.metrics.TopicMetrics; | ||
|
|
||
| import org.slf4j.Logger; | ||
|
|
||
| import java.util.Collections; | ||
|
|
@@ -54,17 +56,22 @@ | |
| import java.util.Map; | ||
| import java.util.concurrent.atomic.AtomicReference; | ||
|
|
||
| import static org.apache.kafka.streams.processor.internals.ClientUtils.producerRecordSizeInBytes; | ||
|
|
||
| public class RecordCollectorImpl implements RecordCollector { | ||
| private final static String SEND_EXCEPTION_MESSAGE = "Error encountered sending record to topic %s for task %s due to:%n%s"; | ||
|
|
||
| private final Logger log; | ||
| private final TaskId taskId; | ||
| private final StreamsProducer streamsProducer; | ||
| private final ProductionExceptionHandler productionExceptionHandler; | ||
| private final Sensor droppedRecordsSensor; | ||
| private final boolean eosEnabled; | ||
| private final Map<TopicPartition, Long> offsets; | ||
|
|
||
| private final StreamsMetricsImpl streamsMetrics; | ||
| private final Sensor droppedRecordsSensor; | ||
| private final Map<String, Map<String, Sensor>> sinkNodeToProducedSensorByTopic = new HashMap<>(); | ||
|
|
||
| private final AtomicReference<KafkaException> sendException = new AtomicReference<>(null); | ||
|
|
||
| /** | ||
|
|
@@ -74,15 +81,29 @@ public RecordCollectorImpl(final LogContext logContext, | |
| final TaskId taskId, | ||
| final StreamsProducer streamsProducer, | ||
| final ProductionExceptionHandler productionExceptionHandler, | ||
| final StreamsMetricsImpl streamsMetrics) { | ||
| final StreamsMetricsImpl streamsMetrics, | ||
| final ProcessorTopology topology) { | ||
| this.log = logContext.logger(getClass()); | ||
| this.taskId = taskId; | ||
| this.streamsProducer = streamsProducer; | ||
| this.productionExceptionHandler = productionExceptionHandler; | ||
| this.eosEnabled = streamsProducer.eosEnabled(); | ||
| this.streamsMetrics = streamsMetrics; | ||
|
|
||
| final String threadId = Thread.currentThread().getName(); | ||
| this.droppedRecordsSensor = TaskMetrics.droppedRecordsSensor(threadId, taskId.toString(), streamsMetrics); | ||
| for (final String topic : topology.sinkTopics()) { | ||
| final String processorNodeId = topology.sink(topic).name(); | ||
| sinkNodeToProducedSensorByTopic.computeIfAbsent(processorNodeId, t -> new HashMap<>()).put( | ||
| topic, | ||
| TopicMetrics.producedSensor( | ||
| threadId, | ||
| taskId.toString(), | ||
| processorNodeId, | ||
| topic, | ||
| streamsMetrics | ||
| )); | ||
| } | ||
|
|
||
| this.offsets = new HashMap<>(); | ||
| } | ||
|
|
@@ -106,6 +127,8 @@ public <K, V> void send(final String topic, | |
| final Long timestamp, | ||
| final Serializer<K> keySerializer, | ||
| final Serializer<V> valueSerializer, | ||
| final String processorNodeId, | ||
| final InternalProcessorContext<Void, Void> context, | ||
| final StreamPartitioner<? super K, ? super V> partitioner) { | ||
| final Integer partition; | ||
|
|
||
|
|
@@ -122,7 +145,7 @@ public <K, V> void send(final String topic, | |
| // here we cannot drop the message on the floor even if it is a transient timeout exception, | ||
| // so we treat everything the same as a fatal exception | ||
| throw new StreamsException("Could not determine the number of partitions for topic '" + topic + | ||
| "' for task " + taskId + " due to " + fatal.toString(), | ||
| "' for task " + taskId + " due to " + fatal, | ||
| fatal | ||
| ); | ||
| } | ||
|
|
@@ -136,7 +159,7 @@ public <K, V> void send(final String topic, | |
| partition = null; | ||
| } | ||
|
|
||
| send(topic, key, value, headers, partition, timestamp, keySerializer, valueSerializer); | ||
| send(topic, key, value, headers, partition, timestamp, keySerializer, valueSerializer, processorNodeId, context); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -147,7 +170,9 @@ public <K, V> void send(final String topic, | |
| final Integer partition, | ||
| final Long timestamp, | ||
| final Serializer<K> keySerializer, | ||
| final Serializer<V> valueSerializer) { | ||
| final Serializer<V> valueSerializer, | ||
| final String processorNodeId, | ||
| final InternalProcessorContext<Void, Void> context) { | ||
| checkForException(); | ||
|
|
||
| final byte[] keyBytes; | ||
|
|
@@ -173,7 +198,7 @@ public <K, V> void send(final String topic, | |
| valueClass), | ||
| exception); | ||
| } catch (final RuntimeException exception) { | ||
| final String errorMessage = String.format(SEND_EXCEPTION_MESSAGE, topic, taskId, exception.toString()); | ||
| final String errorMessage = String.format(SEND_EXCEPTION_MESSAGE, topic, taskId, exception); | ||
| throw new StreamsException(errorMessage, exception); | ||
| } | ||
|
|
||
|
|
@@ -192,6 +217,28 @@ public <K, V> void send(final String topic, | |
| } else { | ||
| log.warn("Received offset={} in produce response for {}", metadata.offset(), tp); | ||
| } | ||
|
|
||
| if (!topic.endsWith("-changelog")) { | ||
| // we may not have created a sensor yet if the node uses dynamic topic routing | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment is a bit misleading here. AFAIU it refers to the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I do see how it might be confusing if it's interpreted as referring to the line above, and I do know your preference, but I stand by this particular comment -- it follows the rule of explaining WHY we do something rather than WHAT, and I believe in this case the why is very much not obvious* and worry someone reading this code in the future might think this branch would actually indicate a bug and decide to throw an exception or something. So I'll adjust the spacing to clarify which line it's referring to but I would like to keep it.
* Source: I myself did not remember about dynamic topics and only realized this was possible when looking at some TTD tests
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh wait sorry I'm being dumb, yes, it does refer to the |
||
| final Map<String, Sensor> producedSensorByTopic = sinkNodeToProducedSensorByTopic.get(processorNodeId); | ||
| if (producedSensorByTopic == null) { | ||
| log.error("Unable to records bytes produced to topic {} by sink node {} as the node is not recognized.\n" | ||
| + "Known sink nodes are {}.", topic, processorNodeId, sinkNodeToProducedSensorByTopic.keySet()); | ||
| } else { | ||
| final Sensor topicProducedSensor = producedSensorByTopic.computeIfAbsent( | ||
| topic, | ||
| t -> TopicMetrics.producedSensor( | ||
| Thread.currentThread().getName(), | ||
| taskId.toString(), | ||
| processorNodeId, | ||
| topic, | ||
| context.metrics() | ||
| ) | ||
| ); | ||
| final long bytesProduced = producerRecordSizeInBytes(serializedRecord); | ||
| topicProducedSensor.record(bytesProduced, context.currentSystemTimeMs()); | ||
| } | ||
| } | ||
| } else { | ||
| recordSendError(topic, exception, serializedRecord); | ||
|
|
||
|
|
@@ -267,6 +314,8 @@ public void flush() { | |
| public void closeClean() { | ||
| log.info("Closing record collector clean"); | ||
|
|
||
| removeAllProducedSensors(); | ||
|
|
||
| // No need to abort transaction during a clean close: either we have successfully committed the ongoing | ||
| // transaction during handleRevocation and thus there is no transaction in flight, or else none of the revoked | ||
| // tasks had any data in the current transaction and therefore there is no need to commit or abort it. | ||
|
|
@@ -290,6 +339,14 @@ public void closeDirty() { | |
| checkForException(); | ||
| } | ||
|
|
||
| private void removeAllProducedSensors() { | ||
| for (final Map<String, Sensor> nodeMap : sinkNodeToProducedSensorByTopic.values()) { | ||
| for (final Sensor sensor : nodeMap.values()) { | ||
| streamsMetrics.removeSensor(sensor); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public Map<TopicPartition, Long> offsets() { | ||
| return Collections.unmodifiableMap(new HashMap<>(offsets)); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll follow up with a docs PR separately