From c98f9ab48eb043330cbb535f89997204190d2ad5 Mon Sep 17 00:00:00 2001 From: Ahmed Kamel Date: Fri, 13 Feb 2026 18:59:58 +0000 Subject: [PATCH] Add propertiesSupplier support to EmfMetricLoggingPublisher Enable `EmfMetricLoggingPublisher` to accept an optional `Supplier>` for enriching EMF records with custom properties at publish time - Add `propertiesSupplier` field and builder method to `EmfMetricLoggingPublisher.Builder` - Add `propertiesSupplier` field, accessor, and builder setter to `EmfMetricConfiguration`, defaulting to empty map when null - Add `resolveProperties()` to `MetricEmfConverter` which invokes the supplier from config once per convert call, handling null returns and exceptions gracefully - Add `writeCustomProperties()` to `MetricEmfConverter` which writes properties first in the EMF JSON so `_aws`, dimensions, and metrics overwrite any key collisions Closes #6595 --- ...nCloudWatchEMFMetricPublisher-33792c9.json | 6 + .../emf/EmfMetricLoggingPublisher.java | 25 ++++ .../emf/internal/EmfMetricConfiguration.java | 16 ++ .../emf/internal/MetricEmfConverter.java | 38 ++++- .../emf/EmfMetricLoggingPublisherTest.java | 98 ++++++++++++ .../emf/internal/MetricEmfConverterTest.java | 140 +++++++++++++++++- 6 files changed, 317 insertions(+), 6 deletions(-) create mode 100644 .changes/next-release/feature-AmazonCloudWatchEMFMetricPublisher-33792c9.json diff --git a/.changes/next-release/feature-AmazonCloudWatchEMFMetricPublisher-33792c9.json b/.changes/next-release/feature-AmazonCloudWatchEMFMetricPublisher-33792c9.json new file mode 100644 index 000000000000..d6aa0fee0b52 --- /dev/null +++ b/.changes/next-release/feature-AmazonCloudWatchEMFMetricPublisher-33792c9.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "Amazon CloudWatch EMF Metric Publisher", + "contributor": "humanzz", + "description": "Add `propertiesSupplier` to `EmfMetricLoggingPublisher.Builder`, enabling users to enrich EMF records with custom key-value properties that are searchable in CloudWatch Logs Insights. See [#6595](https://github.com/aws/aws-sdk-java-v2/issues/6595)." +} diff --git a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java index 07bba8a97442..4bd12cd68135 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java @@ -20,6 +20,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.function.Supplier; import software.amazon.awssdk.annotations.Immutable; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; @@ -82,6 +84,7 @@ private EmfMetricLoggingPublisher(Builder builder) { .dimensions(builder.dimensions) .metricLevel(builder.metricLevel) .metricCategories(builder.metricCategories) + .propertiesSupplier(builder.propertiesSupplier) .build(); this.metricConverter = new MetricEmfConverter(config); @@ -123,6 +126,7 @@ public static final class Builder { private Collection> dimensions; private Collection metricCategories; private MetricLevel metricLevel; + private Supplier> propertiesSupplier; private Builder() { } @@ -217,6 +221,27 @@ public Builder metricLevel(MetricLevel metricLevel) { } + /** + * Configure a supplier of custom properties to include in each EMF record. + * The supplier is invoked on each {@link #publish(MetricCollection)} call, + * and the returned map entries are written as top-level key-value pairs + * in the EMF JSON output. These appear as searchable fields in + * CloudWatch Logs Insights. + * + *

Keys that collide with reserved EMF fields ({@code _aws}), configured + * dimension names, or reported metric names are silently skipped. + * + *

