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 2bdb2c901d3 NIFI-16380 Add Records Metrics to ConsumeKinesis (#11712)
2bdb2c901d3 is described below

commit 2bdb2c901d307407a618e037529d8d4641571f59
Author: David Handermann <[email protected]>
AuthorDate: Fri Sep 25 09:21:24 2026 -0500

    NIFI-16380 Add Records Metrics to ConsumeKinesis (#11712)
---
 .../processors/aws/kinesis/ConsumeKinesis.java     |  51 ++++
 .../processors/aws/kinesis/KinesisMetricName.java  |  37 +++
 .../processors/aws/kinesis/ConsumeKinesisTest.java | 257 ++++++++++++++++++++-
 3 files changed, 333 insertions(+), 12 deletions(-)

diff --git 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/main/java/org/apache/nifi/processors/aws/kinesis/ConsumeKinesis.java
 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/main/java/org/apache/nifi/processors/aws/kinesis/ConsumeKinesis.java
index 83fdb502e46..56fa367de72 100644
--- 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/main/java/org/apache/nifi/processors/aws/kinesis/ConsumeKinesis.java
+++ 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/main/java/org/apache/nifi/processors/aws/kinesis/ConsumeKinesis.java
@@ -46,6 +46,7 @@ import org.apache.nifi.processor.ProcessSession;
 import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.processor.exception.ProcessException;
 import org.apache.nifi.processor.io.OutputStreamCallback;
+import org.apache.nifi.processor.metrics.CommitTiming;
 import org.apache.nifi.processor.util.StandardValidators;
 import 
org.apache.nifi.processors.aws.credentials.provider.AwsCredentialsProviderService;
 import org.apache.nifi.processors.aws.region.RegionUtil;
@@ -589,6 +590,11 @@ public class ConsumeKinesis extends AbstractProcessor 
implements BacklogReportin
             consumed = consumeRecords(claimedShards);
             final List<ShardFetchResult> accepted = 
discardRelinquishedResults(consumed, claimedShards);
 
+            for (final ShardFetchResult result : accepted) {
+                shardMillisBehindLatest.put(result.shardId(), 
result.millisBehindLatest());
+            }
+            recordBehindMetrics(session, ownedShardIds);
+
             if (accepted.isEmpty()) {
                 consumerClient.releaseShards(claimedShards);
                 context.yield();
@@ -895,6 +901,7 @@ public class ConsumeKinesis extends AbstractProcessor 
implements BacklogReportin
             if (processingStrategy == ProcessingStrategy.FLOW_FILE) {
                 final BatchAccumulator batch = new BatchAccumulator();
                 for (final List<ShardFetchResult> shardResults : 
resultsByShard.values()) {
+                    recordConsumptionMetrics(session, shardResults);
                     for (final ShardFetchResult result : shardResults) {
                         batch.updateMillisBehind(result.millisBehindLatest());
                         for (final UserRecord record : result.records()) {
@@ -910,6 +917,7 @@ public class ConsumeKinesis extends AbstractProcessor 
implements BacklogReportin
                 for (final Map.Entry<String, List<ShardFetchResult>> entry : 
resultsByShard.entrySet()) {
                     final BatchAccumulator batch = new BatchAccumulator();
                     batch.setLastShardId(entry.getKey());
+                    recordConsumptionMetrics(session, entry.getValue());
                     for (final ShardFetchResult result : entry.getValue()) {
                         batch.updateMillisBehind(result.millisBehindLatest());
                         batch.updateSequenceRange(result);
@@ -946,6 +954,46 @@ public class ConsumeKinesis extends AbstractProcessor 
implements BacklogReportin
         return new WriteResult(produced, parseFailures, totalRecordCount, 
totalBytesConsumed, maxMillisBehind);
     }
 
+    private void recordConsumptionMetrics(final ProcessSession session, final 
List<ShardFetchResult> shardResults) {
+        long recordCount = 0;
+        long bytesConsumed = 0;
+        String shardId = null;
+        for (final ShardFetchResult result : shardResults) {
+            shardId = result.shardId();
+            for (final UserRecord record : result.records()) {
+                recordCount++;
+                bytesConsumed += record.data().length;
+            }
+        }
+
+        if (recordCount == 0) {
+            return;
+        }
+
+        final Map<String, String> attributes = getMetricAttributes(streamName, 
shardId);
+        
session.adjustCounter(KinesisMetricName.RECORDS_CONSUMED.getMetricName(), 
recordCount, attributes, CommitTiming.NOW);
+        
session.adjustCounter(KinesisMetricName.BYTES_CONSUMED.getMetricName(), 
bytesConsumed, attributes, CommitTiming.NOW);
+    }
+
+    private void recordBehindMetrics(final ProcessSession session, final 
Set<String> ownedShardIds) {
+        for (final String shardId : ownedShardIds) {
+            final Long millisBehind = shardMillisBehindLatest.get(shardId);
+            // Kinesis uses -1 when millisBehindLatest is not provided
+            if (millisBehind == null || millisBehind < 0) {
+                continue;
+            }
+            final Map<String, String> metricAttributes = 
getMetricAttributes(streamName, shardId);
+            
session.recordGauge(KinesisMetricName.CONSUMER_MILLISECONDS_BEHIND.getMetricName(),
 millisBehind, metricAttributes, CommitTiming.NOW);
+        }
+    }
+
+    private static Map<String, String> getMetricAttributes(final String 
streamName, final String shardId) {
+        return Map.of(
+                ATTR_STREAM_NAME, streamName,
+                ATTR_SHARD_ID, shardId
+        );
+    }
+
     private void writeFlowFilePerRecord(final ProcessSession session, final 
List<ShardFetchResult> results,
                                         final String streamName, final 
BatchAccumulator batch, final List<FlowFile> output) {
         for (final ShardFetchResult result : results) {
@@ -1360,6 +1408,9 @@ public class ConsumeKinesis extends AbstractProcessor 
implements BacklogReportin
                 }
                 flowFile = session.putAllAttributes(flowFile, attributes);
                 parseFailureOutput.add(flowFile);
+
+                final Map<String, String> metricAttributes = 
getMetricAttributes(streamName, record.shardId());
+                
session.adjustCounter(KinesisMetricName.RECORDS_PARSED_ERRORS.getMetricName(), 
1, metricAttributes, CommitTiming.NOW);
             } catch (final Exception e) {
                 session.remove(flowFile);
                 throw e;
diff --git 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/main/java/org/apache/nifi/processors/aws/kinesis/KinesisMetricName.java
 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/main/java/org/apache/nifi/processors/aws/kinesis/KinesisMetricName.java
new file mode 100644
index 00000000000..cec30ea32d5
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/main/java/org/apache/nifi/processors/aws/kinesis/KinesisMetricName.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.processors.aws.kinesis;
+
+/**
+ * Metric names for ConsumeKinesis counters and gauges.
+ */
+public enum KinesisMetricName {
+    BYTES_CONSUMED("kinesis.bytes.consumed"),
+    RECORDS_CONSUMED("kinesis.records.consumed"),
+    CONSUMER_MILLISECONDS_BEHIND("kinesis.consumer.milliseconds.behind"),
+    RECORDS_PARSED_ERRORS("kinesis.records.parsed.errors");
+
+    private final String metricName;
+
+    KinesisMetricName(final String metricName) {
+        this.metricName = metricName;
+    }
+
+    public String getMetricName() {
+        return metricName;
+    }
+}
diff --git 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/test/java/org/apache/nifi/processors/aws/kinesis/ConsumeKinesisTest.java
 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/test/java/org/apache/nifi/processors/aws/kinesis/ConsumeKinesisTest.java
index ef538ed5fd1..67f263b2e84 100644
--- 
a/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/test/java/org/apache/nifi/processors/aws/kinesis/ConsumeKinesisTest.java
+++ 
b/nifi-extension-bundles/nifi-aws-bundle/nifi-aws-kinesis/src/test/java/org/apache/nifi/processors/aws/kinesis/ConsumeKinesisTest.java
@@ -45,6 +45,7 @@ import java.util.Set;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 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.anyString;
 import static org.mockito.Mockito.mock;
@@ -52,6 +53,32 @@ import static org.mockito.Mockito.when;
 
 class ConsumeKinesisTest {
 
+    private static final String TEST_STREAM_NAME = "test-stream";
+
+    private static final String DEFAULT_SHARD_ID = "shardId-000000000001";
+
+    private static final String FIRST_SHARD_ID = "shard-A";
+
+    private static final String SECOND_SHARD_ID = "shard-B";
+
+    private static final String FIRST_JSON_RECORD = "{\"id\":1}";
+
+    private static final String SECOND_JSON_RECORD = "{\"id\":2}";
+
+    private static final String THIRD_JSON_RECORD = "{\"id\":3}";
+
+    private static final String FOURTH_JSON_RECORD = "{\"id\":4}";
+
+    private static final String FIRST_FLOW_FILE_RECORD = "record-one";
+
+    private static final String SECOND_FLOW_FILE_RECORD = "record-two";
+
+    private static final long FIRST_SHARD_BEHIND_MS = 1000L;
+
+    private static final long SECOND_SHARD_BEHIND_MS = 2500L;
+
+    private static final long FLOW_FILE_BEHIND_MS = 5000L;
+
     private TestRunner runner;
 
     @BeforeEach
@@ -79,7 +106,7 @@ class ConsumeKinesisTest {
         targetRunner.enableControllerService(credentialsService);
 
         targetRunner.setProperty(ConsumeKinesis.APPLICATION_NAME, "test-app");
-        targetRunner.setProperty(ConsumeKinesis.STREAM_NAME, "test-stream");
+        targetRunner.setProperty(ConsumeKinesis.STREAM_NAME, TEST_STREAM_NAME);
         
targetRunner.setProperty(ConsumeKinesis.AWS_CREDENTIALS_PROVIDER_SERVICE, 
"creds");
     }
 
@@ -168,6 +195,10 @@ class ConsumeKinesisTest {
         runner.assertTransferCount(ConsumeKinesis.REL_PARSE_FAILURE, 0);
         final MockFlowFile success = 
runner.getFlowFilesForRelationship(ConsumeKinesis.REL_SUCCESS).getFirst();
         success.assertAttributeEquals("record.count", "3");
+
+        final Map<String, String> attributes = 
getMetricAttributes(DEFAULT_SHARD_ID);
+        assertEquals(3L, 
runner.getCounterValue(KinesisMetricName.RECORDS_CONSUMED.getMetricName(), 
attributes));
+        assertEquals(List.of(0.0), 
runner.getGaugeValues(KinesisMetricName.CONSUMER_MILLISECONDS_BEHIND.getMetricName(),
 attributes));
     }
 
     @Test
@@ -205,6 +236,10 @@ class ConsumeKinesisTest {
         assertTrue(failureSequences.contains("1"));
         assertTrue(failureSequences.contains("3"));
         assertTrue(failureSequences.contains("5"));
+
+        final Map<String, String> attributes = 
getMetricAttributes(DEFAULT_SHARD_ID);
+        assertEquals(5L, 
runner.getCounterValue(KinesisMetricName.RECORDS_CONSUMED.getMetricName(), 
attributes));
+        assertEquals(3L, 
runner.getCounterValue(KinesisMetricName.RECORDS_PARSED_ERRORS.getMetricName(), 
attributes));
     }
 
     @Test
@@ -218,6 +253,10 @@ class ConsumeKinesisTest {
 
         runner.assertTransferCount(ConsumeKinesis.REL_SUCCESS, 0);
         runner.assertTransferCount(ConsumeKinesis.REL_PARSE_FAILURE, 3);
+
+        final Map<String, String> attributes = 
getMetricAttributes(DEFAULT_SHARD_ID);
+        assertEquals(3L, 
runner.getCounterValue(KinesisMetricName.RECORDS_CONSUMED.getMetricName(), 
attributes));
+        assertEquals(3L, 
runner.getCounterValue(KinesisMetricName.RECORDS_PARSED_ERRORS.getMetricName(), 
attributes));
     }
 
     @Test
@@ -234,7 +273,7 @@ class ConsumeKinesisTest {
         final List<MockFlowFile> flowFiles = 
runner.getFlowFilesForRelationship(ConsumeKinesis.REL_SUCCESS);
         for (final MockFlowFile flowFile : flowFiles) {
             flowFile.assertAttributeEquals("record.count", "1");
-            flowFile.assertAttributeEquals(ConsumeKinesis.ATTR_STREAM_NAME, 
"test-stream");
+            flowFile.assertAttributeEquals(ConsumeKinesis.ATTR_STREAM_NAME, 
TEST_STREAM_NAME);
             flowFile.assertAttributeEquals(ConsumeKinesis.ATTR_SHARD_ID, 
"shardId-000000000001");
             final String firstSequence = 
flowFile.getAttribute(ConsumeKinesis.ATTR_FIRST_SEQUENCE);
             final String lastSequence = 
flowFile.getAttribute(ConsumeKinesis.ATTR_LAST_SEQUENCE);
@@ -284,10 +323,10 @@ class ConsumeKinesisTest {
 
     @Test
     void testMultipleShardsNoDataLoss() throws Exception {
-        final ShardFetchResult shard1Result = new ShardFetchResult("shard-A",
-                List.of(testRecord("10", "{\"id\":1}"), testRecord("20", 
"{\"id\":2}")), 0L);
-        final ShardFetchResult shard2Result = new ShardFetchResult("shard-B",
-                List.of(testRecord("30", "{\"id\":3}"), testRecord("40", 
"{\"id\":4}")), 0L);
+        final ShardFetchResult shard1Result = new 
ShardFetchResult(FIRST_SHARD_ID,
+                List.of(testRecord("10", FIRST_JSON_RECORD), testRecord("20", 
SECOND_JSON_RECORD)), FIRST_SHARD_BEHIND_MS);
+        final ShardFetchResult shard2Result = new 
ShardFetchResult(SECOND_SHARD_ID,
+                List.of(testRecord("30", THIRD_JSON_RECORD), testRecord("40", 
FOURTH_JSON_RECORD)), SECOND_SHARD_BEHIND_MS);
 
         triggerWithResults(List.of(shard1Result, shard2Result), "RECORD");
 
@@ -301,8 +340,138 @@ class ConsumeKinesisTest {
             
shardsSeen.add(flowFile.getAttribute(ConsumeKinesis.ATTR_SHARD_ID));
             totalRecords += 
Long.parseLong(flowFile.getAttribute("record.count"));
         }
-        assertEquals(Set.of("shard-A", "shard-B"), shardsSeen);
+        assertEquals(Set.of(FIRST_SHARD_ID, SECOND_SHARD_ID), shardsSeen);
         assertEquals(4, totalRecords);
+
+        final Map<String, String> firstAttributes = 
getMetricAttributes(FIRST_SHARD_ID);
+        final Map<String, String> secondAttributes = 
getMetricAttributes(SECOND_SHARD_ID);
+        final long firstShardBytes = payloadBytes(FIRST_JSON_RECORD) + 
payloadBytes(SECOND_JSON_RECORD);
+        final long secondShardBytes = payloadBytes(THIRD_JSON_RECORD) + 
payloadBytes(FOURTH_JSON_RECORD);
+
+        assertEquals(2L, 
runner.getCounterValue(KinesisMetricName.RECORDS_CONSUMED.getMetricName(), 
firstAttributes));
+        assertEquals(2L, 
runner.getCounterValue(KinesisMetricName.RECORDS_CONSUMED.getMetricName(), 
secondAttributes));
+        assertEquals(firstShardBytes, 
runner.getCounterValue(KinesisMetricName.BYTES_CONSUMED.getMetricName(), 
firstAttributes));
+        assertEquals(secondShardBytes, 
runner.getCounterValue(KinesisMetricName.BYTES_CONSUMED.getMetricName(), 
secondAttributes));
+        assertEquals(List.of((double) FIRST_SHARD_BEHIND_MS),
+                
runner.getGaugeValues(KinesisMetricName.CONSUMER_MILLISECONDS_BEHIND.getMetricName(),
 firstAttributes));
+        assertEquals(List.of((double) SECOND_SHARD_BEHIND_MS),
+                
runner.getGaugeValues(KinesisMetricName.CONSUMER_MILLISECONDS_BEHIND.getMetricName(),
 secondAttributes));
+    }
+
+    @Test
+    void testEmptyConsumeDoesNotRecordMetrics() throws Exception {
+        final KinesisShardManager mockShardManager = 
buildShardManager(DEFAULT_SHARD_ID);
+        final TestableConsumeKinesis processor = new 
TestableConsumeKinesis(mockShardManager, List.of());
+        runner = TestRunners.newTestRunner(processor);
+
+        setCommonProperties();
+        runner.setProperty(ConsumeKinesis.PROCESSING_STRATEGY, "FLOW_FILE");
+        runner.setProperty(ConsumeKinesis.CONSUMER_TYPE, "SHARED_THROUGHPUT");
+
+        runner.run();
+
+        final Map<String, String> attributes = 
getMetricAttributes(DEFAULT_SHARD_ID);
+        
assertNull(runner.getCounterValue(KinesisMetricName.RECORDS_CONSUMED.getMetricName(),
 attributes));
+        
assertNull(runner.getCounterValue(KinesisMetricName.BYTES_CONSUMED.getMetricName(),
 attributes));
+        
assertTrue(runner.getGaugeValues(KinesisMetricName.CONSUMER_MILLISECONDS_BEHIND.getMetricName(),
 attributes).isEmpty());
+        runner.assertTransferCount(ConsumeKinesis.REL_SUCCESS, 0);
+    }
+
+    @Test
+    void testEmptyPollRecordsBehindGaugeWhenStillLagging() throws Exception {
+        final Map<String, Long> shardLag = new LinkedHashMap<>();
+        shardLag.put(DEFAULT_SHARD_ID, FLOW_FILE_BEHIND_MS);
+        final KinesisShardManager mockShardManager = 
buildShardManager(DEFAULT_SHARD_ID);
+        final LagReportingConsumeKinesis processor = new 
LagReportingConsumeKinesis(mockShardManager, shardLag);
+        runner = TestRunners.newTestRunner(processor);
+
+        setCommonProperties();
+        runner.setProperty(ConsumeKinesis.PROCESSING_STRATEGY, "FLOW_FILE");
+        runner.setProperty(ConsumeKinesis.CONSUMER_TYPE, "SHARED_THROUGHPUT");
+
+        runner.run(1, false, true);
+
+        final Map<String, String> attributes = 
getMetricAttributes(DEFAULT_SHARD_ID);
+        
assertNull(runner.getCounterValue(KinesisMetricName.RECORDS_CONSUMED.getMetricName(),
 attributes));
+        
assertNull(runner.getCounterValue(KinesisMetricName.BYTES_CONSUMED.getMetricName(),
 attributes));
+        assertEquals(List.of((double) FLOW_FILE_BEHIND_MS),
+                
runner.getGaugeValues(KinesisMetricName.CONSUMER_MILLISECONDS_BEHIND.getMetricName(),
 attributes));
+        runner.assertTransferCount(ConsumeKinesis.REL_SUCCESS, 0);
+    }
+
+    @Test
+    void testEmptyPollUpdatesBehindGaugeToCaughtUp() throws Exception {
+        final List<UserRecord> records = List.of(
+                testRecord("1", FIRST_FLOW_FILE_RECORD),
+                testRecord("2", SECOND_FLOW_FILE_RECORD));
+        final KinesisShardManager mockShardManager = 
buildShardManager(DEFAULT_SHARD_ID);
+        final ShardFetchResult firstFetch = new 
ShardFetchResult(DEFAULT_SHARD_ID, records, FLOW_FILE_BEHIND_MS);
+        final SequentialFetchConsumeKinesis processor = new 
SequentialFetchConsumeKinesis(mockShardManager, firstFetch, 0L);
+        runner = TestRunners.newTestRunner(processor);
+
+        setCommonProperties();
+        runner.setProperty(ConsumeKinesis.PROCESSING_STRATEGY, "FLOW_FILE");
+        runner.setProperty(ConsumeKinesis.CONSUMER_TYPE, "SHARED_THROUGHPUT");
+
+        runner.run(2, false, true);
+
+        final Map<String, String> attributes = 
getMetricAttributes(DEFAULT_SHARD_ID);
+        final long expectedBytes = payloadBytes(FIRST_FLOW_FILE_RECORD) + 
payloadBytes(SECOND_FLOW_FILE_RECORD);
+        assertEquals(2L, 
runner.getCounterValue(KinesisMetricName.RECORDS_CONSUMED.getMetricName(), 
attributes));
+        assertEquals(expectedBytes, 
runner.getCounterValue(KinesisMetricName.BYTES_CONSUMED.getMetricName(), 
attributes));
+        assertEquals(List.of((double) FLOW_FILE_BEHIND_MS, 0.0),
+                
runner.getGaugeValues(KinesisMetricName.CONSUMER_MILLISECONDS_BEHIND.getMetricName(),
 attributes));
+        runner.assertTransferCount(ConsumeKinesis.REL_SUCCESS, 2);
+    }
+
+    @Test
+    void testAbsentMillisBehindDoesNotRecordGauge() throws Exception {
+        final List<UserRecord> records = List.of(testRecord("1", 
FIRST_FLOW_FILE_RECORD));
+        final KinesisShardManager mockShardManager = 
buildShardManager(DEFAULT_SHARD_ID);
+        final ShardFetchResult fetchResult = new 
ShardFetchResult(DEFAULT_SHARD_ID, records, -1L);
+        final TestableConsumeKinesis processor = new 
TestableConsumeKinesis(mockShardManager, fetchResult);
+        runner = TestRunners.newTestRunner(processor);
+
+        setCommonProperties();
+        runner.setProperty(ConsumeKinesis.PROCESSING_STRATEGY, "FLOW_FILE");
+        runner.setProperty(ConsumeKinesis.CONSUMER_TYPE, "SHARED_THROUGHPUT");
+
+        runner.run();
+
+        runner.assertTransferCount(ConsumeKinesis.REL_SUCCESS, 1);
+
+        final Map<String, String> attributes = 
getMetricAttributes(DEFAULT_SHARD_ID);
+        assertEquals(1L, 
runner.getCounterValue(KinesisMetricName.RECORDS_CONSUMED.getMetricName(), 
attributes));
+        assertEquals(payloadBytes(FIRST_FLOW_FILE_RECORD),
+                
runner.getCounterValue(KinesisMetricName.BYTES_CONSUMED.getMetricName(), 
attributes));
+        
assertTrue(runner.getGaugeValues(KinesisMetricName.CONSUMER_MILLISECONDS_BEHIND.getMetricName(),
 attributes).isEmpty());
+    }
+
+    @Test
+    void testFlowFileStrategyRecordsBytesAndBehindMetrics() throws Exception {
+        final List<UserRecord> records = List.of(
+                testRecord("1", FIRST_FLOW_FILE_RECORD),
+                testRecord("2", SECOND_FLOW_FILE_RECORD));
+        final KinesisShardManager mockShardManager = 
buildShardManager(DEFAULT_SHARD_ID);
+        final ShardFetchResult fetchResult = new 
ShardFetchResult(DEFAULT_SHARD_ID, records, FLOW_FILE_BEHIND_MS);
+        final TestableConsumeKinesis processor = new 
TestableConsumeKinesis(mockShardManager, fetchResult);
+        runner = TestRunners.newTestRunner(processor);
+
+        setCommonProperties();
+        runner.setProperty(ConsumeKinesis.PROCESSING_STRATEGY, "FLOW_FILE");
+        runner.setProperty(ConsumeKinesis.CONSUMER_TYPE, "SHARED_THROUGHPUT");
+
+        runner.run();
+
+        runner.assertTransferCount(ConsumeKinesis.REL_SUCCESS, 2);
+
+        final Map<String, String> attributes = 
getMetricAttributes(DEFAULT_SHARD_ID);
+        final long expectedBytes = payloadBytes(FIRST_FLOW_FILE_RECORD) + 
payloadBytes(SECOND_FLOW_FILE_RECORD);
+        assertEquals(2L, 
runner.getCounterValue(KinesisMetricName.RECORDS_CONSUMED.getMetricName(), 
attributes));
+        assertEquals(expectedBytes, 
runner.getCounterValue(KinesisMetricName.BYTES_CONSUMED.getMetricName(), 
attributes));
+        assertEquals(2L, runner.getCounterValue("Records Consumed"));
+        assertEquals(List.of((double) FLOW_FILE_BEHIND_MS),
+                
runner.getGaugeValues(KinesisMetricName.CONSUMER_MILLISECONDS_BEHIND.getMetricName(),
 attributes));
     }
 
     @Test
@@ -413,8 +582,8 @@ class ConsumeKinesisTest {
     }
 
     private void triggerWithRecords(final List<UserRecord> records) throws 
Exception {
-        final KinesisShardManager mockShardManager = 
buildShardManager("shardId-000000000001");
-        final ShardFetchResult fetchResult = new 
ShardFetchResult("shardId-000000000001", records, 0L);
+        final KinesisShardManager mockShardManager = 
buildShardManager(DEFAULT_SHARD_ID);
+        final ShardFetchResult fetchResult = new 
ShardFetchResult(DEFAULT_SHARD_ID, records, 0L);
         final TestableConsumeKinesis processor = new 
TestableConsumeKinesis(mockShardManager, fetchResult);
         runner = TestRunners.newTestRunner(processor);
 
@@ -456,11 +625,15 @@ class ConsumeKinesisTest {
         failure.assertContentEquals(expectedFailureContent);
         failure.assertAttributeEquals(ConsumeKinesis.ATTR_FIRST_SEQUENCE, 
expectedFailureSequence);
         
assertNotNull(failure.getAttribute(ConsumeKinesis.ATTR_RECORD_ERROR_MESSAGE));
+
+        final Map<String, String> attributes = 
getMetricAttributes(DEFAULT_SHARD_ID);
+        assertEquals(3L, 
runner.getCounterValue(KinesisMetricName.RECORDS_CONSUMED.getMetricName(), 
attributes));
+        assertEquals(1L, 
runner.getCounterValue(KinesisMetricName.RECORDS_PARSED_ERRORS.getMetricName(), 
attributes));
     }
 
     private void triggerWithOutputStrategy(final List<UserRecord> records, 
final String outputStrategy) throws Exception {
-        final KinesisShardManager mockShardManager = 
buildShardManager("shardId-000000000001");
-        final ShardFetchResult fetchResult = new 
ShardFetchResult("shardId-000000000001", records, 0L);
+        final KinesisShardManager mockShardManager = 
buildShardManager(DEFAULT_SHARD_ID);
+        final ShardFetchResult fetchResult = new 
ShardFetchResult(DEFAULT_SHARD_ID, records, 0L);
         final TestableConsumeKinesis processor = new 
TestableConsumeKinesis(mockShardManager, fetchResult);
         runner = TestRunners.newTestRunner(processor);
 
@@ -538,7 +711,7 @@ class ConsumeKinesisTest {
 
     private static UserRecord testRecord(final String sequenceNumber, final 
String data, final Instant arrivalTimestamp) {
         return new UserRecord(
-                "shardId-000000000001",
+                DEFAULT_SHARD_ID,
                 sequenceNumber,
                 0,
                 "pk-" + sequenceNumber,
@@ -546,6 +719,17 @@ class ConsumeKinesisTest {
                 arrivalTimestamp);
     }
 
+    private static Map<String, String> getMetricAttributes(final String 
shardId) {
+        return Map.of(
+                ConsumeKinesis.ATTR_STREAM_NAME, TEST_STREAM_NAME,
+                ConsumeKinesis.ATTR_SHARD_ID, shardId
+        );
+    }
+
+    private static long payloadBytes(final String payload) {
+        return payload.getBytes(StandardCharsets.UTF_8).length;
+    }
+
     static class TestableConsumeKinesis extends ConsumeKinesis {
         private final KinesisShardManager mockShardManager;
         private final List<ShardFetchResult> preloadedResults;
@@ -576,6 +760,55 @@ class ConsumeKinesisTest {
         }
     }
 
+    static class SequentialFetchConsumeKinesis extends ConsumeKinesis {
+        private final KinesisShardManager mockShardManager;
+        private final ShardFetchResult firstFetch;
+        private final long emptyPollMillisBehind;
+
+        SequentialFetchConsumeKinesis(final KinesisShardManager 
mockShardManager, final ShardFetchResult firstFetch,
+                final long emptyPollMillisBehind) {
+            this.mockShardManager = mockShardManager;
+            this.firstFetch = firstFetch;
+            this.emptyPollMillisBehind = emptyPollMillisBehind;
+        }
+
+        @Override
+        protected KinesisShardManager createShardManager(final KinesisClient 
kinesisClient, final DynamoDbClient dynamoDbClient,
+                final ComponentLog logger, final String checkpointTableName, 
final String streamName) {
+            return mockShardManager;
+        }
+
+        @Override
+        protected KinesisConsumerClient createConsumerClient(final 
KinesisClient kinesisClient, final ComponentLog logger,
+                final boolean efoMode) {
+            return new 
SequentialFetchConsumerClient(mock(KinesisClient.class), logger, firstFetch, 
emptyPollMillisBehind);
+        }
+    }
+
+    static class SequentialFetchConsumerClient extends StubConsumerClient {
+        private final ShardFetchResult firstFetch;
+        private final long emptyPollMillisBehind;
+        private int fetchCycles;
+
+        SequentialFetchConsumerClient(final KinesisClient kinesisClient, final 
ComponentLog logger,
+                final ShardFetchResult firstFetch, final long 
emptyPollMillisBehind) {
+            super(kinesisClient, logger);
+            this.firstFetch = firstFetch;
+            this.emptyPollMillisBehind = emptyPollMillisBehind;
+        }
+
+        @Override
+        void startFetches(final List<Shard> shards, final String streamName, 
final int batchSize,
+                final String initialStreamPosition, final KinesisShardManager 
shardManager) {
+            if (fetchCycles++ == 0) {
+                enqueueResult(firstFetch);
+                recordShardLag(firstFetch.shardId(), 
firstFetch.millisBehindLatest());
+            } else {
+                recordShardLag(firstFetch.shardId(), emptyPollMillisBehind);
+            }
+        }
+    }
+
     static class LagReportingConsumeKinesis extends ConsumeKinesis {
         private final KinesisShardManager mockShardManager;
         private final Map<String, Long> shardLag;

Reply via email to