This is an automated email from the ASF dual-hosted git repository.

pvillard31 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 856e5fd0a9d NIFI-16375 Add Lag and Record Metrics to ConsumeKafka 
(#11702)
856e5fd0a9d is described below

commit 856e5fd0a9da89d3efdc2d2cab447c7d9d23df5c
Author: David Handermann <[email protected]>
AuthorDate: Wed Sep 23 03:25:45 2026 -0500

    NIFI-16375 Add Lag and Record Metrics to ConsumeKafka (#11702)
---
 .../nifi/kafka/processors/ConsumeKafkaIT.java      |   9 ++
 .../apache/nifi/kafka/processors/ConsumeKafka.java | 103 ++++++++++++++-
 .../kafka/processors/consumer/KafkaMetricName.java |  37 ++++++
 .../kafka/processors/consumer/OffsetTracker.java   |  34 ++++-
 .../AbstractRecordStreamKafkaMessageConverter.java |   8 ++
 .../nifi/kafka/processors/ConsumeKafkaTest.java    | 147 +++++++++++++++++++++
 .../processors/consumer/OffsetTrackerTest.java     | 131 ++++++++++++++++++
 .../RecordStreamKafkaMessageConverterTest.java     |  65 +++++++--
 8 files changed, 517 insertions(+), 17 deletions(-)

diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaIT.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaIT.java
index 76fe12d06ad..50daa420b83 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaIT.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaIT.java
@@ -20,6 +20,7 @@ import org.apache.kafka.clients.producer.ProducerRecord;
 import org.apache.kafka.common.header.Header;
 import org.apache.kafka.common.header.internals.RecordHeader;
 import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.kafka.processors.consumer.KafkaMetricName;
 import org.apache.nifi.kafka.processors.consumer.ProcessingStrategy;
 import org.apache.nifi.kafka.service.Kafka3ConnectionService;
 import org.apache.nifi.kafka.service.api.consumer.AutoOffsetReset;
@@ -39,6 +40,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 
@@ -115,6 +117,13 @@ class ConsumeKafkaIT extends AbstractConsumeKafkaIT {
         flowFile.assertAttributeEquals("bbb", "value");
         flowFile.assertAttributeNotExists("aaa");
         flowFile.assertAttributeNotExists("ccc");
+
+        final Map<String, String> topicPartitionAttributes = Map.of(
+                KafkaFlowFileAttribute.KAFKA_TOPIC, topic,
+                KafkaFlowFileAttribute.KAFKA_PARTITION, 
Integer.toString(FIRST_PARTITION));
+        assertEquals(1L, 
runner.getCounterValue(KafkaMetricName.RECORDS_CONSUMED.getMetricName(), 
topicPartitionAttributes));
+        assertEquals(RECORD_VALUE.getBytes(StandardCharsets.UTF_8).length,
+                
runner.getCounterValue(KafkaMetricName.BYTES_CONSUMED.getMetricName(), 
topicPartitionAttributes));
     }
 
     @Test
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/ConsumeKafka.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/ConsumeKafka.java
index 483f5466ff2..f000ea8b8be 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/ConsumeKafka.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/ConsumeKafka.java
@@ -36,6 +36,7 @@ import 
org.apache.nifi.components.connector.components.ConnectorMethod;
 import org.apache.nifi.expression.ExpressionLanguageScope;
 import org.apache.nifi.kafka.processors.common.HeaderValueConverter;
 import org.apache.nifi.kafka.processors.common.KafkaUtils;
+import org.apache.nifi.kafka.processors.consumer.KafkaMetricName;
 import org.apache.nifi.kafka.processors.consumer.OffsetTracker;
 import org.apache.nifi.kafka.processors.consumer.ProcessingStrategy;
 import org.apache.nifi.kafka.processors.consumer.bundle.ByteRecordBundler;
@@ -49,6 +50,7 @@ import 
org.apache.nifi.kafka.processors.consumer.convert.RecordStreamKafkaMessag
 import 
org.apache.nifi.kafka.processors.consumer.convert.WrapperRecordStreamKafkaMessageConverter;
 import org.apache.nifi.kafka.service.api.KafkaConnectionService;
 import org.apache.nifi.kafka.service.api.common.PartitionState;
+import org.apache.nifi.kafka.service.api.common.TopicPartitionSummary;
 import org.apache.nifi.kafka.service.api.consumer.AutoOffsetReset;
 import org.apache.nifi.kafka.service.api.consumer.KafkaConsumerService;
 import org.apache.nifi.kafka.service.api.consumer.PollingContext;
@@ -70,6 +72,7 @@ import org.apache.nifi.processor.ProcessSession;
 import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.processor.VerifiableProcessor;
 import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.metrics.CommitTiming;
 import org.apache.nifi.processor.util.StandardValidators;
 import org.apache.nifi.serialization.RecordReader;
 import org.apache.nifi.serialization.RecordReaderFactory;
@@ -85,11 +88,13 @@ import java.time.Duration;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.HexFormat;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.OptionalLong;
 import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -500,7 +505,9 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
                         break;
                     }
 
-                    final Iterator<ByteRecord> consumerRecords = 
consumerService.poll(maxWaitDuration).iterator();
+                    final TopicPartitionScanningIterator consumerRecords = new 
TopicPartitionScanningIterator(
+                            consumerService.poll(maxWaitDuration).iterator(), 
offsetTracker
+                    );
                     if (!consumerRecords.hasNext()) {
                         getLogger().trace("No Kafka Records consumed: {}", 
pollingContext);
                         // Check if a rebalance occurred during poll - if so, 
break to commit what we have
@@ -513,6 +520,7 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
 
                     recordsReceived = true;
                     processConsumerRecords(context, session, offsetTracker, 
consumerRecords);
+                    recordLag(session, consumerService, 
consumerRecords.getTopicPartitionSummaries());
 
                     // Check if a rebalance occurred during poll - if so, 
break to commit what we have
                     if (consumerService.hasRevokedPartitions()) {
@@ -529,6 +537,8 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
                     }
                 } catch (final Exception e) {
                     getLogger().error("Failed to consume Kafka Records", e);
+                    // Metrics recorded based on Processor behavior regardless 
of exceptions
+                    recordConsumption(session, offsetTracker);
                     consumerService.rollback();
                     close(consumerService, "Encountered Exception while 
consuming or writing out Kafka Records");
                     context.yield();
@@ -539,6 +549,10 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
                 }
             }
 
+            if (recordsReceived) {
+                recordConsumption(session, offsetTracker);
+            }
+
             if (!recordsReceived && !consumerService.hasRevokedPartitions()) {
                 getLogger().trace("No Kafka Records consumed, re-queuing 
consumer");
                 consumerServices.offer(consumerService);
@@ -572,6 +586,39 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
         }
     }
 
+    // Lag recorded for polled partitions that return records
+    private void recordLag(final ProcessSession session, final 
KafkaConsumerService consumerService,
+            final Collection<TopicPartitionSummary> topicPartitions) {
+        for (final TopicPartitionSummary topicPartition : topicPartitions) {
+            final OptionalLong lag = 
consumerService.currentLag(topicPartition);
+            if (lag.isPresent()) {
+                
session.recordGauge(KafkaMetricName.RECORDS_CONSUMER_LAG.getMetricName(), 
lag.getAsLong(),
+                        topicPartitionAttributes(topicPartition.getTopic(), 
topicPartition.getPartition()), CommitTiming.NOW);
+            }
+        }
+    }
+
+    private void recordConsumption(final ProcessSession session, final 
OffsetTracker offsetTracker) {
+        for (final Map.Entry<TopicPartitionSummary, Long> entry : 
offsetTracker.getPartitionRecords().entrySet()) {
+            final TopicPartitionSummary topicPartition = entry.getKey();
+            
session.adjustCounter(KafkaMetricName.RECORDS_CONSUMED.getMetricName(), 
entry.getValue(),
+                    topicPartitionAttributes(topicPartition.getTopic(), 
topicPartition.getPartition()), CommitTiming.NOW);
+        }
+
+        for (final Map.Entry<TopicPartitionSummary, Long> entry : 
offsetTracker.getPartitionBytes().entrySet()) {
+            final TopicPartitionSummary topicPartition = entry.getKey();
+            
session.adjustCounter(KafkaMetricName.BYTES_CONSUMED.getMetricName(), 
entry.getValue(),
+                    topicPartitionAttributes(topicPartition.getTopic(), 
topicPartition.getPartition()), CommitTiming.NOW);
+        }
+    }
+
+    private static Map<String, String> topicPartitionAttributes(final String 
topic, final int partition) {
+        return Map.of(
+                KafkaFlowFileAttribute.KAFKA_TOPIC, topic,
+                KafkaFlowFileAttribute.KAFKA_PARTITION, 
Integer.toString(partition)
+        );
+    }
+
     /**
      * Reports backlog by querying committed and end offsets for the 
Processor's consumer group via the
      * connection service's Admin client. The Admin-client path is used 
regardless of whether any
@@ -841,6 +888,7 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
                     getLogger().debug("Session committed successfully during 
rebalance callback");
 
                     if (offsetTracker != null) {
+                        recordConsumption(session, offsetTracker);
                         offsetTracker.getRecordCounts().forEach((topic, count) 
-> {
                             session.adjustCounter("Records Acknowledged for " 
+ topic, count, true);
                         });
@@ -950,6 +998,59 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
         return pollingContext;
     }
 
+    static class TopicPartitionScanningIterator implements 
Iterator<ByteRecord> {
+        private final Iterator<ByteRecord> iterator;
+        private final OffsetTracker offsetTracker;
+        private final Set<TopicPartitionSummary> topicPartitionSummaries = new 
HashSet<>();
+        private TopicPartitionSummary lastTopicPartition;
+
+        TopicPartitionScanningIterator(final Iterator<ByteRecord> iterator, 
final OffsetTracker offsetTracker) {
+            this.iterator = iterator;
+            this.offsetTracker = offsetTracker;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return iterator.hasNext();
+        }
+
+        @Override
+        public ByteRecord next() {
+            final ByteRecord record = iterator.next();
+            final TopicPartitionSummary topicPartition = 
topicPartition(record);
+            offsetTracker.recordConsumed(topicPartition, 
record.getBundledCount(), consumedBytes(record));
+            return record;
+        }
+
+        public Set<TopicPartitionSummary> getTopicPartitionSummaries() {
+            return topicPartitionSummaries;
+        }
+
+        private TopicPartitionSummary topicPartition(final ByteRecord record) {
+            if (isNewTopicPartition(record)) {
+                final TopicPartitionSummary summary = new 
TopicPartitionSummary(record.getTopic(), record.getPartition());
+                topicPartitionSummaries.add(summary);
+                lastTopicPartition = summary;
+            }
+            return lastTopicPartition;
+        }
+
+        private boolean isNewTopicPartition(final ByteRecord record) {
+            return lastTopicPartition == null
+                    || !lastTopicPartition.getTopic().equals(record.getTopic())
+                    || lastTopicPartition.getPartition() != 
record.getPartition();
+        }
+
+        private static long consumedBytes(final ByteRecord record) {
+            long recordSize = record.getValue().length;
+            final Optional<byte[]> key = record.getKey();
+            if (key.isPresent()) {
+                recordSize += key.get().length;
+            }
+            return recordSize;
+        }
+    }
+
     private static class RebalanceSessionHolder implements SessionContext {
         private final ProcessSession session;
         private final OffsetTracker offsetTracker;
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/KafkaMetricName.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/KafkaMetricName.java
new file mode 100644
index 00000000000..8b099aac322
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/KafkaMetricName.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.kafka.processors.consumer;
+
+/**
+ * Metric names for ConsumeKafka counters and gauges.
+ */
+public enum KafkaMetricName {
+    BYTES_CONSUMED("kafka.bytes.consumed"),
+    RECORDS_CONSUMED("kafka.records.consumed"),
+    RECORDS_CONSUMER_LAG("kafka.records.consumer.lag"),
+    RECORDS_PARSED_ERRORS("kafka.records.parsed.errors");
+
+    private final String metricName;
+
+    KafkaMetricName(final String metricName) {
+        this.metricName = metricName;
+    }
+
+    public String getMetricName() {
+        return metricName;
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/OffsetTracker.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/OffsetTracker.java
index b79725940c1..a727140308b 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/OffsetTracker.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/OffsetTracker.java
@@ -24,11 +24,14 @@ import org.apache.nifi.kafka.service.api.record.ByteRecord;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 
 public class OffsetTracker {
     private final Map<TopicPartitionSummary, OffsetSummary> offsets = new 
HashMap<>();
     private final Map<String, Long> recordCounts = new HashMap<>();
+    private final Map<TopicPartitionSummary, Long> partitionRecords = new 
HashMap<>();
+    private final Map<TopicPartitionSummary, Long> partitionBytes = new 
HashMap<>();
     private final AtomicLong totalRecordSize = new AtomicLong();
 
     public void update(final ByteRecord consumerRecord) {
@@ -36,13 +39,22 @@ public class OffsetTracker {
         final long offset = consumerRecord.getOffset();
         final OffsetSummary offsetSummary = 
offsets.computeIfAbsent(topicPartitionSummary, (summary) -> new 
OffsetSummary(offset));
         offsetSummary.setOffset(offset);
-        recordCounts.merge(consumerRecord.getTopic(), 
consumerRecord.getBundledCount(), Long::sum);
 
-        // Update Total Record Size with Key and Value length
-        consumerRecord.getKey()
-                .map(key -> key.length)
-                .ifPresent(totalRecordSize::addAndGet);
-        totalRecordSize.addAndGet(consumerRecord.getValue().length);
+        final long bundledCount = consumerRecord.getBundledCount();
+        recordCounts.merge(consumerRecord.getTopic(), bundledCount, Long::sum);
+
+        long recordSize = consumerRecord.getValue().length;
+        final Optional<byte[]> key = consumerRecord.getKey();
+        if (key.isPresent()) {
+            recordSize += key.get().length;
+        }
+
+        totalRecordSize.addAndGet(recordSize);
+    }
+
+    public void recordConsumed(final TopicPartitionSummary topicPartition, 
final long records, final long bytes) {
+        partitionRecords.merge(topicPartition, records, Long::sum);
+        partitionBytes.merge(topicPartition, bytes, Long::sum);
     }
 
     public long getTotalRecordSize() {
@@ -53,6 +65,14 @@ public class OffsetTracker {
         return recordCounts;
     }
 
+    public Map<TopicPartitionSummary, Long> getPartitionRecords() {
+        return partitionRecords;
+    }
+
+    public Map<TopicPartitionSummary, Long> getPartitionBytes() {
+        return partitionBytes;
+    }
+
     public PollingSummary getPollingSummary(final PollingContext 
pollingContext) {
         final PollingSummary pollingSummary;
         if (pollingContext.getTopicPattern().isPresent()) {
@@ -68,6 +88,8 @@ public class OffsetTracker {
     public void clear() {
         offsets.clear();
         recordCounts.clear();
+        partitionRecords.clear();
+        partitionBytes.clear();
         totalRecordSize.set(0);
     }
 }
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/AbstractRecordStreamKafkaMessageConverter.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/AbstractRecordStreamKafkaMessageConverter.java
index bab7e0c93be..a707c22e34e 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/AbstractRecordStreamKafkaMessageConverter.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/AbstractRecordStreamKafkaMessageConverter.java
@@ -20,11 +20,14 @@ import org.apache.nifi.flowfile.FlowFile;
 import org.apache.nifi.kafka.processors.ConsumeKafka;
 import org.apache.nifi.kafka.processors.common.HeaderValueConverter;
 import org.apache.nifi.kafka.processors.common.KafkaUtils;
+import org.apache.nifi.kafka.processors.consumer.KafkaMetricName;
 import org.apache.nifi.kafka.processors.consumer.OffsetTracker;
 import org.apache.nifi.kafka.service.api.record.ByteRecord;
+import org.apache.nifi.kafka.shared.attribute.KafkaFlowFileAttribute;
 import org.apache.nifi.kafka.shared.property.KeyEncoding;
 import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.metrics.CommitTiming;
 import org.apache.nifi.schema.access.SchemaNotFoundException;
 import org.apache.nifi.serialization.MalformedRecordException;
 import org.apache.nifi.serialization.RecordReader;
@@ -122,6 +125,11 @@ public abstract class 
AbstractRecordStreamKafkaMessageConverter implements Kafka
         ff = session.write(ff, out -> out.write(value));
         session.transfer(ff, ConsumeKafka.PARSE_FAILURE);
         session.adjustCounter("Records Received from " + 
consumerRecord.getTopic(), 1, false);
+        final Map<String, String> counterAttributes = Map.of(
+                KafkaFlowFileAttribute.KAFKA_TOPIC, consumerRecord.getTopic(),
+                KafkaFlowFileAttribute.KAFKA_PARTITION, 
Integer.toString(consumerRecord.getPartition())
+        );
+        
session.adjustCounter(KafkaMetricName.RECORDS_PARSED_ERRORS.getMetricName(), 1, 
counterAttributes, CommitTiming.NOW);
     }
 
     /**
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaTest.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaTest.java
index 92e82516415..4cd6fc81d71 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaTest.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaTest.java
@@ -19,9 +19,14 @@ package org.apache.nifi.kafka.processors;
 import org.apache.nifi.components.Backlog;
 import org.apache.nifi.components.ConfigVerificationResult;
 import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.kafka.processors.consumer.KafkaMetricName;
+import org.apache.nifi.kafka.processors.consumer.ProcessingStrategy;
 import org.apache.nifi.kafka.service.api.KafkaConnectionService;
 import org.apache.nifi.kafka.service.api.common.PartitionState;
+import org.apache.nifi.kafka.service.api.common.TopicPartitionSummary;
 import org.apache.nifi.kafka.service.api.consumer.KafkaConsumerService;
+import org.apache.nifi.kafka.service.api.record.ByteRecord;
+import org.apache.nifi.kafka.shared.attribute.KafkaFlowFileAttribute;
 import org.apache.nifi.reporting.InitializationException;
 import org.apache.nifi.util.TestRunner;
 import org.apache.nifi.util.TestRunners;
@@ -32,9 +37,13 @@ import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.OptionalLong;
 import java.util.Queue;
@@ -45,7 +54,9 @@ import static 
org.apache.nifi.kafka.processors.ConsumeKafka.GROUP_ID;
 import static org.apache.nifi.kafka.processors.ConsumeKafka.TOPICS;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.never;
@@ -59,6 +70,30 @@ class ConsumeKafkaTest {
 
     private static final int FIRST_PARTITION = 0;
 
+    private static final int SECOND_PARTITION = 1;
+
+    private static final byte[] FIRST_RECORD_KEY = 
"k".getBytes(StandardCharsets.UTF_8);
+
+    private static final byte[] FIRST_RECORD_VALUE = 
"alpha".getBytes(StandardCharsets.UTF_8);
+
+    private static final byte[] SECOND_RECORD_VALUE = 
"beta".getBytes(StandardCharsets.UTF_8);
+
+    private static final byte[] SECOND_RECORD_KEY = 
"k2".getBytes(StandardCharsets.UTF_8);
+
+    private static final long FIRST_RECORD_BYTES = FIRST_RECORD_KEY.length + 
FIRST_RECORD_VALUE.length;
+
+    private static final long SECOND_RECORD_BYTES = SECOND_RECORD_KEY.length + 
SECOND_RECORD_VALUE.length;
+
+    private static final String RECORD_DEMARCATOR = "\n";
+
+    private static final long FIRST_PARTITION_LAG = 4L;
+
+    private static final long SECOND_PARTITION_LAG = 9L;
+
+    private static final String BROKER_URI = "kafka://localhost";
+
+    private static final String MAX_UNCOMMITTED_TIME = "250 ms";
+
     private static final String DYNAMIC_PROPERTY_KEY_PUBLISH = 
"delivery.timeout.ms";
     private static final String DYNAMIC_PROPERTY_VALUE_PUBLISH = "60000";
     private static final String DYNAMIC_PROPERTY_KEY_CONSUME = 
"fetch.max.wait.ms";
@@ -231,6 +266,111 @@ class ConsumeKafkaTest {
         assertEquals(OptionalLong.of(5L), afterStop.get().getRecordCount());
     }
 
+    @Test
+    public void testOnTriggerRecordsBytesAndLagMetrics() throws 
InitializationException {
+        setConnectionService();
+        configureProcessor();
+        when(kafkaConnectionService.getBrokerUri()).thenReturn(BROKER_URI);
+        
when(kafkaConnectionService.getConsumerService(any())).thenReturn(kafkaConsumerService);
+
+        final ByteRecord firstRecord = new ByteRecord(TEST_TOPIC_NAME, 
FIRST_PARTITION, 0, 0L, List.of(), FIRST_RECORD_KEY, FIRST_RECORD_VALUE, 1);
+        final ByteRecord secondRecord = new ByteRecord(TEST_TOPIC_NAME, 
SECOND_PARTITION, 0, 0L, List.of(), null, SECOND_RECORD_VALUE, 1);
+        
when(kafkaConsumerService.poll(any(Duration.class))).thenReturn(List.of(firstRecord,
 secondRecord)).thenReturn(List.of());
+
+        final TopicPartitionSummary firstPartition = new 
TopicPartitionSummary(TEST_TOPIC_NAME, FIRST_PARTITION);
+        final TopicPartitionSummary secondPartition = new 
TopicPartitionSummary(TEST_TOPIC_NAME, SECOND_PARTITION);
+        
when(kafkaConsumerService.currentLag(firstPartition)).thenReturn(OptionalLong.of(FIRST_PARTITION_LAG));
+        
when(kafkaConsumerService.currentLag(secondPartition)).thenReturn(OptionalLong.of(SECOND_PARTITION_LAG));
+
+        runner.run();
+
+        final Map<String, String> firstAttributes = 
topicPartitionAttributes(TEST_TOPIC_NAME, FIRST_PARTITION);
+        final Map<String, String> secondAttributes = 
topicPartitionAttributes(TEST_TOPIC_NAME, SECOND_PARTITION);
+
+        assertEquals(1L, 
runner.getCounterValue(KafkaMetricName.RECORDS_CONSUMED.getMetricName(), 
firstAttributes));
+        assertEquals(1L, 
runner.getCounterValue(KafkaMetricName.RECORDS_CONSUMED.getMetricName(), 
secondAttributes));
+        assertEquals(FIRST_RECORD_BYTES, 
runner.getCounterValue(KafkaMetricName.BYTES_CONSUMED.getMetricName(), 
firstAttributes));
+        assertEquals(SECOND_RECORD_VALUE.length, 
runner.getCounterValue(KafkaMetricName.BYTES_CONSUMED.getMetricName(), 
secondAttributes));
+        assertEquals(2L, runner.getCounterValue("Records Received from " + 
TEST_TOPIC_NAME));
+
+        
assertFalse(runner.getGaugeValues(KafkaMetricName.RECORDS_CONSUMER_LAG.getMetricName(),
 firstAttributes).isEmpty());
+        
assertFalse(runner.getGaugeValues(KafkaMetricName.RECORDS_CONSUMER_LAG.getMetricName(),
 secondAttributes).isEmpty());
+        
assertTrue(runner.getGaugeValues(KafkaMetricName.RECORDS_CONSUMER_LAG.getMetricName(),
 firstAttributes).stream().allMatch(value -> value == FIRST_PARTITION_LAG));
+        
assertTrue(runner.getGaugeValues(KafkaMetricName.RECORDS_CONSUMER_LAG.getMetricName(),
 secondAttributes).stream().allMatch(value -> value == SECOND_PARTITION_LAG));
+        runner.assertTransferCount(ConsumeKafka.SUCCESS, 2);
+    }
+
+    @Test
+    public void testOnTriggerEmptyPollDoesNotRecordLag() throws 
InitializationException {
+        setConnectionService();
+        configureProcessor();
+        when(kafkaConnectionService.getBrokerUri()).thenReturn(BROKER_URI);
+        
when(kafkaConnectionService.getConsumerService(any())).thenReturn(kafkaConsumerService);
+        
when(kafkaConsumerService.poll(any(Duration.class))).thenReturn(List.of());
+
+        runner.run();
+
+        verify(kafkaConsumerService, never()).currentLag(any());
+        final Map<String, String> firstAttributes = 
topicPartitionAttributes(TEST_TOPIC_NAME, FIRST_PARTITION);
+        
assertTrue(runner.getGaugeValues(KafkaMetricName.RECORDS_CONSUMER_LAG.getMetricName(),
 firstAttributes).isEmpty());
+        runner.assertTransferCount(ConsumeKafka.SUCCESS, 0);
+        
assertNull(runner.getCounterValue(KafkaMetricName.RECORDS_CONSUMED.getMetricName(),
 firstAttributes));
+    }
+
+    @Test
+    public void testOnTriggerEmitsConsumptionWhenPollFailsAfterRecords() 
throws InitializationException {
+        setConnectionService();
+        configureProcessor();
+        when(kafkaConnectionService.getBrokerUri()).thenReturn(BROKER_URI);
+        
when(kafkaConnectionService.getConsumerService(any())).thenReturn(kafkaConsumerService);
+
+        final ByteRecord firstRecord = new ByteRecord(TEST_TOPIC_NAME, 
FIRST_PARTITION, 0, 0L, List.of(), FIRST_RECORD_KEY, FIRST_RECORD_VALUE, 1);
+        
when(kafkaConsumerService.poll(any(Duration.class))).thenReturn(List.of(firstRecord)).thenThrow(new
 RuntimeException("poll failed"));
+
+        runner.run();
+
+        final Map<String, String> firstAttributes = 
topicPartitionAttributes(TEST_TOPIC_NAME, FIRST_PARTITION);
+        assertEquals(1L, 
runner.getCounterValue(KafkaMetricName.RECORDS_CONSUMED.getMetricName(), 
firstAttributes));
+        assertEquals(FIRST_RECORD_BYTES, 
runner.getCounterValue(KafkaMetricName.BYTES_CONSUMED.getMetricName(), 
firstAttributes));
+        runner.assertTransferCount(ConsumeKafka.SUCCESS, 0);
+    }
+
+    @Test
+    public void testOnTriggerDemarcatorRecordsOriginalKafkaBytes() throws 
InitializationException {
+        setConnectionService();
+        configureProcessor();
+        runner.setProperty(ConsumeKafka.PROCESSING_STRATEGY, 
ProcessingStrategy.DEMARCATOR);
+        runner.setProperty(ConsumeKafka.MESSAGE_DEMARCATOR, RECORD_DEMARCATOR);
+        when(kafkaConnectionService.getBrokerUri()).thenReturn(BROKER_URI);
+        
when(kafkaConnectionService.getConsumerService(any())).thenReturn(kafkaConsumerService);
+
+        final ByteRecord firstRecord = new ByteRecord(TEST_TOPIC_NAME, 
FIRST_PARTITION, 0, 0L, new ArrayList<>(), FIRST_RECORD_KEY, 
FIRST_RECORD_VALUE, 1);
+        final ByteRecord secondRecord = new ByteRecord(TEST_TOPIC_NAME, 
FIRST_PARTITION, 1, 0L, new ArrayList<>(), SECOND_RECORD_KEY, 
SECOND_RECORD_VALUE, 1);
+        
when(kafkaConsumerService.poll(any(Duration.class))).thenReturn(List.of(firstRecord,
 secondRecord)).thenReturn(List.of());
+
+        final TopicPartitionSummary firstPartition = new 
TopicPartitionSummary(TEST_TOPIC_NAME, FIRST_PARTITION);
+        
when(kafkaConsumerService.currentLag(firstPartition)).thenReturn(OptionalLong.of(FIRST_PARTITION_LAG));
+
+        runner.run();
+
+        final Map<String, String> firstAttributes = 
topicPartitionAttributes(TEST_TOPIC_NAME, FIRST_PARTITION);
+        final long originalKafkaBytes = FIRST_RECORD_BYTES + 
SECOND_RECORD_BYTES;
+        final long bundledFlowFileBytes = FIRST_RECORD_VALUE.length
+                + RECORD_DEMARCATOR.getBytes(StandardCharsets.UTF_8).length
+                + SECOND_RECORD_VALUE.length;
+
+        assertEquals(2L, 
runner.getCounterValue(KafkaMetricName.RECORDS_CONSUMED.getMetricName(), 
firstAttributes));
+        assertEquals(originalKafkaBytes, 
runner.getCounterValue(KafkaMetricName.BYTES_CONSUMED.getMetricName(), 
firstAttributes));
+        assertNotEquals(bundledFlowFileBytes, originalKafkaBytes);
+        runner.assertTransferCount(ConsumeKafka.SUCCESS, 1);
+    }
+
+    private void configureProcessor() {
+        runner.setProperty(TOPICS, TEST_TOPIC_NAME);
+        runner.setProperty(GROUP_ID, CONSUMER_GROUP_ID);
+        runner.setProperty(ConsumeKafka.MAX_UNCOMMITTED_TIME, 
MAX_UNCOMMITTED_TIME);
+    }
+
     private void setConnectionService() throws InitializationException {
         when(kafkaConnectionService.getIdentifier()).thenReturn(SERVICE_ID);
 
@@ -239,4 +379,11 @@ class ConsumeKafkaTest {
 
         runner.setProperty(CONNECTION_SERVICE, SERVICE_ID);
     }
+
+    private static Map<String, String> topicPartitionAttributes(final String 
topic, final int partition) {
+        return Map.of(
+                KafkaFlowFileAttribute.KAFKA_TOPIC, topic,
+                KafkaFlowFileAttribute.KAFKA_PARTITION, 
Integer.toString(partition)
+        );
+    }
 }
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/OffsetTrackerTest.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/OffsetTrackerTest.java
new file mode 100644
index 00000000000..995baae8fa2
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/OffsetTrackerTest.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.kafka.processors.consumer;
+
+import org.apache.nifi.kafka.service.api.common.TopicPartitionSummary;
+import org.apache.nifi.kafka.service.api.record.ByteRecord;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class OffsetTrackerTest {
+
+    private static final String FIRST_TOPIC = "orders";
+    private static final String SECOND_TOPIC = "shipments";
+    private static final int FIRST_PARTITION = 0;
+    private static final int SECOND_PARTITION = 1;
+    private static final byte[] RECORD_KEY = 
"key".getBytes(StandardCharsets.UTF_8);
+    private static final byte[] FIRST_VALUE = 
"alpha".getBytes(StandardCharsets.UTF_8);
+    private static final byte[] SECOND_VALUE = 
"beta".getBytes(StandardCharsets.UTF_8);
+    private static final byte[] FIRST_KEY = 
"k1".getBytes(StandardCharsets.UTF_8);
+    private static final byte[] SECOND_KEY = 
"k2".getBytes(StandardCharsets.UTF_8);
+    private static final long SECOND_TOPIC_BUNDLED_COUNT = 3L;
+    private static final byte[] NEWLINE_DEMARCATOR = new byte[]{10};
+
+    @Test
+    void testUpdateAggregatesRecordsAndBytesByPartition() {
+        final OffsetTracker offsetTracker = new OffsetTracker();
+        recordConsumed(offsetTracker, newByteRecord(FIRST_TOPIC, 
FIRST_PARTITION, RECORD_KEY, FIRST_VALUE, 1));
+        recordConsumed(offsetTracker, newByteRecord(FIRST_TOPIC, 
FIRST_PARTITION, null, SECOND_VALUE, 1));
+        recordConsumed(offsetTracker, newByteRecord(FIRST_TOPIC, 
SECOND_PARTITION, RECORD_KEY, FIRST_VALUE, 1));
+        recordConsumed(offsetTracker, newByteRecord(SECOND_TOPIC, 
FIRST_PARTITION, null, SECOND_VALUE, SECOND_TOPIC_BUNDLED_COUNT));
+        offsetTracker.update(newByteRecord(FIRST_TOPIC, FIRST_PARTITION, 
RECORD_KEY, FIRST_VALUE, 1));
+        offsetTracker.update(newByteRecord(FIRST_TOPIC, FIRST_PARTITION, null, 
SECOND_VALUE, 1));
+        offsetTracker.update(newByteRecord(FIRST_TOPIC, SECOND_PARTITION, 
RECORD_KEY, FIRST_VALUE, 1));
+        offsetTracker.update(newByteRecord(SECOND_TOPIC, FIRST_PARTITION, 
null, SECOND_VALUE, SECOND_TOPIC_BUNDLED_COUNT));
+
+        final TopicPartitionSummary firstTopicFirstPartition = new 
TopicPartitionSummary(FIRST_TOPIC, FIRST_PARTITION);
+        final TopicPartitionSummary firstTopicSecondPartition = new 
TopicPartitionSummary(FIRST_TOPIC, SECOND_PARTITION);
+        final TopicPartitionSummary secondTopicFirstPartition = new 
TopicPartitionSummary(SECOND_TOPIC, FIRST_PARTITION);
+
+        final long firstTopicFirstPartitionBytes = RECORD_KEY.length + 
FIRST_VALUE.length + SECOND_VALUE.length;
+        final long firstTopicSecondPartitionBytes = RECORD_KEY.length + 
FIRST_VALUE.length;
+        final long secondTopicFirstPartitionBytes = SECOND_VALUE.length;
+        final long expectedTotalRecordSize = firstTopicFirstPartitionBytes + 
firstTopicSecondPartitionBytes + secondTopicFirstPartitionBytes;
+
+        assertEquals(2L, 
offsetTracker.getPartitionRecords().get(firstTopicFirstPartition));
+        assertEquals(1L, 
offsetTracker.getPartitionRecords().get(firstTopicSecondPartition));
+        assertEquals(SECOND_TOPIC_BUNDLED_COUNT, 
offsetTracker.getPartitionRecords().get(secondTopicFirstPartition));
+
+        assertEquals(firstTopicFirstPartitionBytes, 
offsetTracker.getPartitionBytes().get(firstTopicFirstPartition));
+        assertEquals(firstTopicSecondPartitionBytes, 
offsetTracker.getPartitionBytes().get(firstTopicSecondPartition));
+        assertEquals(secondTopicFirstPartitionBytes, 
offsetTracker.getPartitionBytes().get(secondTopicFirstPartition));
+
+        assertEquals(3L, offsetTracker.getRecordCounts().get(FIRST_TOPIC));
+        assertEquals(SECOND_TOPIC_BUNDLED_COUNT, 
offsetTracker.getRecordCounts().get(SECOND_TOPIC));
+        assertEquals(expectedTotalRecordSize, 
offsetTracker.getTotalRecordSize());
+    }
+
+    @Test
+    void testRecordConsumedKeepsOriginalBytesWhenUpdateSeesBundledRecord() {
+        final OffsetTracker offsetTracker = new OffsetTracker();
+        final ByteRecord firstRecord = newByteRecord(FIRST_TOPIC, 
FIRST_PARTITION, FIRST_KEY, FIRST_VALUE, 1);
+        final ByteRecord secondRecord = newByteRecord(FIRST_TOPIC, 
FIRST_PARTITION, SECOND_KEY, SECOND_VALUE, 1);
+        final TopicPartitionSummary topicPartition = new 
TopicPartitionSummary(FIRST_TOPIC, FIRST_PARTITION);
+        final long originalBytes = FIRST_KEY.length + FIRST_VALUE.length + 
SECOND_KEY.length + SECOND_VALUE.length;
+
+        recordConsumed(offsetTracker, firstRecord);
+        recordConsumed(offsetTracker, secondRecord);
+
+        final byte[] bundledValue = new byte[FIRST_VALUE.length + 
NEWLINE_DEMARCATOR.length + SECOND_VALUE.length];
+        System.arraycopy(FIRST_VALUE, 0, bundledValue, 0, FIRST_VALUE.length);
+        System.arraycopy(NEWLINE_DEMARCATOR, 0, bundledValue, 
FIRST_VALUE.length, NEWLINE_DEMARCATOR.length);
+        System.arraycopy(SECOND_VALUE, 0, bundledValue, FIRST_VALUE.length + 
NEWLINE_DEMARCATOR.length, SECOND_VALUE.length);
+        offsetTracker.update(newByteRecord(FIRST_TOPIC, FIRST_PARTITION, null, 
bundledValue, 2));
+
+        assertEquals(2L, 
offsetTracker.getPartitionRecords().get(topicPartition));
+        assertEquals(originalBytes, 
offsetTracker.getPartitionBytes().get(topicPartition));
+        assertEquals(bundledValue.length, offsetTracker.getTotalRecordSize());
+    }
+
+    @Test
+    void testClearRemovesPartitionAggregates() {
+        final OffsetTracker offsetTracker = new OffsetTracker();
+        recordConsumed(offsetTracker, newByteRecord(FIRST_TOPIC, 
FIRST_PARTITION, RECORD_KEY, FIRST_VALUE, 2));
+        offsetTracker.update(newByteRecord(FIRST_TOPIC, FIRST_PARTITION, 
RECORD_KEY, FIRST_VALUE, 2));
+
+        offsetTracker.clear();
+
+        assertTrue(offsetTracker.getPartitionRecords().isEmpty());
+        assertTrue(offsetTracker.getPartitionBytes().isEmpty());
+        assertTrue(offsetTracker.getRecordCounts().isEmpty());
+        assertEquals(0L, offsetTracker.getTotalRecordSize());
+    }
+
+    private static void recordConsumed(final OffsetTracker offsetTracker, 
final ByteRecord consumerRecord) {
+        offsetTracker.recordConsumed(
+                new TopicPartitionSummary(consumerRecord.getTopic(), 
consumerRecord.getPartition()),
+                consumerRecord.getBundledCount(),
+                consumedBytes(consumerRecord));
+    }
+
+    private static long consumedBytes(final ByteRecord consumerRecord) {
+        long recordSize = consumerRecord.getValue().length;
+        if (consumerRecord.getKey().isPresent()) {
+            recordSize += consumerRecord.getKey().get().length;
+        }
+        return recordSize;
+    }
+
+    private static ByteRecord newByteRecord(final String topic, final int 
partition, final byte[] key, final byte[] value, final long bundledCount) {
+        return new ByteRecord(topic, partition, 0, 0L, List.of(), key, value, 
bundledCount);
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/convert/RecordStreamKafkaMessageConverterTest.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/convert/RecordStreamKafkaMessageConverterTest.java
index 8c544769de9..506f55517da 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/convert/RecordStreamKafkaMessageConverterTest.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/convert/RecordStreamKafkaMessageConverterTest.java
@@ -17,12 +17,16 @@
 package org.apache.nifi.kafka.processors.consumer.convert;
 
 import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.kafka.processors.consumer.KafkaMetricName;
 import org.apache.nifi.kafka.processors.consumer.OffsetTracker;
 import org.apache.nifi.kafka.service.api.record.ByteRecord;
 import org.apache.nifi.kafka.shared.attribute.KafkaFlowFileAttribute;
 import org.apache.nifi.kafka.shared.property.KeyEncoding;
 import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.metrics.CommitTiming;
+import org.apache.nifi.serialization.MalformedRecordException;
+import org.apache.nifi.serialization.RecordReaderFactory;
 import org.apache.nifi.serialization.RecordSetWriterFactory;
 import org.apache.nifi.serialization.record.MockRecordParser;
 import org.apache.nifi.serialization.record.RecordFieldType;
@@ -42,6 +46,8 @@ import java.util.regex.Pattern;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -50,6 +56,11 @@ import static org.mockito.Mockito.withSettings;
 
 class RecordStreamKafkaMessageConverterTest {
 
+    private static final String FIRST_TOPIC = "topic1";
+    private static final String SECOND_TOPIC = "topic2";
+    private static final int FIRST_PARTITION = 0;
+    private static final String BROKER_URI = "brokerUri";
+
     private final RecordSetWriterFactory writerFactory = 
mock(RecordSetWriterFactory.class, 
withSettings().defaultAnswer(Answers.RETURNS_DEEP_STUBS));
     private final ComponentLog logger = mock(ComponentLog.class);
     private final ProcessSession session = mock(ProcessSession.class, 
withSettings().defaultAnswer(Answers.RETURNS_DEEP_STUBS));
@@ -78,17 +89,17 @@ class RecordStreamKafkaMessageConverterTest {
                 true,
                 offsetTracker,
                 logger,
-                "brokerUri",
-                new CreateNewFlowFileGrouping(writerFactory, logger, 
"brokerUri", true)
+                BROKER_URI,
+                new CreateNewFlowFileGrouping(writerFactory, logger, 
BROKER_URI, true)
         );
 
         // Create ByteRecords
-        final ByteRecord group1Record1 = new ByteRecord("topic1", 0, 0, 1000L, 
List.of(), null, "value1".getBytes(), 0L);
-        final ByteRecord group1Record2 = new ByteRecord("topic1", 0, 3, 500L, 
List.of(), null, "value4".getBytes(), 0L);
+        final ByteRecord group1Record1 = new ByteRecord(FIRST_TOPIC, 0, 0, 
1000L, List.of(), null, "value1".getBytes(), 0L);
+        final ByteRecord group1Record2 = new ByteRecord(FIRST_TOPIC, 0, 3, 
500L, List.of(), null, "value4".getBytes(), 0L);
 
-        final ByteRecord group2 = new ByteRecord("topic1", 1, 1, 2000L, 
List.of(), null, "value2".getBytes(), 0L);
+        final ByteRecord group2 = new ByteRecord(FIRST_TOPIC, 1, 1, 2000L, 
List.of(), null, "value2".getBytes(), 0L);
 
-        final ByteRecord group3 = new ByteRecord("topic2", 0, 2, 3000L, 
List.of(), null, "value3".getBytes(), 0L);
+        final ByteRecord group3 = new ByteRecord(SECOND_TOPIC, 0, 2, 3000L, 
List.of(), null, "value3".getBytes(), 0L);
 
         final Iterator<ByteRecord> consumerRecords = List.of(group1Record1, 
group2, group3, group1Record2).iterator();
         // Mock the session.create() and session.write() methods
@@ -109,18 +120,18 @@ class RecordStreamKafkaMessageConverterTest {
         final List<Map<String, String>> capturedAttributes = 
attributesCaptor.getAllValues();
 
         // check group1 records
-        assertEquals("topic1", 
capturedAttributes.get(0).get(KafkaFlowFileAttribute.KAFKA_TOPIC));
+        assertEquals(FIRST_TOPIC, 
capturedAttributes.get(0).get(KafkaFlowFileAttribute.KAFKA_TOPIC));
         assertEquals("0", 
capturedAttributes.get(0).get(KafkaFlowFileAttribute.KAFKA_PARTITION));
 
-        assertEquals("topic1", 
capturedAttributes.get(3).get(KafkaFlowFileAttribute.KAFKA_TOPIC));
+        assertEquals(FIRST_TOPIC, 
capturedAttributes.get(3).get(KafkaFlowFileAttribute.KAFKA_TOPIC));
         assertEquals("0", 
capturedAttributes.get(3).get(KafkaFlowFileAttribute.KAFKA_PARTITION));
 
         //check group2 records
-        assertEquals("topic1", 
capturedAttributes.get(1).get(KafkaFlowFileAttribute.KAFKA_TOPIC));
+        assertEquals(FIRST_TOPIC, 
capturedAttributes.get(1).get(KafkaFlowFileAttribute.KAFKA_TOPIC));
         assertEquals("1", 
capturedAttributes.get(1).get(KafkaFlowFileAttribute.KAFKA_PARTITION));
 
         //check group3 records
-        assertEquals("topic2", 
capturedAttributes.get(2).get(KafkaFlowFileAttribute.KAFKA_TOPIC));
+        assertEquals(SECOND_TOPIC, 
capturedAttributes.get(2).get(KafkaFlowFileAttribute.KAFKA_TOPIC));
         assertEquals("0", 
capturedAttributes.get(2).get(KafkaFlowFileAttribute.KAFKA_PARTITION));
 
         final List<String> timestamps = capturedAttributes.stream()
@@ -131,4 +142,38 @@ class RecordStreamKafkaMessageConverterTest {
         assertTrue(timestamps.contains("2000"), "Expected timestamp from 
group2");
         assertTrue(timestamps.contains("3000"), "Expected timestamp from 
group3");
     }
+
+    @Test
+    void testParseFailureIncrementsParseErrorCounterImmediately() throws 
Exception {
+        final RecordReaderFactory readerFactory = 
mock(RecordReaderFactory.class);
+        when(readerFactory.createRecordReader(any(), any(), anyLong(), any()))
+                .thenThrow(new MalformedRecordException("invalid record"));
+
+        final FlowFile flowFile = new MockFlowFile(1);
+        when(session.create()).thenReturn(flowFile);
+        when(session.putAllAttributes(any(FlowFile.class), 
any())).thenReturn(flowFile);
+        
when(session.write(any(FlowFile.class))).thenReturn(mock(OutputStream.class));
+
+        final RecordStreamKafkaMessageConverter converter = new 
RecordStreamKafkaMessageConverter(
+                readerFactory,
+                writerFactory,
+                value -> new String(value, StandardCharsets.UTF_8),
+                Pattern.compile(".*"),
+                KeyEncoding.UTF8,
+                true,
+                offsetTracker,
+                logger,
+                BROKER_URI,
+                new CreateNewFlowFileGrouping(writerFactory, logger, 
BROKER_URI, true)
+        );
+
+        final ByteRecord consumerRecord = new ByteRecord(FIRST_TOPIC, 
FIRST_PARTITION, 0, 1000L, List.of(), null, 
"invalid".getBytes(StandardCharsets.UTF_8), 1L);
+        converter.toFlowFiles(session, List.of(consumerRecord).iterator());
+
+        
verify(session).adjustCounter(eq(KafkaMetricName.RECORDS_PARSED_ERRORS.getMetricName()),
 eq(1L),
+                eq(Map.of(
+                        KafkaFlowFileAttribute.KAFKA_TOPIC, FIRST_TOPIC,
+                        KafkaFlowFileAttribute.KAFKA_PARTITION, 
Integer.toString(FIRST_PARTITION))),
+                eq(CommitTiming.NOW));
+    }
 }

Reply via email to