If this is not specified, no custom properties are added. + * + * @param propertiesSupplier a supplier returning a map of property names to values, + * or {@code null} to disable custom properties + * @return this builder + */ + public Builder propertiesSupplier(Supplier> propertiesSupplier) { + this.propertiesSupplier = propertiesSupplier; + return this; + } + /** * Build a {@link EmfMetricLoggingPublisher} using the configuration currently configured on this publisher. */ diff --git a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java index 739cf0b0e4ca..a28417d53763 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java @@ -18,7 +18,9 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -43,6 +45,7 @@ public final class EmfMetricConfiguration { private final Set> dimensions; private final Collection metricCategories; private final MetricLevel metricLevel; + private final Supplier> propertiesSupplier; private EmfMetricConfiguration(Builder builder) { this.namespace = builder.namespace == null ? DEFAULT_NAMESPACE : builder.namespace; @@ -50,6 +53,9 @@ private EmfMetricConfiguration(Builder builder) { this.dimensions = builder.dimensions == null ? DEFAULT_DIMENSIONS : new HashSet<>(builder.dimensions); this.metricCategories = builder.metricCategories == null ? DEFAULT_CATEGORIES : new HashSet<>(builder.metricCategories); this.metricLevel = builder.metricLevel == null ? DEFAULT_METRIC_LEVEL : builder.metricLevel; + this.propertiesSupplier = builder.propertiesSupplier == null + ? Collections::emptyMap + : builder.propertiesSupplier; } @@ -59,6 +65,7 @@ public static class Builder { private Collection> dimensions; private Collection metricCategories; private MetricLevel metricLevel; + private Supplier> propertiesSupplier; public Builder namespace(String namespace) { this.namespace = namespace; @@ -85,6 +92,11 @@ public Builder metricLevel(MetricLevel metricLevel) { return this; } + public Builder propertiesSupplier(Supplier> propertiesSupplier) { + this.propertiesSupplier = propertiesSupplier; + return this; + } + public EmfMetricConfiguration build() { return new EmfMetricConfiguration(this); } @@ -110,6 +122,10 @@ public MetricLevel metricLevel() { return metricLevel; } + public Supplier> propertiesSupplier() { + return propertiesSupplier; + } + private String resolveLogGroupName(Builder builder) { return builder.logGroupName != null ? builder.logGroupName : SystemSettingUtils.resolveEnvironmentVariable("AWS_LAMBDA_LOG_GROUP_NAME").orElse(null); diff --git a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java index 3ab16d3b0878..34515c27f1bb 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java @@ -19,12 +19,14 @@ import java.time.Clock; import java.time.Duration; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; +import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.annotations.SdkTestInternalApi; import software.amazon.awssdk.metrics.MetricCategory; @@ -66,12 +68,14 @@ public class MetricEmfConverter { private final EmfMetricConfiguration config; private final boolean metricCategoriesContainsAll; private final Clock clock; + private final Supplier> propertiesSupplier; @SdkTestInternalApi public MetricEmfConverter(EmfMetricConfiguration config, Clock clock) { this.config = config; this.clock = clock; this.metricCategoriesContainsAll = config.metricCategories().contains(MetricCategory.ALL); + this.propertiesSupplier = config.propertiesSupplier(); } public MetricEmfConverter(EmfMetricConfiguration config) { @@ -136,7 +140,18 @@ public List convertMetricCollectionToEmf(MetricCollection metricCollecti } } - return createEmfStrings(aggregatedMetrics); + Map properties = resolveProperties(); + return createEmfStrings(aggregatedMetrics, properties); + } + + private Map resolveProperties() { + try { + Map result = propertiesSupplier.get(); + return result == null ? Collections.emptyMap() : result; + } catch (Exception e) { + logger.warn(() -> "Properties supplier threw an exception, publishing without custom properties", e); + return Collections.emptyMap(); + } } /** @@ -188,7 +203,8 @@ private void processAndWriteValue(JsonWriter jsonWriter, MetricRecord mRecord } } - private List createEmfStrings(Map, List>> aggregatedMetrics) { + private List createEmfStrings(Map, List>> aggregatedMetrics, + Map properties) { List emfStrings = new ArrayList<>(); Map, List>> currentMetricBatch = new HashMap<>(); @@ -204,24 +220,26 @@ private List createEmfStrings(Map, List>> a } if (currentMetricBatch.size() == MAX_METRIC_NUM) { - emfStrings.add(createEmfString(currentMetricBatch)); + emfStrings.add(createEmfString(currentMetricBatch, properties)); currentMetricBatch = new HashMap<>(); } currentMetricBatch.put(metric, records); } - emfStrings.add(createEmfString(currentMetricBatch)); + emfStrings.add(createEmfString(currentMetricBatch, properties)); return emfStrings; } - private String createEmfString(Map, List>> metrics) { + private String createEmfString(Map, List>> metrics, + Map properties) { JsonWriter jsonWriter = JsonWriter.create(); jsonWriter.writeStartObject(); + writeCustomProperties(jsonWriter, properties); writeAwsObject(jsonWriter, metrics.keySet()); writeMetricValues(jsonWriter, metrics); @@ -231,6 +249,16 @@ private String createEmfString(Map, List>> metrics) } + private void writeCustomProperties(JsonWriter jsonWriter, Map properties) { + for (Map.Entry entry : properties.entrySet()) { + jsonWriter.writeFieldName(entry.getKey()); + jsonWriter.writeValue(entry.getValue()); + } + } + + + + private void writeAwsObject(JsonWriter jsonWriter, Set> metricNames) { jsonWriter.writeFieldName("_aws"); jsonWriter.writeStartObject(); diff --git a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java index f3da12abfb4c..fb713e1d406b 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java +++ b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java @@ -18,6 +18,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.logging.log4j.Level; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import software.amazon.awssdk.http.HttpMetric; @@ -97,4 +103,96 @@ void Publish_multipleMetrics() { assertThat(loggedEvents()).hasSize(2); } + @Test + void publish_propertiesSupplierThrowsException_publishesWithoutCustomProperties() { + EmfMetricLoggingPublisher publisher = publisherBuilder + .logGroupName("/aws/lambda/emfMetricTest") + .propertiesSupplier(() -> { throw new RuntimeException("supplier failed"); }) + .build(); + + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + publisher.publish(metricCollector.collect()); + + // Should have: 1 warning about supplier + 1 EMF info log + boolean hasWarning = loggedEvents().stream() + .anyMatch(e -> e.getLevel() == Level.WARN + && e.getMessage().getFormattedMessage().contains("Properties supplier threw an exception")); + assertThat(hasWarning).isTrue(); + + boolean hasEmfOutput = loggedEvents().stream() + .anyMatch(e -> e.getLevel() == Level.INFO + && e.getMessage().getFormattedMessage().contains("\"_aws\":{")); + assertThat(hasEmfOutput).isTrue(); + + // EMF output should not contain any custom properties + String emfLog = loggedEvents().stream() + .filter(e -> e.getLevel() == Level.INFO + && e.getMessage().getFormattedMessage().contains("\"_aws\":{")) + .findFirst().get().getMessage().getFormattedMessage(); + assertThat(emfLog).contains("\"AvailableConcurrency\":5"); + } + + @Test + void publish_propertiesSupplierReturnsNull_publishesWithoutCustomProperties() { + EmfMetricLoggingPublisher publisher = publisherBuilder + .logGroupName("/aws/lambda/emfMetricTest") + .propertiesSupplier(() -> null) + .build(); + + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + publisher.publish(metricCollector.collect()); + + // Should have EMF output without custom properties + boolean hasEmfOutput = loggedEvents().stream() + .anyMatch(e -> e.getLevel() == Level.INFO + && e.getMessage().getFormattedMessage().contains("\"_aws\":{")); + assertThat(hasEmfOutput).isTrue(); + + String emfLog = loggedEvents().stream() + .filter(e -> e.getLevel() == Level.INFO + && e.getMessage().getFormattedMessage().contains("\"_aws\":{")) + .findFirst().get().getMessage().getFormattedMessage(); + assertThat(emfLog).contains("\"AvailableConcurrency\":5"); + // No warning should be logged for null return + boolean hasWarning = loggedEvents().stream() + .anyMatch(e -> e.getLevel() == Level.WARN); + assertThat(hasWarning).isFalse(); + } + + @Test + void publish_statefulSupplier_eachPublishUsesCurrentMap() { + AtomicInteger counter = new AtomicInteger(0); + EmfMetricLoggingPublisher publisher = publisherBuilder + .logGroupName("/aws/lambda/emfMetricTest") + .propertiesSupplier(() -> { + int count = counter.incrementAndGet(); + Map map = new HashMap(); + map.put("InvocationCount", String.valueOf(count)); + return map; + }) + .build(); + + // First publish + MetricCollector mc1 = MetricCollector.create("test1"); + mc1.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + publisher.publish(mc1.collect()); + + // Second publish + MetricCollector mc2 = MetricCollector.create("test2"); + mc2.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 10); + publisher.publish(mc2.collect()); + + // Collect all EMF info logs + List emfLogs = loggedEvents().stream() + .filter(e -> e.getLevel() == Level.INFO + && e.getMessage().getFormattedMessage().contains("\"_aws\":{")) + .map(e -> e.getMessage().getFormattedMessage()) + .collect(java.util.stream.Collectors.toList()); + + assertThat(emfLogs).hasSize(2); + assertThat(emfLogs.get(0)).contains("\"InvocationCount\":\"1\""); + assertThat(emfLogs.get(1)).contains("\"InvocationCount\":\"2\""); + } } diff --git a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java index a01af0f96320..2c83317752b0 100644 --- a/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java +++ b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java @@ -26,7 +26,9 @@ import java.time.Clock; import java.time.Instant; import java.time.ZoneOffset; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -250,4 +252,140 @@ void ConvertMetricCollectionToEMF_longValueShouldSucceed() { + "\"CloudWatchMetrics\":[{\"Namespace\":\"AwsSdk/JavaSdk2\",\"Dimensions\":[[]]," + "\"Metrics\":[{\"Name\":\"TestMetric\"}]}]},\"TestMetric\":42}"); } -} \ No newline at end of file + + @Test + void convertMetricCollectionToEmf_withCustomProperties_propertiesAppearInOutput() { + Map properties = new HashMap(); + properties.put("RequestId", "abc-123"); + properties.put("FunctionName", "myLambda"); + + MetricEmfConverter converter = converterWithProperties(properties); + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + List emfLogs = converter.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).hasSize(1); + String emfLog = emfLogs.get(0); + assertThat(emfLog).contains("\"RequestId\":\"abc-123\""); + assertThat(emfLog).contains("\"FunctionName\":\"myLambda\""); + assertThat(emfLog).contains("\"AvailableConcurrency\":5"); + assertThat(emfLog).contains("\"_aws\":{"); + } + + @Test + void convertMetricCollectionToEmf_noProperties_identicalToCurrentBehavior() { + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + List emfLogs = metricEmfConverterDefault.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).containsOnly( + "{\"_aws\":{\"Timestamp\":12345678,\"LogGroupName\":\"my_log_group_name\"," + + "\"CloudWatchMetrics\":[{\"Namespace\":\"AwsSdk/JavaSdk2\",\"Dimensions\":[[]]," + + "\"Metrics\":[{\"Name\":\"AvailableConcurrency\"}]}]},\"AvailableConcurrency\":5}"); + } + + @Test + void convertMetricCollectionToEmf_emptyProperties_noExtraKeys() { + MetricEmfConverter converter = converterWithProperties(new HashMap()); + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + List emfLogs = converter.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).hasSize(1); + String emfLog = emfLogs.get(0); + assertThat(emfLog).doesNotContain("\"RequestId\""); + assertThat(emfLog).doesNotContain("\"FunctionName\""); + } + + @Test + void convertMetricCollectionToEmf_propertyKeyMatchesAwsKey_awsObjectPreserved() { + Map properties = new HashMap(); + properties.put("_aws", "should-be-overwritten"); + + MetricEmfConverter converter = converterWithProperties(properties); + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + List emfLogs = converter.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).hasSize(1); + String emfLog = emfLogs.get(0); + assertThat(emfLog).contains("\"_aws\":{\"Timestamp\":"); + assertThat(emfLog).contains("\"CloudWatchMetrics\":"); + } + + @Test + void convertMetricCollectionToEmf_propertyKeyMatchesDimensionName_dimensionPreserved() { + Map properties = new HashMap(); + properties.put("OperationName", "should-be-overwritten"); + + MetricEmfConverter converter = converterWithProperties(properties); + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(CoreMetric.OPERATION_NAME, "GetObject"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + List emfLogs = converter.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).hasSize(1); + String emfLog = emfLogs.get(0); + assertThat(emfLog).contains("\"OperationName\":\"GetObject\""); + int customIndex = emfLog.indexOf("\"OperationName\":\"should-be-overwritten\""); + int realIndex = emfLog.indexOf("\"OperationName\":\"GetObject\""); + assertThat(customIndex).isGreaterThanOrEqualTo(0); + assertThat(realIndex).isGreaterThan(customIndex); + } + + @Test + void convertMetricCollectionToEmf_propertyKeyMatchesMetricName_metricPreserved() { + Map properties = new HashMap(); + properties.put("AvailableConcurrency", "should-be-overwritten"); + + MetricEmfConverter converter = converterWithProperties(properties); + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + List emfLogs = converter.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).hasSize(1); + String emfLog = emfLogs.get(0); + assertThat(emfLog).contains("\"AvailableConcurrency\":5"); + int customIndex = emfLog.indexOf("\"AvailableConcurrency\":\"should-be-overwritten\""); + int realIndex = emfLog.indexOf("\"AvailableConcurrency\":5"); + assertThat(customIndex).isGreaterThanOrEqualTo(0); + assertThat(realIndex).isGreaterThan(customIndex); + } + + @Test + void convertMetricCollectionToEmf_batchedRecords_allContainCustomProperties() { + Map properties = new HashMap(); + properties.put("RequestId", "batch-test-123"); + properties.put("TraceId", "trace-abc"); + + MetricEmfConverter converter = converterWithProperties(properties); + MetricCollector metricCollector = MetricCollector.create("test"); + for (int i = 0; i < 220; i++) { + metricCollector.reportMetric( + SdkMetric.create("cp_batch_metric_" + i, Integer.class, MetricLevel.INFO, MetricCategory.CORE), i); + } + + List emfLogs = converter.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).hasSize(3); + for (String emfLog : emfLogs) { + assertThat(emfLog).contains("\"RequestId\":\"batch-test-123\""); + assertThat(emfLog).contains("\"TraceId\":\"trace-abc\""); + assertThat(emfLog).contains("\"_aws\":{"); + } + } + + private MetricEmfConverter converterWithProperties(Map properties) { + EmfMetricConfiguration config = new EmfMetricConfiguration.Builder() + .logGroupName("my_log_group_name") + .propertiesSupplier(() -> properties) + .build(); + return new MetricEmfConverter(config, fixedClock); + } +}