From b5e4c925d034f7b7e21aed88d40def8b154471c1 Mon Sep 17 00:00:00 2001 From: Lukas Franz Date: Tue, 18 Jun 2024 23:52:49 +0000 Subject: [PATCH] Added Statistic Set Metric Type --- .../emf/logger/MetricsLogger.java | 164 ++++++++++++++---- .../emf/model/AggregationType.java | 33 ++++ .../cloudwatchlogs/emf/model/Metric.java | 46 ++--- .../emf/model/MetricDefinition.java | 33 +++- .../emf/model/MetricDirective.java | 45 +++-- .../emf/model/MetricsContext.java | 164 +++++++++++++++--- .../emf/model/StatisticSet.java | 107 ++++++++++++ .../cloudwatchlogs/emf/model/Statistics.java | 90 ++++++++++ .../cloudwatchlogs/emf/util/Validator.java | 67 ++++++- .../emf/logger/MetricsLoggerTest.java | 18 ++ .../emf/model/MetricDefinitionTest.java | 13 +- .../emf/model/MetricDirectiveTest.java | 51 +++++- .../emf/model/MetricsContextTest.java | 80 ++++++++- .../emf/model/StatisticSetTest.java | 158 +++++++++++++++++ 14 files changed, 967 insertions(+), 102 deletions(-) create mode 100644 src/main/java/software/amazon/cloudwatchlogs/emf/model/AggregationType.java create mode 100644 src/main/java/software/amazon/cloudwatchlogs/emf/model/StatisticSet.java create mode 100644 src/main/java/software/amazon/cloudwatchlogs/emf/model/Statistics.java create mode 100644 src/test/java/software/amazon/cloudwatchlogs/emf/model/StatisticSetTest.java diff --git a/src/main/java/software/amazon/cloudwatchlogs/emf/logger/MetricsLogger.java b/src/main/java/software/amazon/cloudwatchlogs/emf/logger/MetricsLogger.java index 00c955f7..9d29c0a9 100644 --- a/src/main/java/software/amazon/cloudwatchlogs/emf/logger/MetricsLogger.java +++ b/src/main/java/software/amazon/cloudwatchlogs/emf/logger/MetricsLogger.java @@ -30,7 +30,9 @@ import software.amazon.cloudwatchlogs.emf.exception.InvalidMetricException; import software.amazon.cloudwatchlogs.emf.exception.InvalidNamespaceException; import software.amazon.cloudwatchlogs.emf.exception.InvalidTimestampException; +import software.amazon.cloudwatchlogs.emf.model.AggregationType; import software.amazon.cloudwatchlogs.emf.model.DimensionSet; +import software.amazon.cloudwatchlogs.emf.model.Metric; import software.amazon.cloudwatchlogs.emf.model.MetricsContext; import software.amazon.cloudwatchlogs.emf.model.StorageResolution; import software.amazon.cloudwatchlogs.emf.model.Unit; @@ -45,6 +47,7 @@ public class MetricsLogger { private MetricsContext context; private CompletableFuture environmentFuture; private EnvironmentProvider environmentProvider; + @Getter @Setter private volatile AggregationType defaultAggregationType = AggregationType.LIST; /** * This lock is used to create an internal sync context for flush() method in multi-threaded * situations. Flush() acquires write lock, other methods (accessing mutable shared data with @@ -191,6 +194,7 @@ public MetricsLogger resetDimensions(boolean useDefault) { * @param value is the value of the metric * @param unit is the unit of the metric value * @param storageResolution is the resolution of the metric + * @param aggregationType is the aggregation type of the metric * @see CloudWatch * High Resolution Metrics @@ -198,11 +202,15 @@ public MetricsLogger resetDimensions(boolean useDefault) { * @throws InvalidMetricException if the metric is invalid */ public MetricsLogger putMetric( - String key, double value, Unit unit, StorageResolution storageResolution) + String key, + double value, + Unit unit, + StorageResolution storageResolution, + AggregationType aggregationType) throws InvalidMetricException { rwl.readLock().lock(); try { - this.context.putMetric(key, value, unit, storageResolution); + this.context.putMetric(key, value, unit, storageResolution, aggregationType); return this; } finally { rwl.readLock().unlock(); @@ -210,55 +218,153 @@ public MetricsLogger putMetric( } /** - * Put a metric value. This value will be emitted to CloudWatch Metrics asynchronously and does - * not contribute to your account TPS limits. The value will also be available in your - * CloudWatch Logs * - * @param key is the name of the metric - * @param value is the value of the metric - * @param storageResolution is the resolution of the metric - * @see CloudWatch - * High Resolution Metrics - * @return the current logger - * @throws InvalidMetricException if the metric is invalid + * + * + * + * @see MetricsLogger#putMetric(String key, double value, Unit unit, StorageResolution + * storageResolution, AggregationType aggregationType) putMetric(String key, double value, + * Unit unit, StorageResolution storageResolution, AggregationType aggregationType) */ public MetricsLogger putMetric(String key, double value, StorageResolution storageResolution) throws InvalidMetricException { - this.putMetric(key, value, Unit.NONE, storageResolution); + this.putMetric(key, value, Unit.NONE, storageResolution, this.defaultAggregationType); return this; } /** - * Put a metric value. This value will be emitted to CloudWatch Metrics asynchronously and does - * not contribute to your account TPS limits. The value will also be available in your - * CloudWatch Logs * - * @param key is the name of the metric - * @param value is the value of the metric - * @param unit is the unit of the metric value - * @return the current logger - * @throws InvalidMetricException if the metric is invalid + * + * + * + * @see MetricsLogger#putMetric(String key, double value, Unit unit, StorageResolution + * storageResolution, AggregationType aggregationType) putMetric(String key, double value, + * Unit unit, StorageResolution storageResolution, AggregationType aggregationType) */ public MetricsLogger putMetric(String key, double value, Unit unit) throws InvalidMetricException { - this.putMetric(key, value, unit, StorageResolution.STANDARD); + this.putMetric(key, value, unit, StorageResolution.STANDARD, this.defaultAggregationType); return this; } /** - * Put a metric value. This value will be emitted to CloudWatch Metrics asynchronously and does - * not contribute to your account TPS limits. The value will also be available in your - * CloudWatch Logs + * + * + * + * + * @see MetricsLogger#putMetric(String key, double value, Unit unit, StorageResolution + * storageResolution, AggregationType aggregationType) putMetric(String key, double value, + * Unit unit, StorageResolution storageResolution, AggregationType aggregationType) + */ + public MetricsLogger putMetric(String key, double value) throws InvalidMetricException { + this.putMetric( + key, value, Unit.NONE, StorageResolution.STANDARD, this.defaultAggregationType); + return this; + } + + /** + * + * + * + * + * @see MetricsLogger#putMetric(String key, double value, Unit unit, StorageResolution + * storageResolution, AggregationType aggregationType) putMetric(String key, double value, + * Unit unit, StorageResolution storageResolution, AggregationType aggregationType) + */ + public MetricsLogger putMetric( + String key, double value, Unit unit, StorageResolution storageResolution) + throws InvalidMetricException { + this.putMetric(key, value, Unit.NONE, storageResolution, this.defaultAggregationType); + return this; + } + + /** + * + * + * + * + * @see MetricsLogger#putMetric(String key, double value, Unit unit, StorageResolution + * storageResolution, AggregationType aggregationType) putMetric(String key, double value, + * Unit unit, StorageResolution storageResolution, AggregationType aggregationType) + */ + public MetricsLogger putMetric( + String key, + double value, + StorageResolution storageResolution, + AggregationType aggregationType) + throws InvalidMetricException { + this.putMetric(key, value, Unit.NONE, storageResolution, aggregationType); + return this; + } + + /** + * + * + * + * + * @see MetricsLogger#putMetric(String key, double value, Unit unit, StorageResolution + * storageResolution, AggregationType aggregationType) putMetric(String key, double value, + * Unit unit, StorageResolution storageResolution, AggregationType aggregationType) + */ + public MetricsLogger putMetric( + String key, double value, Unit unit, AggregationType aggregationType) + throws InvalidMetricException { + this.putMetric(key, value, unit, StorageResolution.STANDARD, aggregationType); + return this; + } + + /** + * + * + * + * + * @see MetricsLogger#putMetric(String key, double value, Unit unit, StorageResolution + * storageResolution, AggregationType aggregationType) putMetric(String key, double value, + * Unit unit, StorageResolution storageResolution, AggregationType aggregationType) + */ + public MetricsLogger putMetric(String key, double value, AggregationType aggregationType) + throws InvalidMetricException { + this.putMetric(key, value, Unit.NONE, StorageResolution.STANDARD, aggregationType); + return this; + } + + /** + * Set a metric value, if a metric already has the same key it will be overwitten. This value + * will be emitted to CloudWatch Metrics asynchronously and does not contribute to your account + * TPS limits. The value will also be available in your CloudWatch Logs * * @param key the name of the metric * @param value the value of the metric * @return the current logger * @throws InvalidMetricException if the metric is invalid */ - public MetricsLogger putMetric(String key, double value) throws InvalidMetricException { - this.putMetric(key, value, Unit.NONE, StorageResolution.STANDARD); - return this; + public MetricsLogger setMetric(String key, Metric value) throws InvalidMetricException { + try { + this.context.setMetric(key, value); + return this; + } finally { + rwl.readLock().unlock(); + } } /** diff --git a/src/main/java/software/amazon/cloudwatchlogs/emf/model/AggregationType.java b/src/main/java/software/amazon/cloudwatchlogs/emf/model/AggregationType.java new file mode 100644 index 00000000..9b6826dd --- /dev/null +++ b/src/main/java/software/amazon/cloudwatchlogs/emf/model/AggregationType.java @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed 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 software.amazon.cloudwatchlogs.emf.model; + +public enum AggregationType { + LIST(0), + STATISTIC_SET(1), + UNKNOWN_TO_SDK_VERSION(-1); + + private final int value; + + AggregationType(final int newValue) { + value = newValue; + } + + public int getValue() { + return this.value; + } +} diff --git a/src/main/java/software/amazon/cloudwatchlogs/emf/model/Metric.java b/src/main/java/software/amazon/cloudwatchlogs/emf/model/Metric.java index 9923fb92..868f46f4 100644 --- a/src/main/java/software/amazon/cloudwatchlogs/emf/model/Metric.java +++ b/src/main/java/software/amazon/cloudwatchlogs/emf/model/Metric.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.util.LinkedList; import lombok.AccessLevel; import lombok.Getter; import lombok.NonNull; @@ -41,7 +42,7 @@ public abstract class Metric { @JsonProperty("Unit") @JsonSerialize(using = UnitSerializer.class) @JsonDeserialize(using = UnitDeserializer.class) - protected Unit unit; + protected Unit unit = Unit.NONE; @JsonProperty("StorageResolution") @JsonInclude( @@ -49,7 +50,7 @@ public abstract class Metric { valueFilter = StorageResolutionFilter.class) // Do not serialize when valueFilter is true @JsonSerialize(using = StorageResolutionSerializer.class) - protected StorageResolution storageResolution; + protected StorageResolution storageResolution = StorageResolution.STANDARD; @JsonIgnore @Getter protected V values; @@ -58,23 +59,20 @@ protected Object getFormattedValues() { return this.getValues(); } - /** - * Creates a Metric with the first {@code size} values of the current metric - * - * @param size the maximum size of the returned metric's values - * @return a Metric with the first {@code size} values of the current metric. - */ - protected abstract Metric getMetricValuesUnderSize(int size); + /** @return true if the values of this metric are valid, false otherwise. */ + public abstract boolean hasValidValues(); + + /** @return true if the values of this metric are oversized for CloudWatch Logs */ + protected abstract boolean isOversized(); /** - * Creates a Metric all metrics after the first {@code size} values of the current metric. If - * there are less than {@code size} values, null is returned. + * Creates a list of new Metrics based off the values in this metric split in so that they are + * small enough that CWL will not drop the message values * - * @param size the maximum size of the returned metric's values - * @return a Metric with the all metrics after the first {@code size} values of the current - * metric. If there are less than {@code size} values, null is returned. + * @return a list of metrics based off of the values of this metric that aren't too large for + * CWL */ - protected abstract Metric getMetricValuesOverSize(int size); + protected abstract LinkedList serialize(); public abstract static class MetricBuilder> extends Metric { @@ -92,7 +90,7 @@ public abstract static class MetricBuilder> ext * * @return the built metric */ - abstract Metric build(); + abstract Metric build(); protected T name(@NonNull String name) { this.name = name; @@ -109,16 +107,24 @@ public T storageResolution(StorageResolution storageResolution) { return getThis(); } - protected Metric getMetricValuesOverSize(int size) { - return build().getMetricValuesOverSize(size); + @Override + public boolean hasValidValues() { + return build().hasValidValues(); } - protected Metric getMetricValuesUnderSize(int size) { - return build().getMetricValuesUnderSize(size); + @Override + protected LinkedList serialize() { + return build().serialize(); } + @Override protected Object getFormattedValues() { return build().getFormattedValues(); } + + @Override + protected boolean isOversized() { + return build().isOversized(); + } } } diff --git a/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricDefinition.java b/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricDefinition.java index 7db9a14f..9612562f 100644 --- a/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricDefinition.java +++ b/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricDefinition.java @@ -17,8 +17,10 @@ package software.amazon.cloudwatchlogs.emf.model; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; import lombok.NonNull; +import software.amazon.cloudwatchlogs.emf.Constants; /** Represents the MetricDefinition of the EMF schema. */ public class MetricDefinition extends Metric> { @@ -41,8 +43,19 @@ private MetricDefinition( } @Override - protected Metric getMetricValuesUnderSize(int size) { - List subList = values.subList(0, Math.min(values.size(), size)); + protected LinkedList serialize() { + LinkedList metrics = new LinkedList<>(); + MetricDefinition metric = this; + while (metric != null) { + metrics.add(metric.getFirstMetricBatch(Constants.MAX_DATAPOINTS_PER_METRIC)); + metric = metric.getRemainingMetricBatch(Constants.MAX_DATAPOINTS_PER_METRIC); + } + + return metrics; + } + + private MetricDefinition getFirstMetricBatch(int batchSize) { + List subList = values.subList(0, Math.min(values.size(), batchSize)); MetricDefinition metric = MetricDefinition.builder() .unit(unit) @@ -53,12 +66,11 @@ protected Metric getMetricValuesUnderSize(int size) { return metric; } - @Override - protected Metric getMetricValuesOverSize(int size) { - if (size > values.size()) { + private MetricDefinition getRemainingMetricBatch(int batchSize) { + if (batchSize > values.size()) { return null; } - List subList = values.subList(size, values.size()); + List subList = values.subList(batchSize, values.size()); MetricDefinition metric = MetricDefinition.builder() .name(name) @@ -69,6 +81,10 @@ protected Metric getMetricValuesOverSize(int size) { return metric; } + protected boolean isOversized() { + return values.size() > Constants.MAX_DATAPOINTS_PER_METRIC; + } + public static MetricDefinitionBuilder builder() { return new MetricDefinitionBuilder(); } @@ -82,6 +98,11 @@ protected Object getFormattedValues() { return values.size() == 1 ? values.get(0) : values; } + @Override + public boolean hasValidValues() { + return values != null && !values.isEmpty(); + } + public static class MetricDefinitionBuilder extends Metric.MetricBuilder, MetricDefinitionBuilder> { diff --git a/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricDirective.java b/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricDirective.java index 09446e58..f8951a01 100644 --- a/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricDirective.java +++ b/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricDirective.java @@ -69,31 +69,42 @@ void putDimensionSet(DimensionSet dimensionSet) { // Helper method for testing putMetric() void putMetric(String key, double value) { - putMetric(key, value, Unit.NONE, StorageResolution.STANDARD); + putMetric(key, value, Unit.NONE, StorageResolution.STANDARD, AggregationType.LIST); } // Helper method for testing putMetric() void putMetric(String key, double value, Unit unit) { - putMetric(key, value, unit, StorageResolution.STANDARD); + putMetric(key, value, unit, StorageResolution.STANDARD, AggregationType.LIST); } // Helper method for testing serialization void putMetric(String key, double value, StorageResolution storageResolution) { - putMetric(key, value, Unit.NONE, storageResolution); + putMetric(key, value, Unit.NONE, storageResolution, AggregationType.LIST); } - void putMetric(String key, double value, Unit unit, StorageResolution storageResolution) { + void putMetric( + String key, + double value, + Unit unit, + StorageResolution storageResolution, + AggregationType aggregationType) { metrics.compute( key, (k, v) -> { if (v == null) { - MetricDefinition.MetricDefinitionBuilder builder = - MetricDefinition.builder() - .name(k) - .unit(unit) - .storageResolution(storageResolution) - .addValue(value); - return builder; + Metric.MetricBuilder builder; + switch (aggregationType) { + case STATISTIC_SET: + builder = StatisticSet.builder(); + break; + case LIST: + default: + builder = MetricDefinition.builder(); + } + return builder.name(k) + .unit(unit) + .storageResolution(storageResolution) + .addValue(value); } else if (v instanceof Metric.MetricBuilder) { ((Metric.MetricBuilder) v).addValue(value); return v; @@ -106,6 +117,18 @@ void putMetric(String key, double value, Unit unit, StorageResolution storageRes }); } + /** + * Sets a metric to the given value. If a metric with the same name already exists, it will be + * overwritten. + * + * @param key the name of the metric + * @param value the value of the metric + */ + void setMetric(String key, Metric value) { + value.setName(key); + metrics.put(key, value); + } + @JsonProperty("Metrics") Collection getAllMetrics() { return metrics.values(); diff --git a/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsContext.java b/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsContext.java index 7edf6652..53a3154f 100644 --- a/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsContext.java +++ b/src/main/java/software/amazon/cloudwatchlogs/emf/model/MetricsContext.java @@ -21,9 +21,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.PriorityQueue; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import lombok.Getter; @@ -43,6 +43,8 @@ public class MetricsContext { private MetricDirective metricDirective; private final Map metricNameAndResolutionMap = new ConcurrentHashMap<>(); + private final Map metricNameAndAggregationMap = + new ConcurrentHashMap<>(); public MetricsContext() { this(new RootNode()); @@ -112,6 +114,41 @@ public boolean hasDefaultDimensions() { return !getDefaultDimensions().getDimensionKeys().isEmpty(); } + /** + * Add a metric measurement to the context. Multiple calls using the same key will be stored as + * an array of scalar values. + * + *
{@code
+     * metricContext.putMetric("Latency", 100, Unit.MILLISECONDS, StorageResolution.HIGH, AggregationType.LIST)
+     * }
+ * + * @param key Name of the metric + * @param value Value of the metric + * @param unit The unit of the metric + * @param storageResolution The resolution of the metric + * @param aggregationType The aggregation type of the metric + * @throws InvalidMetricException if the metric is invalid + */ + public void putMetric( + String key, + double value, + Unit unit, + StorageResolution storageResolution, + AggregationType aggregationType) + throws InvalidMetricException { + Validator.validateMetric( + key, + value, + unit, + storageResolution, + aggregationType, + metricNameAndResolutionMap, + metricNameAndAggregationMap); + metricDirective.putMetric(key, value, unit, storageResolution, aggregationType); + metricNameAndResolutionMap.put(key, storageResolution); + metricNameAndAggregationMap.put(key, aggregationType); + } + /** * Add a metric measurement to the context. Multiple calls using the same key will be stored as * an array of scalar values. @@ -128,10 +165,9 @@ public boolean hasDefaultDimensions() { */ public void putMetric(String key, double value, Unit unit, StorageResolution storageResolution) throws InvalidMetricException { - Validator.validateMetric(key, value, unit, storageResolution, metricNameAndResolutionMap); - metricDirective.putMetric(key, value, unit, storageResolution); - metricNameAndResolutionMap.put(key, storageResolution); + putMetric(key, value, unit, storageResolution, AggregationType.LIST); } + /** * Add a metric measurement to the context with a storage resolution but without a unit. * Multiple calls using the same key will be stored as an array of scalar values. @@ -147,7 +183,7 @@ public void putMetric(String key, double value, Unit unit, StorageResolution sto */ public void putMetric(String key, double value, StorageResolution storageResolution) throws InvalidMetricException { - putMetric(key, value, Unit.NONE, storageResolution); + putMetric(key, value, Unit.NONE, storageResolution, AggregationType.LIST); } /** @@ -164,7 +200,67 @@ public void putMetric(String key, double value, StorageResolution storageResolut * @throws InvalidMetricException if the metric is invalid */ public void putMetric(String key, double value, Unit unit) throws InvalidMetricException { - putMetric(key, value, unit, StorageResolution.STANDARD); + putMetric(key, value, unit, StorageResolution.STANDARD, AggregationType.LIST); + } + + /** + * Add a metric measurement to the context without a unit Multiple calls using the same key will + * be stored as an array of scalar values. + * + *
{@code
+     * metricContext.putMetric("Count", 10, AggregationType.LIST)
+     * }
+ * + * @param key Name of the metric + * @param value Value of the metric + * @param aggregationType The aggregation type of the metric + * @throws InvalidMetricException if the metric is invalid + */ + public void putMetric(String key, double value, AggregationType aggregationType) + throws InvalidMetricException { + putMetric(key, value, Unit.NONE, StorageResolution.STANDARD, aggregationType); + } + + /** + * Add a metric measurement to the context with a storage resolution but without a unit. + * Multiple calls using the same key will be stored as an array of scalar values. + * + *
{@code
+     * metricContext.putMetric("Latency", 100, StorageResolution.HIGH, AggregationType.LIST)
+     * }
+ * + * @param key Name of the metric + * @param value Value of the metric + * @param storageResolution The resolution of the metric + * @param aggregationType The aggregation type of the metric + * @throws InvalidMetricException if the metric is invalid + */ + public void putMetric( + String key, + double value, + StorageResolution storageResolution, + AggregationType aggregationType) + throws InvalidMetricException { + putMetric(key, value, Unit.NONE, storageResolution, aggregationType); + } + + /** + * Add a metric measurement to the context without a storage resolution. Multiple calls using + * the same key will be stored as an array of scalar values. + * + *
{@code
+     * metricContext.putMetric("Latency", 100, Unit.MILLISECONDS, AggregationType.LIST)
+     * }
+ * + * @param key Name of the metric + * @param value Value of the metric + * @param unit The unit of the metric + * @param aggregationType The aggregation type of the metric + * @throws InvalidMetricException if the metric is invalid + */ + public void putMetric(String key, double value, Unit unit, AggregationType aggregationType) + throws InvalidMetricException { + putMetric(key, value, unit, StorageResolution.STANDARD, aggregationType); } /** @@ -180,7 +276,26 @@ public void putMetric(String key, double value, Unit unit) throws InvalidMetricE * @throws InvalidMetricException if the metric is invalid */ public void putMetric(String key, double value) throws InvalidMetricException { - putMetric(key, value, Unit.NONE, StorageResolution.STANDARD); + putMetric(key, value, Unit.NONE, StorageResolution.STANDARD, AggregationType.LIST); + } + + /** + * Set a metric measurement to the context overwriting any existing metric that may be + * associated with that key. + * + *
{@code
+     * metricContext.setMetric("Latency", StatisticSet.builer().addValue(10).addValue(100).build())
+     * }
+ * + * @param key Name of the metric + * @param value Value of the metric + * @throws InvalidMetricException if the metric is invalid + */ + public void setMetric(String key, Metric value) throws InvalidMetricException { + Validator.validateMetric(key, value); + metricDirective.setMetric(key, value); + metricNameAndResolutionMap.put(key, value.storageResolution); + metricNameAndAggregationMap.put(key, AggregationType.STATISTIC_SET); } /** @@ -324,25 +439,32 @@ public List serialize() throws JsonProcessingException { } else { List nodes = new ArrayList<>(); Map metrics = new HashMap<>(); - Queue metricQueue = new LinkedList<>(rootNode.metrics().values()); - while (!metricQueue.isEmpty()) { - Metric metric = metricQueue.poll(); + ArrayList> remainingMetrics = new ArrayList<>(); + PriorityQueue> metricQueue = + new PriorityQueue<>((x, y) -> Integer.compare(x.size(), y.size())); + + for (Metric metric : rootNode.metrics().values()) { + metricQueue.offer(metric.serialize()); + } + // Split metrics into batches of 100 (max allowed by EMF backend + while (!metricQueue.isEmpty() || !remainingMetrics.isEmpty()) { if (metrics.size() == Constants.MAX_METRICS_PER_EVENT - || metrics.containsKey(metric.getName())) { + || metricQueue.isEmpty() + || metrics.containsKey(metricQueue.peek().peek().getName())) { nodes.add(buildRootNode(metrics)); metrics = new HashMap<>(); + metricQueue.addAll(remainingMetrics); + remainingMetrics.clear(); } - Metric overSizeMetric = - metric.getMetricValuesOverSize(Constants.MAX_DATAPOINTS_PER_METRIC); - Metric underSizeMetric = - metric.getMetricValuesUnderSize(Constants.MAX_DATAPOINTS_PER_METRIC); + Queue serializedMetrics = metricQueue.poll(); + Metric firstBatch = serializedMetrics.poll(); - metrics.put(metric.getName(), underSizeMetric); + metrics.put(firstBatch.getName(), firstBatch); - if (overSizeMetric != null) { - metricQueue.offer(overSizeMetric); + if (!serializedMetrics.isEmpty()) { + remainingMetrics.add(serializedMetrics); } } @@ -366,10 +488,6 @@ private RootNode buildRootNode(Map metrics) { } private boolean anyMetricWithTooManyDataPoints(RootNode node) { - return node.metrics().values().stream() - .anyMatch( - metric -> - metric.getMetricValuesOverSize(Constants.MAX_DATAPOINTS_PER_METRIC) - != null); + return node.metrics().values().stream().anyMatch(metric -> metric.isOversized()); } } diff --git a/src/main/java/software/amazon/cloudwatchlogs/emf/model/StatisticSet.java b/src/main/java/software/amazon/cloudwatchlogs/emf/model/StatisticSet.java new file mode 100644 index 00000000..b5b46993 --- /dev/null +++ b/src/main/java/software/amazon/cloudwatchlogs/emf/model/StatisticSet.java @@ -0,0 +1,107 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed 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 software.amazon.cloudwatchlogs.emf.model; + +import java.util.LinkedList; +import lombok.NonNull; +import software.amazon.cloudwatchlogs.emf.exception.InvalidMetricException; + +/** Represents the StatisticSet of the EMF schema. */ +public class StatisticSet extends Metric { + + StatisticSet( + Unit unit, + StorageResolution storageResolution, + double max, + double min, + int count, + double sum) { + this(unit, storageResolution, new Statistics(max, min, count, sum)); + } + + protected StatisticSet( + @NonNull String name, + Unit unit, + StorageResolution storageResolution, + Statistics statistics) { + this.unit = unit; + this.storageResolution = storageResolution; + this.values = statistics; + this.name = name; + } + + StatisticSet(Unit unit, StorageResolution storageResolution, Statistics statistics) { + this.unit = unit; + this.storageResolution = storageResolution; + this.values = statistics; + } + + @Override + protected LinkedList serialize() throws InvalidMetricException { + // A statistic set is a complete metric that cannot be broken into smaller pieces therefore + // this metric will be the only one in the returned list + LinkedList queue = new LinkedList<>(); + queue.add(this); + + return queue; + } + + protected boolean isOversized() { + return false; // StatisticSets cannot be oversized according to CWL + } + + @Override + public boolean hasValidValues() { + return values != null && values.isValid(); + } + + public static StatisticSetBuilder builder() { + return new StatisticSetBuilder(); + } + + public static class StatisticSetBuilder + extends Metric.MetricBuilder { + + @Override + protected StatisticSetBuilder getThis() { + return this; + } + + public StatisticSetBuilder() { + values = new Statistics(); + } + + @Override + public StatisticSetBuilder addValue(double value) { + this.values.addValue(value); + return this; + } + + public StatisticSetBuilder values(@NonNull Statistics values) { + this.values = values; + return this; + } + + @Override + public StatisticSet build() { + if (name == null) { + return new StatisticSet(unit, storageResolution, values); + } + return new StatisticSet(name, unit, storageResolution, values); + } + } +} diff --git a/src/main/java/software/amazon/cloudwatchlogs/emf/model/Statistics.java b/src/main/java/software/amazon/cloudwatchlogs/emf/model/Statistics.java new file mode 100644 index 00000000..dbf3d5e4 --- /dev/null +++ b/src/main/java/software/amazon/cloudwatchlogs/emf/model/Statistics.java @@ -0,0 +1,90 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed 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 software.amazon.cloudwatchlogs.emf.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +class Statistics { + Statistics(double max, double min, int count, double sum) { + this.max = max; + this.min = min; + this.count = count; + this.sum = sum; + if (!isValid()) { + throw new IllegalArgumentException( + "This is an impossible statistic set, there is no set of values that can create these statistics."); + } + } + + Statistics() { + count = 0; + sum = 0.; + }; + + @JsonProperty("Max") + public Double max = Double.MIN_VALUE; + + @JsonProperty("Min") + public Double min = Double.MAX_VALUE; + + @JsonProperty("Count") + public int count; + + @JsonProperty("Sum") + public Double sum; + + int size() { + return 4; + } + + void addValue(double value) { + count++; + sum += value; + if (value > max) { + max = value; + } + if (value < min) { + min = value; + } + } + + /** @returns true if this object represents a possible non-empy set of real values. */ + boolean isValid() { + return !(max < min + // Statistic set must not be empty or have a negative count + || (count <= 0) + // The max and min must be the same if there is only one value + || (count == 1 && Math.abs(max - min) > 1e-5) + // the sum must be less than or equal to the greatest possible value that could be + // created with this max, min and count + || min + max * (count - 1) < sum + // the sum must be greater than or equal to the smallest possible value that could + // be created with this max, min and count + || max + min * (count - 1) > sum); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Statistics that = (Statistics) o; + return count == that.count + && that.sum.equals(sum) + && that.max.equals(max) + && that.min.equals(min); + } +} diff --git a/src/main/java/software/amazon/cloudwatchlogs/emf/util/Validator.java b/src/main/java/software/amazon/cloudwatchlogs/emf/util/Validator.java index d8bf54b4..7ca4ca6d 100644 --- a/src/main/java/software/amazon/cloudwatchlogs/emf/util/Validator.java +++ b/src/main/java/software/amazon/cloudwatchlogs/emf/util/Validator.java @@ -20,7 +20,12 @@ import java.util.Map; import org.apache.commons.lang3.StringUtils; import software.amazon.cloudwatchlogs.emf.Constants; -import software.amazon.cloudwatchlogs.emf.exception.*; +import software.amazon.cloudwatchlogs.emf.exception.InvalidDimensionException; +import software.amazon.cloudwatchlogs.emf.exception.InvalidMetricException; +import software.amazon.cloudwatchlogs.emf.exception.InvalidNamespaceException; +import software.amazon.cloudwatchlogs.emf.exception.InvalidTimestampException; +import software.amazon.cloudwatchlogs.emf.model.AggregationType; +import software.amazon.cloudwatchlogs.emf.model.Metric; import software.amazon.cloudwatchlogs.emf.model.StorageResolution; import software.amazon.cloudwatchlogs.emf.model.Unit; @@ -92,6 +97,7 @@ public static void validateDimensionSet(String dimensionName, String dimensionVa * @param value Metric value * @param unit Metric unit * @param storageResolution Metric resolution + * @param aggregationType Metric aggregation type * @param metricNameAndResolutionMap Map to validate Metric * @throws InvalidMetricException if metric is invalid */ @@ -100,7 +106,9 @@ public static void validateMetric( double value, Unit unit, StorageResolution storageResolution, - Map metricNameAndResolutionMap) + AggregationType aggregationType, + Map metricNameAndResolutionMap, + Map metricNameAndAggregationMap) throws InvalidMetricException { if (name == null || name.trim().isEmpty()) { @@ -136,6 +144,61 @@ public static void validateMetric( + name + " is already set. A single log event cannot have a metric with two different resolutions."); } + + if (aggregationType == null || aggregationType == AggregationType.UNKNOWN_TO_SDK_VERSION) { + throw new InvalidMetricException("Metric aggregation type is invalid"); + } + + if ((metricNameAndAggregationMap.containsKey(name)) + && (!metricNameAndAggregationMap.get(name).equals(aggregationType))) { + throw new InvalidMetricException( + "Aggregation type for metric " + + name + + " is already set. A single log event cannot have a metric with two different aggregation types."); + } + } + + /** + * Validates Metric. + * + * @see CloudWatch + * Metric + * @param name Metric name + * @param metric Metric to be validated + * @throws InvalidMetricException if metric is invalid + */ + public static void validateMetric(String name, Metric metric) throws InvalidMetricException { + + if (name == null || name.trim().isEmpty()) { + throw new InvalidMetricException( + "Metric name " + name + " must include at least one non-whitespace character"); + } + + if (name.length() > Constants.MAX_METRIC_NAME_LENGTH) { + throw new InvalidMetricException( + "Metric name exceeds maximum length of " + + Constants.MAX_METRIC_NAME_LENGTH + + ": " + + name); + } + + if (metric == null) { + throw new InvalidMetricException("Metric cannot be null"); + } + + if (!metric.hasValidValues()) { + throw new InvalidMetricException("Metric has no values"); + } + + if (metric.getUnit() == null) { + throw new InvalidMetricException("Metric unit cannot be null"); + } + + if (metric.getStorageResolution() == null + || metric.getStorageResolution() == StorageResolution.UNKNOWN_TO_SDK_VERSION) { + throw new InvalidMetricException("Metric resolution is invalid"); + } } /** diff --git a/src/test/java/software/amazon/cloudwatchlogs/emf/logger/MetricsLoggerTest.java b/src/test/java/software/amazon/cloudwatchlogs/emf/logger/MetricsLoggerTest.java index 683fa75f..ce73e472 100644 --- a/src/test/java/software/amazon/cloudwatchlogs/emf/logger/MetricsLoggerTest.java +++ b/src/test/java/software/amazon/cloudwatchlogs/emf/logger/MetricsLoggerTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import com.fasterxml.jackson.core.JsonProcessingException; import java.time.Instant; import java.util.Arrays; import java.util.List; @@ -36,6 +37,7 @@ import software.amazon.cloudwatchlogs.emf.exception.InvalidMetricException; import software.amazon.cloudwatchlogs.emf.exception.InvalidNamespaceException; import software.amazon.cloudwatchlogs.emf.exception.InvalidTimestampException; +import software.amazon.cloudwatchlogs.emf.model.AggregationType; import software.amazon.cloudwatchlogs.emf.model.DimensionSet; import software.amazon.cloudwatchlogs.emf.model.MetricsContext; import software.amazon.cloudwatchlogs.emf.model.StorageResolution; @@ -531,6 +533,22 @@ void flush_doesNotPreserveMetrics() assertFalse(sink.getLogEvents().get(0).contains("Count")); } + @Test + void testDefaultAggregationType() + throws InvalidMetricException, DimensionSetExceededException, JsonProcessingException { + logger.setDefaultAggregationType(AggregationType.STATISTIC_SET); + assertEquals(AggregationType.STATISTIC_SET, logger.getDefaultAggregationType()); + + logger.putMetric("Count", 1); + logger.flush(); + + assertTrue( + sink.getContext() + .serialize() + .get(0) + .contains("\"Count\":{\"Max\":1.0,\"Min\":1.0,\"Count\":1,\"Sum\":1.0}")); + } + private void expectDimension(String dimension, String value) throws DimensionSetExceededException { List dimensions = sink.getContext().getDimensions(); diff --git a/src/test/java/software/amazon/cloudwatchlogs/emf/model/MetricDefinitionTest.java b/src/test/java/software/amazon/cloudwatchlogs/emf/model/MetricDefinitionTest.java index 1bfd902c..09cce9aa 100644 --- a/src/test/java/software/amazon/cloudwatchlogs/emf/model/MetricDefinitionTest.java +++ b/src/test/java/software/amazon/cloudwatchlogs/emf/model/MetricDefinitionTest.java @@ -17,19 +17,24 @@ package software.amazon.cloudwatchlogs.emf.model; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Arrays; import java.util.Collections; -import org.junit.Test; +import org.junit.jupiter.api.Test; class MetricDefinitionTest { - @Test(expected = NullPointerException.class) + @Test public void testThrowExceptionIfNameIsNull() { MetricDefinition.MetricDefinitionBuilder builder = MetricDefinition.builder(); - builder.setName(null); + assertThrows( + NullPointerException.class, + () -> { + builder.setName(null); + }); } @Test @@ -106,7 +111,7 @@ public void testSerializeMetricDefinition() throws JsonProcessingException { @Test public void testAddValue() { MetricDefinition.MetricDefinitionBuilder builder = - MetricDefinition.builder().unit(Unit.MILLISECONDS).addValue(10).addValue(20); + MetricDefinition.builder().unit(Unit.MILLISECONDS).addValue(10); assertEquals(Collections.singletonList(10d), builder.getValues()); builder.addValue(20); diff --git a/src/test/java/software/amazon/cloudwatchlogs/emf/model/MetricDirectiveTest.java b/src/test/java/software/amazon/cloudwatchlogs/emf/model/MetricDirectiveTest.java index c1c2f8f8..ed672249 100644 --- a/src/test/java/software/amazon/cloudwatchlogs/emf/model/MetricDirectiveTest.java +++ b/src/test/java/software/amazon/cloudwatchlogs/emf/model/MetricDirectiveTest.java @@ -21,10 +21,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Arrays; import java.util.Collections; +import org.junit.Test; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; import software.amazon.cloudwatchlogs.emf.exception.DimensionSetExceededException; import software.amazon.cloudwatchlogs.emf.exception.InvalidDimensionException; +import software.amazon.cloudwatchlogs.emf.exception.InvalidMetricException; class MetricDirectiveTest { private final ObjectMapper objectMapper = @@ -263,4 +264,52 @@ void testPutDimensionsAfterSetDimensions() "{\"Dimensions\":[[\"Version\"],[\"Region\"],[\"Instance\"]],\"Metrics\":[],\"Namespace\":\"aws-embedded-metrics\"}", serializedMetricDirective); } + + @Test + void testSetMetricStatisticSetReplacement() + throws JsonProcessingException, InvalidDimensionException, + DimensionSetExceededException { + MetricDirective metricDirective = new MetricDirective(); + metricDirective.putMetric("Metric", 10.); + Assertions.assertEquals( + MetricDefinition.MetricDefinitionBuilder.class, + metricDirective.getMetrics().get("Metric").getClass()); + + StatisticSet statisticSet1 = StatisticSet.builder().addValue(7.).addValue(2.5).build(); + + metricDirective.setMetric("Metric", statisticSet1); + Assertions.assertEquals( + StatisticSet.class, metricDirective.getMetrics().get("Metric").getClass()); + Assertions.assertEquals( + 2, ((Statistics) metricDirective.getMetrics().get("Metric").getValues()).count); + + StatisticSet statisticSet2 = StatisticSet.builder().addValue(7.).build(); + + metricDirective.setMetric("Metric", statisticSet2); + Assertions.assertEquals( + StatisticSet.class, metricDirective.getMetrics().get("Metric").getClass()); + Assertions.assertEquals( + 1, ((Statistics) metricDirective.getMetrics().get("Metric").getValues()).count); + } + + @Test + void testSetMetricStatisticSetThenPutMetric() + throws JsonProcessingException, InvalidDimensionException, + DimensionSetExceededException { + MetricDirective metricDirective = new MetricDirective(); + + StatisticSet statisticSet2 = StatisticSet.builder().addValue(7.).build(); + + metricDirective.setMetric("Metric", statisticSet2); + Assertions.assertEquals( + StatisticSet.class, metricDirective.getMetrics().get("Metric").getClass()); + Assertions.assertEquals( + 1, ((Statistics) metricDirective.getMetrics().get("Metric").getValues()).count); + + Assertions.assertThrows( + InvalidMetricException.class, + () -> { + metricDirective.putMetric("Metric", 1.); + }); + } } diff --git a/src/test/java/software/amazon/cloudwatchlogs/emf/model/MetricsContextTest.java b/src/test/java/software/amazon/cloudwatchlogs/emf/model/MetricsContextTest.java index 4191fdef..e59a1818 100644 --- a/src/test/java/software/amazon/cloudwatchlogs/emf/model/MetricsContextTest.java +++ b/src/test/java/software/amazon/cloudwatchlogs/emf/model/MetricsContextTest.java @@ -46,7 +46,7 @@ void testSerializeLessThan100Metrics() throws JsonProcessingException, InvalidMe List events = mc.serialize(); Assertions.assertEquals(1, events.size()); - List metrics = parseMetrics(events.get(0)); + List metrics = parseMetricDefinitions(events.get(0)); Assertions.assertEquals(metrics.size(), metricCount); for (Metric metric : metrics) { Metric originalMetric = mc.getRootNode().metrics().get(metric.getName()); @@ -70,7 +70,7 @@ void testSerializeMoreThen100Metrics() throws JsonProcessingException, InvalidMe List allMetrics = new ArrayList<>(); for (String event : events) { - allMetrics.addAll(parseMetrics(event)); + allMetrics.addAll(parseMetricDefinitions(event)); } Assertions.assertEquals(metricCount, allMetrics.size()); for (Metric metric : allMetrics) { @@ -96,7 +96,7 @@ void testSerializeAMetricWith101DataPoints() Assertions.assertEquals(expectedEventCount, events.size()); List allMetrics = new ArrayList<>(); for (String event : events) { - allMetrics.addAll(parseMetrics(event)); + allMetrics.addAll(parseMetricDefinitions(event)); } List expectedValues = new ArrayList<>(); for (int i = 0; i < Constants.MAX_DATAPOINTS_PER_METRIC; i++) { @@ -121,8 +121,8 @@ void testSerializeMetricsWith101DataPoints() List events = mc.serialize(); Assertions.assertEquals(expectedEventCount, events.size()); - List metricsFromEvent1 = parseMetrics(events.get(0)); - List metricsFromEvent2 = parseMetrics(events.get(1)); + List metricsFromEvent1 = parseMetricDefinitions(events.get(0)); + List metricsFromEvent2 = parseMetricDefinitions(events.get(1)); Assertions.assertEquals(2, metricsFromEvent1.size()); List expectedValues = new ArrayList<>(); @@ -154,6 +154,44 @@ void testSerializeZeroMetric() Assertions.assertFalse(rootNode.containsKey("_aws")); } + @Test + void testSerializeStatisticSetMetric() throws JsonProcessingException { + MetricsContext mc = new MetricsContext(); + int dataPointCount = 100; + String metricName = "metric1"; + for (int i = 0; i < dataPointCount; i++) { + mc.putMetric(metricName, i, AggregationType.STATISTIC_SET); + } + + List events = mc.serialize(); + List statisticSets = parseStatisticSetMetrics(events.get(0)); + Assertions.assertEquals(1, statisticSets.size()); + Assertions.assertEquals( + new Statistics(99., 0., 100, 4950.), statisticSets.get(0).getValues()); + } + + @Test + void testSetInvalidMetric() { + MetricsContext mc = new MetricsContext(); + Assertions.assertThrows( + InvalidMetricException.class, + () -> { + mc.setMetric("Metric", StatisticSet.builder().build()); + }); + + Assertions.assertThrows( + InvalidMetricException.class, + () -> { + mc.setMetric("Metric", null); + }); + + Assertions.assertThrows( + InvalidMetricException.class, + () -> { + mc.setMetric("", StatisticSet.builder().addValue(1).build()); + }); + } + @Test @SuppressWarnings("unchecked") void testSetTimestamp() @@ -188,7 +226,8 @@ void testPutMetadata() { } @SuppressWarnings("unchecked") - private ArrayList parseMetrics(String event) throws JsonProcessingException { + private ArrayList parseMetricDefinitions(String event) + throws JsonProcessingException { Map rootNode = parseRootNode(event); Map metadata = (Map) rootNode.get("_aws"); ArrayList> metricDirectives = @@ -222,6 +261,35 @@ private ArrayList parseMetrics(String event) throws JsonProces return metricDefinitions; } + @SuppressWarnings("unchecked") + private ArrayList parseStatisticSetMetrics(String event) + throws JsonProcessingException { + Map rootNode = parseRootNode(event); + Map metadata = (Map) rootNode.get("_aws"); + ArrayList> metricDirectives = + (ArrayList>) metadata.get("CloudWatchMetrics"); + ArrayList> metrics = + (ArrayList>) metricDirectives.get(0).get("Metrics"); + + ArrayList statisticSets = new ArrayList<>(); + for (Map metric : metrics) { + String name = metric.get("Name"); + Unit unit = Unit.fromValue(metric.get("Unit")); + Map value = (Map) rootNode.get(name); + statisticSets.add( + new StatisticSet( + name, + unit, + StorageResolution.STANDARD, + new Statistics( + (Double) value.get("Max"), + (Double) value.get("Min"), + (int) value.get("Count"), + (Double) value.get("Sum")))); + } + return statisticSets; + } + private Map parseRootNode(String event) throws JsonProcessingException { return new JsonMapper().readValue(event, new TypeReference>() {}); } diff --git a/src/test/java/software/amazon/cloudwatchlogs/emf/model/StatisticSetTest.java b/src/test/java/software/amazon/cloudwatchlogs/emf/model/StatisticSetTest.java new file mode 100644 index 00000000..d26e724e --- /dev/null +++ b/src/test/java/software/amazon/cloudwatchlogs/emf/model/StatisticSetTest.java @@ -0,0 +1,158 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed 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 software.amazon.cloudwatchlogs.emf.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +public class StatisticSetTest { + @Test + public void testSerializeStatisticSetWithoutUnitWithHighStorageResolution() + throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + + StatisticSet statisticSet = + StatisticSet.builder() + .storageResolution(StorageResolution.HIGH) + .addValue(10) + .name("Time") + .build(); + String metricString = objectMapper.writeValueAsString(statisticSet); + + assertEquals("{\"Name\":\"Time\",\"Unit\":\"None\",\"StorageResolution\":1}", metricString); + } + + @Test + public void testSerializeStatisticSetWithUnitWithoutStorageResolution() + throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + StatisticSet statisticSet = + StatisticSet.builder().unit(Unit.MILLISECONDS).name("Time").addValue(10).build(); + String metricString = objectMapper.writeValueAsString(statisticSet); + + assertEquals("{\"Name\":\"Time\",\"Unit\":\"Milliseconds\"}", metricString); + } + + @Test + public void testSerializeStatisticSetWithoutUnitWithStandardStorageResolution() + throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + + StatisticSet statisticSet = + StatisticSet.builder() + .storageResolution(StorageResolution.STANDARD) + .name("Time") + .addValue(10) + .build(); + String metricString = objectMapper.writeValueAsString(statisticSet); + + assertEquals("{\"Name\":\"Time\",\"Unit\":\"None\"}", metricString); + } + + @Test + public void testSerializeStatisticSetWithoutUnit() throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + StatisticSet statisticSet = StatisticSet.builder().name("Time").build(); + String metricString = objectMapper.writeValueAsString(statisticSet); + + assertEquals("{\"Name\":\"Time\",\"Unit\":\"None\"}", metricString); + } + + @Test + public void testSerializeStatisticSet() throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + StatisticSet statisticSet = + StatisticSet.builder() + .unit(Unit.MILLISECONDS) + .storageResolution(StorageResolution.HIGH) + .name("Time") + .addValue(10) + .build(); + String metricString = objectMapper.writeValueAsString(statisticSet); + + assertEquals( + "{\"Name\":\"Time\",\"Unit\":\"Milliseconds\",\"StorageResolution\":1}", + metricString); + } + + @Test + public void testAddValues() { + StatisticSet.StatisticSetBuilder ssb = StatisticSet.builder(); + ssb.addValue(10); + assertEquals(new Statistics(10., 10., 1, 10.), ssb.getValues()); + + ssb.addValue(20); + assertEquals(new Statistics(20., 10., 2, 30.), ssb.getValues()); + } + + @Test + public void testManyAddValues() { + StatisticSet.StatisticSetBuilder ssb = StatisticSet.builder(); + for (int i = 1; i < 100; i++) { + ssb.addValue(i); + assertEquals(new Statistics(i, 1., i, i * (i + 1) / 2), ssb.getValues()); + } + } + + @Test + public void testBuildBuilder() { + StatisticSet statisticSet = StatisticSet.builder().addValue(10).build(); + assertEquals(statisticSet.getValues(), statisticSet.getValues()); + + assertEquals(statisticSet.name, null); + statisticSet.setName("test"); + assertEquals(statisticSet.name, "test"); + } + + @Test + public void testCreateImmutableStatisticSet() { + StatisticSet ss = new StatisticSet(Unit.NONE, StorageResolution.STANDARD, 10, 1, 11, 100); + assertEquals(new Statistics(10, 1, 11, 100), ss.getValues()); + } + + @Test + public void testImpossibleStatisticSet() { + // Sum too big + assertThrows( + IllegalArgumentException.class, + () -> new StatisticSet(Unit.NONE, StorageResolution.STANDARD, 10, 1, 2, 100)); + // Sum too small + assertThrows( + IllegalArgumentException.class, + () -> new StatisticSet(Unit.NONE, StorageResolution.STANDARD, 10, 1, 3, 11)); + // Count == 0 for non-zero set + assertThrows( + IllegalArgumentException.class, + () -> new StatisticSet(Unit.NONE, StorageResolution.STANDARD, 10, 1, 0, 100)); + // min > max + assertThrows( + IllegalArgumentException.class, + () -> new StatisticSet(Unit.NONE, StorageResolution.STANDARD, 0, 1, 1, 0)); + // Negative count + assertThrows( + IllegalArgumentException.class, + () -> new StatisticSet(Unit.NONE, StorageResolution.STANDARD, 10, 1, -1, 10)); + // different max and min for 1 count + assertThrows( + IllegalArgumentException.class, + () -> new StatisticSet(Unit.NONE, StorageResolution.STANDARD, 10, 1, 1, 1)); + } +}