diff --git a/.brazil.json b/.brazil.json index 2f256aadaa0f..07a575add08a 100644 --- a/.brazil.json +++ b/.brazil.json @@ -16,6 +16,7 @@ "aws-xml-protocol": { "packageName": "AwsJavaSdk-Core-AwsXmlProtocol" }, "smithy-rpcv2-protocol": { "packageName": "AwsJavaSdk-Core-SmithyRpcV2Protocol" }, "cloudwatch-metric-publisher": { "packageName": "AwsJavaSdk-MetricPublisher-CloudWatch" }, + "emf-metric-logging-publisher": { "packageName": "AwsJavaSdk-MetricPublisher-Emf" }, "codegen": { "packageName": "AwsJavaSdk-Codegen" }, "dynamodb-enhanced": { "packageName": "AwsJavaSdk-DynamoDb-Enhanced" }, "http-client-spi": { "packageName": "AwsJavaSdk-HttpClient" }, diff --git a/.changes/next-release/feature-EmfMetricLoggingPublisher-d96a2fb.json b/.changes/next-release/feature-EmfMetricLoggingPublisher-d96a2fb.json new file mode 100644 index 000000000000..c0a75acbddec --- /dev/null +++ b/.changes/next-release/feature-EmfMetricLoggingPublisher-d96a2fb.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "Emf Metric Logging Publisher", + "contributor": "", + "description": "Added a new EmfMetricLoggingPublisher class that transforms SdkMetricCollection to emf format string and logs it, which will be automatically collected by cloudwatch." +} diff --git a/aws-sdk-java/pom.xml b/aws-sdk-java/pom.xml index ac3b32d23d99..d9f6b4f41b02 100644 --- a/aws-sdk-java/pom.xml +++ b/aws-sdk-java/pom.xml @@ -1773,6 +1773,11 @@ Amazon AutoScaling, etc). cloudwatch-metric-publisher ${awsjavasdk.version} + + software.amazon.awssdk + emf-metric-logging-publisher + ${awsjavasdk.version} + software.amazon.awssdk launchwizard diff --git a/bom/pom.xml b/bom/pom.xml index 20994074c2f0..e02c840f8482 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -212,6 +212,11 @@ cloudwatch-metric-publisher ${awsjavasdk.version} + + software.amazon.awssdk + emf-metric-logging-publisher + ${awsjavasdk.version} + software.amazon.awssdk s3-transfer-manager diff --git a/metric-publishers/cloudwatch-metric-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/cloudwatch/internal/transform/MetricCollectionAggregator.java b/metric-publishers/cloudwatch-metric-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/cloudwatch/internal/transform/MetricCollectionAggregator.java index 9a00b2d8fa04..16b7ba0da2e4 100644 --- a/metric-publishers/cloudwatch-metric-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/cloudwatch/internal/transform/MetricCollectionAggregator.java +++ b/metric-publishers/cloudwatch-metric-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/cloudwatch/internal/transform/MetricCollectionAggregator.java @@ -33,6 +33,7 @@ import software.amazon.awssdk.services.cloudwatch.model.MetricDatum; import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataRequest; import software.amazon.awssdk.services.cloudwatch.model.StatisticSet; +import software.amazon.awssdk.utils.MetricValueNormalizer; /** * Aggregates {@link MetricCollection}s by: (1) the minute in which they occurred, and (2) the dimensions in the collection diff --git a/metric-publishers/cloudwatch-metric-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/cloudwatch/internal/transform/TimeBucketedMetrics.java b/metric-publishers/cloudwatch-metric-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/cloudwatch/internal/transform/TimeBucketedMetrics.java index 949f16a01504..c850e7de72a3 100644 --- a/metric-publishers/cloudwatch-metric-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/cloudwatch/internal/transform/TimeBucketedMetrics.java +++ b/metric-publishers/cloudwatch-metric-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/cloudwatch/internal/transform/TimeBucketedMetrics.java @@ -36,6 +36,7 @@ import software.amazon.awssdk.metrics.SdkMetric; import software.amazon.awssdk.services.cloudwatch.model.Dimension; import software.amazon.awssdk.services.cloudwatch.model.StandardUnit; +import software.amazon.awssdk.utils.MetricValueNormalizer; /** * "Buckets" metrics by the minute in which they were collected. This allows all metric data for a given 1-minute period to be diff --git a/metric-publishers/emf-metric-logging-publisher/pom.xml b/metric-publishers/emf-metric-logging-publisher/pom.xml new file mode 100644 index 000000000000..f037c0da7f62 --- /dev/null +++ b/metric-publishers/emf-metric-logging-publisher/pom.xml @@ -0,0 +1,78 @@ + + + + + 4.0.0 + + software.amazon.awssdk + metric-publishers + 2.30.3-SNAPSHOT + + + emf-metric-logging-publisher + AWS Java SDK :: Metric Publishers :: Emf + jar + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + software.amazon.awssdk.metrics.publishers.emf + + + + + + + + + + software.amazon.awssdk + annotations + ${awsjavasdk.version} + + + software.amazon.awssdk + http-client-spi + ${awsjavasdk.version} + + + software.amazon.awssdk + json-utils + ${awsjavasdk.version} + compile + + + software.amazon.awssdk + sdk-core + ${awsjavasdk.version} + compile + + + com.networknt + json-schema-validator + ${json-schema-validator.version} + test + + + + \ No newline at end of file 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 new file mode 100644 index 000000000000..07bba8a97442 --- /dev/null +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisher.java @@ -0,0 +1,228 @@ +/* + * 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.metrics.publishers.emf; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.annotations.ThreadSafe; +import software.amazon.awssdk.core.metrics.CoreMetric; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.metrics.MetricCollection; +import software.amazon.awssdk.metrics.MetricLevel; +import software.amazon.awssdk.metrics.MetricPublisher; +import software.amazon.awssdk.metrics.SdkMetric; +import software.amazon.awssdk.metrics.publishers.emf.internal.EmfMetricConfiguration; +import software.amazon.awssdk.metrics.publishers.emf.internal.MetricEmfConverter; +import software.amazon.awssdk.utils.Logger; + +/** + * A metric publisher implementation that converts metrics into CloudWatch Embedded Metric Format (EMF). + * EMF allows metrics to be published through CloudWatch Logs using a structured JSON format, which + * CloudWatch automatically extracts and processes into metrics. + * + *

+ * This publisher is particularly well-suited for serverless environments like AWS Lambda and container + * environments like Amazon ECS that have built-in integration with CloudWatch Logs. Using EMF eliminates + * the need for separate metric publishing infrastructure as metrics are automatically extracted from + * log entries. + *

+ * + *

+ * The EMF publisher converts metric collections into JSON-formatted log entries that conform to the + * CloudWatch EMF specification. The logGroupName field is required for EMF to work. + * CloudWatch automatically processes these logs to generate corresponding metrics that can be used for + * monitoring and alerting. + *

+ * + * @snippet + * // Create a EmfMetricLoggingPublisher using a custom namespace. + * MetricPublisher emfMetricLoggingPublisher = EmfMetricLoggingPublisher.builder() + * .logGroupName("myLogGroupName") + * .namespace("myApplication") + * .build(); + * + * @see MetricPublisher The base interface for metric publishers + * @see MetricCollection For the collection of metrics to be published + * @see EmfMetricConfiguration For configuration options + * @see MetricEmfConverter For the conversion logic + * + */ + +@ThreadSafe +@Immutable +@SdkPublicApi +public final class EmfMetricLoggingPublisher implements MetricPublisher { + + private static final Logger logger = Logger.loggerFor(EmfMetricLoggingPublisher.class); + private final MetricEmfConverter metricConverter; + + + private EmfMetricLoggingPublisher(Builder builder) { + EmfMetricConfiguration config = new EmfMetricConfiguration.Builder() + .namespace(builder.namespace) + .logGroupName(builder.logGroupName) + .dimensions(builder.dimensions) + .metricLevel(builder.metricLevel) + .metricCategories(builder.metricCategories) + .build(); + + this.metricConverter = new MetricEmfConverter(config); + } + + + public static Builder builder() { + return new Builder(); + } + + + @Override + public void publish(MetricCollection metricCollection) { + if (metricCollection == null) { + logger.warn(() -> "Null metric collection passed to the publisher"); + return; + } + try { + List emfStrings = metricConverter.convertMetricCollectionToEmf(metricCollection); + for (String emfString : emfStrings) { + logger.info(() -> emfString); + } + } catch (Exception e) { + logger.error(() -> "Failed to log metrics in EMF format", e); + } + } + + /** + * Closes this metric publisher. This implementation is empty as the EMF metric logging publisher + * does not maintain any resources that require explicit cleanup. + */ + @Override + public void close() { + } + + public static final class Builder { + private String namespace; + private String logGroupName; + private Collection> dimensions; + private Collection metricCategories; + private MetricLevel metricLevel; + + private Builder() { + } + + /** + * Configure the namespace that will be put into the emf log to this publisher. + * + *

If this is not specified, {@code AwsSdk/JavaSdk2} will be used. + */ + public Builder namespace(String namespace) { + this.namespace = namespace; + return this; + } + + /** + * Configure the {@link SdkMetric} that are used to define the Dimension Set Array that will be put into the emf log to + * this + * publisher. + * + *

If this is not specified, {@link CoreMetric#SERVICE_ID} and {@link CoreMetric#OPERATION_NAME} will be used. + */ + public Builder dimensions(Collection> dimensions) { + this.dimensions = new ArrayList<>(dimensions); + return this; + } + + /** + * @see #dimensions(SdkMetric[]) + */ + @SafeVarargs + public final Builder dimensions(SdkMetric... dimensions) { + return dimensions(Arrays.asList(dimensions)); + } + + + /** + * Configure the {@link MetricCategory}s that should be uploaded to CloudWatch. + * + *

If this is not specified, {@link MetricCategory#ALL} is used. + * + *

All {@link SdkMetric}s are associated with at least one {@code MetricCategory}. This setting determines which + * category of metrics uploaded to CloudWatch. Any metrics {@link #publish(MetricCollection)}ed that do not fall under + * these configured categories are ignored. + * + *

Note: If there are {@link #dimensions(Collection)} configured that do not fall under these {@code MetricCategory} + * values, the dimensions will NOT be ignored. In other words, the metric category configuration only affects which + * metrics are uploaded to CloudWatch, not which values can be used for {@code dimensions}. + */ + public Builder metricCategories(Collection metricCategories) { + this.metricCategories = new ArrayList<>(metricCategories); + return this; + } + + /** + * @see #metricCategories(Collection) + */ + public Builder metricCategories(MetricCategory... metricCategories) { + return metricCategories(Arrays.asList(metricCategories)); + } + + /** + * Configure the LogGroupName key that will be put into the emf log to this publisher. This is required when using + * the CloudWatch agent to send embedded metric format logs that tells the agent which log + * group to use. + * + *

If this is not specified, for AWS lambda environments, {@code AWS_LAMBDA_LOG_GROUP_NAME} + * is used. + * This field is required and must not be null or empty for non-lambda environments. + * @throws NullPointerException if non-lambda environment and logGroupName is null + */ + public Builder logGroupName(String logGroupName) { + this.logGroupName = logGroupName; + return this; + } + + /** + * Configure the {@link MetricLevel} that should be uploaded to CloudWatch. + * + *

If this is not specified, {@link MetricLevel#INFO} is used. + * + *

All {@link SdkMetric}s are associated with one {@code MetricLevel}. This setting determines which level of metrics + * uploaded to CloudWatch. Any metrics {@link #publish(MetricCollection)}ed that do not fall under these configured + * categories are ignored. + * + *

Note: If there are {@link #dimensions(Collection)} configured that do not fall under this {@code MetricLevel} + * values, the dimensions will NOT be ignored. In other words, the metric category configuration only affects which + * metrics are uploaded to CloudWatch, not which values can be used for {@code dimensions}. + */ + public Builder metricLevel(MetricLevel metricLevel) { + this.metricLevel = metricLevel; + return this; + } + + + /** + * Build a {@link EmfMetricLoggingPublisher} using the configuration currently configured on this publisher. + */ + public EmfMetricLoggingPublisher build() { + return new EmfMetricLoggingPublisher(this); + } + + } +} 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 new file mode 100644 index 000000000000..739cf0b0e4ca --- /dev/null +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/EmfMetricConfiguration.java @@ -0,0 +1,119 @@ +/* + * 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.metrics.publishers.emf.internal; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.core.metrics.CoreMetric; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.metrics.MetricLevel; +import software.amazon.awssdk.metrics.SdkMetric; +import software.amazon.awssdk.utils.Validate; +import software.amazon.awssdk.utils.internal.SystemSettingUtils; + +@SdkInternalApi +public final class EmfMetricConfiguration { + private static final String DEFAULT_NAMESPACE = "AwsSdk/JavaSdk2"; + private static final Set> DEFAULT_DIMENSIONS = Stream.of(CoreMetric.SERVICE_ID, + CoreMetric.OPERATION_NAME) + .collect(Collectors.toSet()); + private static final Set DEFAULT_CATEGORIES = Collections.singleton(MetricCategory.ALL); + private static final MetricLevel DEFAULT_METRIC_LEVEL = MetricLevel.INFO; + + private final String namespace; + private final String logGroupName; + private final Set> dimensions; + private final Collection metricCategories; + private final MetricLevel metricLevel; + + private EmfMetricConfiguration(Builder builder) { + this.namespace = builder.namespace == null ? DEFAULT_NAMESPACE : builder.namespace; + this.logGroupName = Validate.paramNotNull(resolveLogGroupName(builder), "logGroupName"); + 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; + } + + + public static class Builder { + private String namespace; + private String logGroupName; + private Collection> dimensions; + private Collection metricCategories; + private MetricLevel metricLevel; + + public Builder namespace(String namespace) { + this.namespace = namespace; + return this; + } + + public Builder logGroupName(String logGroupName) { + this.logGroupName = logGroupName; + return this; + } + + public Builder dimensions(Collection> dimensions) { + this.dimensions = dimensions; + return this; + } + + public Builder metricCategories(Collection metricCategories) { + this.metricCategories = metricCategories; + return this; + } + + public Builder metricLevel(MetricLevel metricLevel) { + this.metricLevel = metricLevel; + return this; + } + + public EmfMetricConfiguration build() { + return new EmfMetricConfiguration(this); + } + } + + public String namespace() { + return namespace; + } + + public String logGroupName() { + return logGroupName; + } + + public Collection> dimensions() { + return dimensions; + } + + public Collection metricCategories() { + return metricCategories; + } + + public MetricLevel metricLevel() { + return metricLevel; + } + + 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 new file mode 100644 index 000000000000..7973fa1fa0de --- /dev/null +++ b/metric-publishers/emf-metric-logging-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverter.java @@ -0,0 +1,379 @@ +/* + * 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.metrics.publishers.emf.internal; + +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.Duration; +import java.util.ArrayList; +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 software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.SdkTestInternalApi; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.metrics.MetricCollection; +import software.amazon.awssdk.metrics.MetricRecord; +import software.amazon.awssdk.metrics.SdkMetric; +import software.amazon.awssdk.protocols.jsoncore.JsonWriter; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.MetricValueNormalizer; +import software.amazon.awssdk.utils.Validate; + +/** + * Converts {@link MetricCollection} into List of Amazon CloudWatch Embedded Metric Format (EMF) Strings. + *

+ * This class is responsible for transforming {@link MetricCollection} into the EMF format required by CloudWatch. + * It handles the conversion of different metric types and ensures the output conforms to EMF specifications. + *

+ * + * + * @see + * CloudWatch Embedded Metric Format Specification + * @see EmfMetricConfiguration + */ +@SdkInternalApi +public class MetricEmfConverter { + /** + * EMF allows up to 100 elements in an array + * ... + */ + private static final int MAX_RECORD_SIZE = 100; + + /** + * EMF allows up to 100 MetricDefinition objects in one emf string + */ + private static final int MAX_METRIC_NUM = 100; + + private static final Logger logger = Logger.loggerFor(MetricEmfConverter.class); + private final List dimensions = new ArrayList<>(); + private final EmfMetricConfiguration config; + private final boolean metricCategoriesContainsAll; + private final Clock clock; + + @SdkTestInternalApi + public MetricEmfConverter(EmfMetricConfiguration config, Clock clock) { + this.config = config; + this.clock = clock; + this.metricCategoriesContainsAll = config.metricCategories().contains(MetricCategory.ALL); + } + + public MetricEmfConverter(EmfMetricConfiguration config) { + this(config, Clock.systemUTC()); + } + + /** + *

+ * Convert SDK Metrics to EMF Format. + * Transforms a collection of SDK metrics into CloudWatch's Embedded Metric Format (EMF). + * The method processes standard SDK measurements and structures them according to + * CloudWatch's EMF specification. + *

+ * Example Output + * @snippet + * { + * "_aws": { + * "Timestamp": 1672963200, + * "CloudWatchMetrics": [{ + * "Namespace": "AwsSdk/JavaSdk2", + * "Dimensions": [["ServiceId"]], + * "Metrics": [{ + * "Name": "AvailableConcurrency", + * }] + * }] + * }, + * "ServiceId": "DynamoDB", + * "AvailableConcurrency": 5 + * } + * + * @param metricCollection Collection of SDK metrics to be converted + * @return List of EMF-formatted metrics ready for CloudWatch + */ + public List convertMetricCollectionToEmf(MetricCollection metricCollection) { + Map, List>> aggregatedMetrics = new HashMap<>(); + + // Process metrics using level-order traversal + Queue queue = new LinkedList<>(); + if (!queue.offer(metricCollection)) { + logger.warn(() -> "failed to add metricCollection to the queue"); + } + + while (!queue.isEmpty()) { + MetricCollection current = queue.poll(); + + current.stream().forEach(metricRecord -> { + SdkMetric metric = metricRecord.metric(); + String metricName = metric.name(); + if (isDimension(metric) && !dimensions.contains(metricName)) { + dimensions.add(metricName); + } + + if (shouldReport(metricRecord) || isDimension(metric)) { + aggregatedMetrics.computeIfAbsent(metric, k -> new ArrayList<>()) + .add(metricRecord); + } + + }); + + if (current.children() != null) { + queue.addAll(current.children()); + } + } + + return createEmfStrings(aggregatedMetrics); + } + + /** + * Processes and normalizes metric values for EMF formatting. + * The method handles various input types and normalizes them according to EMF requirements: + * Value Conversion Rules: + *
    + *
  • Numbers (Integer, Long, Double, etc.) are converted to their native numeric format
  • + *
  • Duration values are converted to milliseconds
  • + *
  • Date/Time values are converted to epoch milliseconds
  • + *
  • Null values are omitted from the output
  • + *
  • Boolean values are converted to 1 (true) or 0 (false)
  • + *
  • Non-Dimension metrics with non-numeric values are omitted from the output
  • + *
+ * + * @param mRecord The metric record to process + */ + private void processAndWriteValue(JsonWriter jsonWriter, MetricRecord mRecord) { + Object value = mRecord.value(); + Class valueClass = mRecord.metric().valueClass(); + + if (value == null) { + return; + } + + if (Boolean.class.isAssignableFrom(valueClass)) { + jsonWriter.writeValue(value.equals(true) ? 1.0 : 0.0); + return; + } + + if (Duration.class.isAssignableFrom(valueClass)) { + Duration duration = (Duration) value; + double millisValue = duration.toMillis(); + jsonWriter.writeValue(millisValue); + return; + } + + if (Double.class.isAssignableFrom(valueClass)) { + jsonWriter.writeValue(MetricValueNormalizer.normalize((Double) value)); + return; + } + + if (Integer.class.isAssignableFrom(valueClass) || Long.class.isAssignableFrom(valueClass)) { + jsonWriter.writeValue((Integer) value); + } + } + + private List createEmfStrings(Map, List>> aggregatedMetrics) { + List emfStrings = new ArrayList<>(); + Map, List>> currentMetricBatch = new HashMap<>(); + + for (Map.Entry, List>> entry : aggregatedMetrics.entrySet()) { + SdkMetric metric = entry.getKey(); + List> records = entry.getValue(); + int size = records.size(); + if (records.size() > MAX_RECORD_SIZE) { + records = records.subList(0, MAX_RECORD_SIZE); + logger.warn(() -> String.format("Some AWS SDK client-side metric data have been dropped because it exceeds the " + + "cloudwatch requirements. " + + "There are %d values for metric %s", size, metric.name())); + } + + if (currentMetricBatch.size() == MAX_METRIC_NUM) { + emfStrings.add(createEmfString(currentMetricBatch)); + currentMetricBatch = new HashMap<>(); + } + + currentMetricBatch.put(metric, records); + } + + emfStrings.add(createEmfString(currentMetricBatch)); + + return emfStrings; + } + + + private String createEmfString(Map, List>> metrics) { + + JsonWriter jsonWriter = JsonWriter.create(); + jsonWriter.writeStartObject(); + + writeAwsObject(jsonWriter, metrics.keySet()); + writeMetricValues(jsonWriter, metrics); + + jsonWriter.writeEndObject(); + + return new String(jsonWriter.getBytes(), StandardCharsets.UTF_8); + + } + + private void writeAwsObject(JsonWriter jsonWriter, Set> metricNames) { + jsonWriter.writeFieldName("_aws"); + jsonWriter.writeStartObject(); + + jsonWriter.writeFieldName("Timestamp"); + jsonWriter.writeValue(clock.instant().toEpochMilli()); + + jsonWriter.writeFieldName("LogGroupName"); + jsonWriter.writeValue(config.logGroupName()); + + writeCloudWatchMetricsArray(jsonWriter, metricNames); + jsonWriter.writeEndObject(); + } + + private void writeCloudWatchMetricsArray(JsonWriter jsonWriter, Set> metricNames) { + jsonWriter.writeFieldName("CloudWatchMetrics"); + jsonWriter.writeStartArray(); + + writeCloudWatchMetricsObjects(jsonWriter, metricNames); + jsonWriter.writeEndArray(); + } + + private void writeCloudWatchMetricsObjects(JsonWriter jsonWriter, Set> metricNames) { + jsonWriter.writeStartObject(); + jsonWriter.writeFieldName("Namespace"); + jsonWriter.writeValue(config.namespace()); + + writeDimensionSetArray(jsonWriter); + + writeMetricDefinitionArray(jsonWriter, metricNames); + jsonWriter.writeEndObject(); + } + + private void writeDimensionSetArray(JsonWriter jsonWriter) { + jsonWriter.writeFieldName("Dimensions"); + jsonWriter.writeStartArray(); + jsonWriter.writeStartArray(); + for (String dimension : dimensions) { + jsonWriter.writeValue(dimension); + } + jsonWriter.writeEndArray(); + jsonWriter.writeEndArray(); + } + + private void writeMetricDefinitionArray(JsonWriter jsonWriter, Set> metricNames) { + jsonWriter.writeFieldName("Metrics"); + jsonWriter.writeStartArray(); + + metricNames.forEach(sdkMetric -> writeMetricDefinition(jsonWriter, sdkMetric)); + + jsonWriter.writeEndArray(); + } + + private void writeMetricDefinition(JsonWriter jsonWriter, SdkMetric sdkMetric) { + if (!isNumericMetric(sdkMetric)) { + return; + } + + jsonWriter.writeStartObject(); + jsonWriter.writeFieldName("Name"); + jsonWriter.writeValue(sdkMetric.name()); + + String unit = getMetricUnit(sdkMetric.valueClass()); + if (unit != null) { + jsonWriter.writeFieldName("Unit"); + jsonWriter.writeValue(unit); + } + + jsonWriter.writeEndObject(); + } + + private void writeMetricValues(JsonWriter jsonWriter, Map, List>> metrics) { + metrics.forEach((metric, records) -> { + if (isDimension(metric)) { + writeDimensionValue(jsonWriter, metric, records); + } else { + writeMetricRecord(jsonWriter, metric, records); + } + }); + } + + private void writeDimensionValue(JsonWriter jsonWriter, SdkMetric metric, List> records) { + Validate.validState(records.size() == 1 && String.class.isAssignableFrom(metric.valueClass()), + "Metric (%s) is configured as a dimension, and the value must be a single string", + metric.name()); + + jsonWriter.writeFieldName(metric.name()); + jsonWriter.writeValue((String) records.get(0).value()); + } + + private void writeMetricRecord(JsonWriter jsonWriter, SdkMetric metric, List> records) { + if (!isNumericMetric(metric)) { + return; + } + + jsonWriter.writeFieldName(metric.name()); + + if (records.size() == 1) { + processAndWriteValue(jsonWriter, records.get(0)); + } else { + writeMetricArray(jsonWriter, records); + } + } + + private boolean isNumericMetric(SdkMetric metric) { + return Integer.class.isAssignableFrom(metric.valueClass()) + || Boolean.class.isAssignableFrom(metric.valueClass()) + || Long.class.isAssignableFrom(metric.valueClass()) + || Duration.class.isAssignableFrom(metric.valueClass()) + || Double.class.isAssignableFrom(metric.valueClass()); + } + + + private void writeMetricArray(JsonWriter jsonWriter, List> records) { + jsonWriter.writeStartArray(); + for (MetricRecord mRecord : records) { + processAndWriteValue(jsonWriter, mRecord); + } + jsonWriter.writeEndArray(); + } + + + private boolean isDimension(SdkMetric metric) { + return config.dimensions().contains(metric); + } + + + private String getMetricUnit(Class type) { + if (Duration.class.isAssignableFrom(type)) { + return "Milliseconds"; + } + return null; + } + + private boolean shouldReport(MetricRecord metricRecord) { + return isSupportedCategory(metricRecord) && isSupportedLevel(metricRecord); + } + + private boolean isSupportedCategory(MetricRecord metricRecord) { + return metricCategoriesContainsAll || + metricRecord.metric() + .categories() + .stream() + .anyMatch(config.metricCategories()::contains); + } + + private boolean isSupportedLevel(MetricRecord metricRecord) { + return config.metricLevel().includesLevel(metricRecord.metric().level()); + } +} 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 new file mode 100644 index 000000000000..f3da12abfb4c --- /dev/null +++ b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/EmfMetricLoggingPublisherTest.java @@ -0,0 +1,100 @@ +/* + * 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.metrics.publishers.emf; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.http.HttpMetric; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.metrics.MetricCollector; +import software.amazon.awssdk.metrics.MetricLevel; +import software.amazon.awssdk.testutils.LogCaptor; +import software.amazon.awssdk.utils.internal.SystemSettingUtilsTestBackdoor; + + +public class EmfMetricLoggingPublisherTest extends LogCaptor.LogCaptorTestBase{ + + private EmfMetricLoggingPublisher.Builder publisherBuilder; + + + @BeforeEach + void setUp() { + publisherBuilder = EmfMetricLoggingPublisher.builder(); + } + + @Test + void Publish_noLogGroupName_throwException() { + assertThatThrownBy(() -> { + EmfMetricLoggingPublisher publisher = publisherBuilder.build(); + publisher.publish(null); + }) + .isInstanceOf(NullPointerException.class) + .hasMessage("logGroupName must not be null."); + } + + @Test + void Publish_noLogGroupNameInLambda_defaultLogGroupName() { + SystemSettingUtilsTestBackdoor.addEnvironmentVariableOverride("AWS_LAMBDA_LOG_GROUP_NAME", "/aws/lambda/testFunction"); + EmfMetricLoggingPublisher publisher = publisherBuilder.build(); + MetricCollector metricCollector = MetricCollector.create("test"); + publisher.publish(metricCollector.collect()); + assertThat(loggedEvents()).hasSize(2); + assertThat(loggedEvents().get(1).toString()).contains("/aws/lambda/testFunction"); + SystemSettingUtilsTestBackdoor.clearEnvironmentVariableOverrides(); + } + + @Test + void Publish_nullMetrics() { + EmfMetricLoggingPublisher publisher = publisherBuilder.logGroupName("/aws/lambda/emfMetricTest").build(); + publisher.publish(null); + assertThat(loggedEvents()).hasSize(1); + } + + @Test + void Publish_metricCollectionWithChild() { + EmfMetricLoggingPublisher publisher = publisherBuilder.dimensions(HttpMetric.HTTP_CLIENT_NAME) + .logGroupName("/aws/lambda/emfMetricTest") + .namespace("ExampleSDKV2MetricsEmf") + .metricLevel(MetricLevel.INFO) + .metricCategories(MetricCategory.HTTP_CLIENT) + .build(); + + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.HTTP_CLIENT_NAME, "apache-http-client"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + MetricCollector childMetricCollector1 = metricCollector.createChild("child1"); + childMetricCollector1.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(100)); + MetricCollector childMetricCollector2 = metricCollector.createChild("child2"); + childMetricCollector2.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(200)); + publisher.publish(metricCollector.collect()); + assertThat(loggedEvents()).hasSize(4); + } + + @Test + void Publish_multipleMetrics() { + EmfMetricLoggingPublisher publisher = publisherBuilder.logGroupName("/aws/lambda/emfMetricTest").build(); + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + metricCollector.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(100)); + publisher.publish(metricCollector.collect()); + assertThat(loggedEvents()).hasSize(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 new file mode 100644 index 000000000000..07473f5afc02 --- /dev/null +++ b/metric-publishers/emf-metric-logging-publisher/src/test/java/software/amazon/awssdk/metrics/publishers/emf/internal/MetricEmfConverterTest.java @@ -0,0 +1,236 @@ +/* + * 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.metrics.publishers.emf.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.metrics.CoreMetric; +import software.amazon.awssdk.http.HttpMetric; +import software.amazon.awssdk.metrics.MetricCategory; +import software.amazon.awssdk.metrics.MetricCollector; +import software.amazon.awssdk.metrics.MetricLevel; +import software.amazon.awssdk.metrics.SdkMetric; +import software.amazon.awssdk.utils.IoUtils; + +public class MetricEmfConverterTest { + + private MetricEmfConverter metricEmfConverterDefault; + private MetricEmfConverter metricEmfConverterCustom; + private final EmfMetricConfiguration testConfigDefault = new EmfMetricConfiguration.Builder() + .logGroupName("my_log_group_name") + .build(); + private final EmfMetricConfiguration testConfigCustom = new EmfMetricConfiguration.Builder() + .namespace("AwsSdk/Test/JavaSdk2") + .logGroupName("my_log_group_name") + .dimensions(Stream.of(HttpMetric.HTTP_CLIENT_NAME).collect(Collectors.toSet())) + .metricCategories(Stream.of(MetricCategory.HTTP_CLIENT).collect(Collectors.toSet())) + .metricLevel(MetricLevel.TRACE) + .build(); + private final Clock fixedClock = Clock.fixed( + Instant.ofEpochMilli(12345678), + ZoneOffset.UTC + ); + + @BeforeEach + void setUp() { + metricEmfConverterDefault = new MetricEmfConverter(testConfigDefault,fixedClock); + metricEmfConverterCustom = new MetricEmfConverter(testConfigCustom, fixedClock); + } + + @Test + void ConvertMetricCollectionToEMF_emptyCollection() { + List emfLogs = metricEmfConverterDefault.convertMetricCollectionToEmf(MetricCollector.create("test").collect()); + + assertThat(emfLogs).containsOnly("{\"_aws\":{\"Timestamp\":12345678,\"LogGroupName\":\"my_log_group_name\"," + + "\"CloudWatchMetrics\":[{\"Namespace\":\"AwsSdk/JavaSdk2\",\"Dimensions\":[[]]," + + "\"Metrics\":[]}]}}"); + } + + @Test + void ConvertMetricCollectionToEMF_multipleMetrics() { + + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + metricCollector.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(100)); + 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\"}," + + "{\"Name\":\"ConcurrencyAcquireDuration\",\"Unit\":\"Milliseconds\"}]}]},\"AvailableConcurrency\":5,\"ConcurrencyAcquireDuration\":100.0}"); + } + + + @Test + void ConvertMetricCollectionToEMF_oneDimension() { + + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(CoreMetric.OPERATION_NAME, "operationName"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + metricCollector.reportMetric(HttpMetric.HTTP_STATUS_CODE, 404); + List emfLogs = metricEmfConverterDefault.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).containsOnly("{\"_aws\":{\"Timestamp\":12345678,\"LogGroupName\":\"my_log_group_name\"," + + "\"CloudWatchMetrics\":[{\"Namespace\":\"AwsSdk/JavaSdk2\"," + + "\"Dimensions\":[[\"OperationName\"]]," + + "\"Metrics\":[{\"Name\":\"AvailableConcurrency\"}]}]}," + + "\"AvailableConcurrency\":5," + + "\"OperationName\":\"operationName\"}"); + } + + @Test + void ConvertMetricCollectionToEMF_metricCategoryConfigured_onlyHttpMetric() { + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + metricCollector.reportMetric(CoreMetric.SERVICE_ID, "serviceId"); + List emfLogs = metricEmfConverterCustom.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).containsOnly("{\"_aws\":{\"Timestamp\":12345678,\"LogGroupName\":\"my_log_group_name\",\"CloudWatchMetrics\":[{\"Namespace\":" + + "\"AwsSdk/Test/JavaSdk2\",\"Dimensions\":[[]]," + + "\"Metrics\":[{\"Name\":\"AvailableConcurrency\"}]}]},\"AvailableConcurrency\":5}"); + } + + @Test + void convertMetricCollectionToEmf_traceEnabled_shouldIncludeTrace() { + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + metricCollector.reportMetric(HttpMetric.HTTP_STATUS_CODE, 404); + List emfLogs = metricEmfConverterCustom.convertMetricCollectionToEmf(metricCollector.collect()); + + assertThat(emfLogs).containsOnly("{\"_aws\":{\"Timestamp\":12345678,\"LogGroupName\":\"my_log_group_name\"," + + "\"CloudWatchMetrics\":[{\"Namespace\":\"AwsSdk/Test/JavaSdk2\"" + + ",\"Dimensions\":[[]],\"Metrics\":[{\"Name\":\"AvailableConcurrency\"},{\"Name\":\"HttpStatusCode\"}]}]},\"AvailableConcurrency\":5,\"HttpStatusCode\":404}"); + } + + + + @Test + void ConvertMetricCollectionToEMF_multiChildCollections_recordList() { + + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.HTTP_CLIENT_NAME, "apache-http-client"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + MetricCollector childMetricCollector1 = metricCollector.createChild("child1"); + childMetricCollector1.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(100)); + MetricCollector childMetricCollector2 = metricCollector.createChild("child2"); + childMetricCollector2.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(200)); + 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\"}," + + "{\"Name\":\"ConcurrencyAcquireDuration\",\"Unit\":\"Milliseconds\"}]}]},\"AvailableConcurrency\":5,\"ConcurrencyAcquireDuration\":" + + "[100.0,200.0]}"); + } + + @Test + void ConvertMetricCollectionToEMF_recordsExceedsMaxSize_shouldDrop() { + + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.HTTP_CLIENT_NAME, "apache-http-client"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + + for (int i = 0; i < 120; i++) { + MetricCollector childMetricCollector = metricCollector.createChild("child" + i); + childMetricCollector.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(100+i)); + } + + 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\"},{\"Name\":\"ConcurrencyAcquireDuration\",\"Unit\":\"Milliseconds\"}]}]},\"AvailableConcurrency\":5," + + "\"ConcurrencyAcquireDuration\":[100.0,101.0,102.0,103.0,104.0,105.0,106.0,107.0,108.0,109.0,110.0,111.0,112.0,113.0,114.0,115.0,116.0,117.0,118.0,119.0,120.0,121.0,122.0," + + "123.0,124.0,125.0,126.0,127.0,128.0,129.0,130.0,131.0,132.0,133.0,134.0,135.0,136.0,137.0,138.0,139.0,140.0,141.0,142.0,143.0,144.0,145.0,146.0,147.0,148.0,149.0,150.0,151.0," + + "152.0,153.0,154.0,155.0,156.0,157.0,158.0,159.0,160.0,161.0,162.0,163.0,164.0,165.0,166.0,167.0,168.0,169.0,170.0,171.0,172.0,173.0,174.0,175.0,176.0,177.0,178.0,179.0,180.0,181.0," + + "182.0,183.0,184.0,185.0,186.0,187.0,188.0,189.0,190.0,191.0,192.0,193.0,194.0,195.0,196.0,197.0,198.0,199.0]}"); + + } + + @Test + void ConvertMetricCollectionToEMF_metricCollectionExceedsMaxSize_shouldSplit() { + + MetricCollector metricCollector = MetricCollector.create("test"); + for (int i = 0; i < 220; i++) { + metricCollector.reportMetric(SdkMetric.create("metric_" + i, Integer.class, MetricLevel.INFO, MetricCategory.CORE), i); + } + + List emfLogs = metricEmfConverterDefault.convertMetricCollectionToEmf(metricCollector.collect()); + assertThat(emfLogs).hasSize(3); + } + + @Test + void ConvertMetricCollectionToEMF_dropNonNumericMetrics() { + + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(CoreMetric.SERVICE_ID, "serviceId"); + metricCollector.reportMetric(CoreMetric.OPERATION_NAME, "operationName"); + metricCollector.reportMetric(SdkMetric.create("stringMetric", String.class, MetricLevel.INFO, MetricCategory.CORE), + "stringMetricValue"); + 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\":[[\"OperationName\",\"ServiceId\"]],\"Metrics\":[{\"Name\":\"AvailableConcurrency\"}]}]}," + + "\"AvailableConcurrency\":5,\"OperationName\":\"operationName\",\"ServiceId\":\"serviceId\"}"); + } + + @Test + void ConvertMetricCollectionToEMF_shouldConformToSchema() throws Exception { + + MetricCollector metricCollector = MetricCollector.create("test"); + metricCollector.reportMetric(HttpMetric.AVAILABLE_CONCURRENCY, 5); + metricCollector.reportMetric(CoreMetric.API_CALL_SUCCESSFUL, true); + metricCollector.reportMetric(CoreMetric.API_CALL_DURATION, java.time.Duration.ofMillis(100)); + metricCollector.reportMetric(CoreMetric.SERVICE_ID, "serviceId"); + metricCollector.reportMetric(CoreMetric.OPERATION_NAME, "operationName"); + metricCollector.reportMetric(HttpMetric.HTTP_CLIENT_NAME, "apache-http-client"); + MetricCollector childMetricCollector1 = metricCollector.createChild("child1"); + childMetricCollector1.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(100)); + MetricCollector childMetricCollector2 = metricCollector.createChild("child2"); + childMetricCollector2.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(200)); + MetricCollector childMetricCollector3 = metricCollector.createChild("child3"); + childMetricCollector3.reportMetric(HttpMetric.CONCURRENCY_ACQUIRE_DURATION, java.time.Duration.ofMillis(200)); + + List emfLogs = metricEmfConverterDefault.convertMetricCollectionToEmf(metricCollector.collect()); + + String jsonSchema = IoUtils.toUtf8String(Objects.requireNonNull(getClass().getResourceAsStream("/emfSchema.json"))); + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); + JsonSchema schema = factory.getSchema(jsonSchema); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode jsonNode = mapper.readTree(emfLogs.get(0)); + Set errors = schema.validate(jsonNode); + + assertThat(errors).isEmpty(); + } +} \ No newline at end of file diff --git a/metric-publishers/emf-metric-logging-publisher/src/test/resources/emfSchema.json b/metric-publishers/emf-metric-logging-publisher/src/test/resources/emfSchema.json new file mode 100644 index 000000000000..1ed7be988024 --- /dev/null +++ b/metric-publishers/emf-metric-logging-publisher/src/test/resources/emfSchema.json @@ -0,0 +1,123 @@ +{ + "type": "object", + "title": "Root Node", + "required": [ + "_aws" + ], + "properties": { + "_aws": { + "$id": "#/properties/_aws", + "type": "object", + "title": "Metadata", + "required": [ + "Timestamp", + "CloudWatchMetrics" + ], + "properties": { + "Timestamp": { + "$id": "#/properties/_aws/properties/Timestamp", + "type": "integer", + "title": "The Timestamp Schema", + "examples": [ + 1565375354953 + ] + }, + "CloudWatchMetrics": { + "$id": "#/properties/_aws/properties/CloudWatchMetrics", + "type": "array", + "title": "MetricDirectives", + "items": { + "$id": "#/properties/_aws/properties/CloudWatchMetrics/items", + "type": "object", + "title": "MetricDirective", + "required": [ + "Namespace", + "Dimensions", + "Metrics" + ], + "properties": { + "Namespace": { + "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Namespace", + "type": "string", + "title": "CloudWatch Metrics Namespace", + "examples": [ + "MyApp" + ], + "pattern": "^(.*)$", + "minLength": 1, + "maxLength": 1024 + }, + "Dimensions": { + "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions", + "type": "array", + "title": "The Dimensions Schema", + "minItems": 1, + "items": { + "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions/items", + "type": "array", + "title": "DimensionSet", + "minItems": 0, + "maxItems": 30, + "items": { + "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Dimensions/items/items", + "type": "string", + "title": "DimensionReference", + "examples": [ + "Operation" + ], + "pattern": "^(.*)$", + "minLength": 1, + "maxLength": 250 + } + } + }, + "Metrics": { + "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics", + "type": "array", + "title": "MetricDefinitions", + "items": { + "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items", + "type": "object", + "title": "MetricDefinition", + "required": [ + "Name" + ], + "properties": { + "Name": { + "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/Name", + "type": "string", + "title": "MetricName", + "examples": [ + "ProcessingLatency" + ], + "pattern": "^(.*)$", + "minLength": 1, + "maxLength": 1024 + }, + "Unit": { + "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/Unit", + "type": "string", + "title": "MetricUnit", + "examples": [ + "Milliseconds" + ], + "pattern": "^(Seconds|Microseconds|Milliseconds|Bytes|Kilobytes|Megabytes|Gigabytes|Terabytes|Bits|Kilobits|Megabits|Gigabits|Terabits|Percent|Count|Bytes\\/Second|Kilobytes\\/Second|Megabytes\\/Second|Gigabytes\\/Second|Terabytes\\/Second|Bits\\/Second|Kilobits\\/Second|Megabits\\/Second|Gigabits\\/Second|Terabits\\/Second|Count\\/Second|None)$" + }, + "StorageResolution": { + "$id": "#/properties/_aws/properties/CloudWatchMetrics/items/properties/Metrics/items/properties/StorageResolution", + "type": "integer", + "title": "StorageResolution", + "examples": [ + 60 + ] + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/metric-publishers/pom.xml b/metric-publishers/pom.xml index 5800a26658a0..1878d660e9fc 100644 --- a/metric-publishers/pom.xml +++ b/metric-publishers/pom.xml @@ -26,6 +26,7 @@ cloudwatch-metric-publisher + emf-metric-logging-publisher diff --git a/pom.xml b/pom.xml index 56dd62d53d86..aa9c00d648c9 100644 --- a/pom.xml +++ b/pom.xml @@ -144,6 +144,7 @@ 9.4.45.v20220203 1.14.15 1.3.0 + 1.5.4 3.1.2 @@ -655,6 +656,7 @@ netty-nio-client url-connection-client cloudwatch-metric-publisher + emf-metric-logging-publisher utils imds retries diff --git a/test/architecture-tests/archunit_store/18fc8858-1308-4d5d-b92d-87817d2fab53 b/test/architecture-tests/archunit_store/18fc8858-1308-4d5d-b92d-87817d2fab53 index 00043c4de219..0e7fc4de658f 100644 --- a/test/architecture-tests/archunit_store/18fc8858-1308-4d5d-b92d-87817d2fab53 +++ b/test/architecture-tests/archunit_store/18fc8858-1308-4d5d-b92d-87817d2fab53 @@ -14,14 +14,12 @@ Class de Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) -Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) -Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) @@ -50,7 +48,6 @@ Class depends on an int Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) -Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) Class depends on an internal API from a different module (Class ) diff --git a/test/architecture-tests/pom.xml b/test/architecture-tests/pom.xml index 562668933601..8e01fe730df0 100644 --- a/test/architecture-tests/pom.xml +++ b/test/architecture-tests/pom.xml @@ -51,6 +51,11 @@ software.amazon.awssdk ${awsjavasdk.version}
+ + emf-metric-logging-publisher + software.amazon.awssdk + ${awsjavasdk.version} + iam-policy-builder software.amazon.awssdk diff --git a/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java b/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java index 8259a12ca0db..62507546ec93 100644 --- a/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java +++ b/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/CodingConventionWithSuppressionTest.java @@ -32,6 +32,8 @@ import java.util.Set; import java.util.regex.Pattern; import org.junit.jupiter.api.Test; +import software.amazon.awssdk.metrics.publishers.emf.EmfMetricLoggingPublisher; +import software.amazon.awssdk.metrics.publishers.emf.internal.MetricEmfConverter; import software.amazon.awssdk.utils.Logger; /** @@ -48,7 +50,9 @@ public class CodingConventionWithSuppressionTest { *

* DO NOT ADD NEW EXCEPTIONS */ - private static final Set ALLOWED_WARN_LOG_SUPPRESSION = new HashSet<>(); + private static final Set ALLOWED_WARN_LOG_SUPPRESSION = new HashSet<>( + Arrays.asList(ArchUtils.classNameToPattern(EmfMetricLoggingPublisher.class), ArchUtils.classNameToPattern(MetricEmfConverter.class)) + ); /** * Suppressions for APIs used in generated code to avoid having to update archunit_store for new services. Unfortunately, we @@ -56,7 +60,9 @@ public class CodingConventionWithSuppressionTest { *

* DO NOT ADD NEW EXCEPTIONS */ - private static final Set ALLOWED_ERROR_LOG_SUPPRESSION = new HashSet<>(); + private static final Set ALLOWED_ERROR_LOG_SUPPRESSION = new HashSet<>( + Arrays.asList(ArchUtils.classNameToPattern(EmfMetricLoggingPublisher.class)) + ); @Test void shouldNotAbuseWarnLog() { diff --git a/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/InternalApiBoundaryTest.java b/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/InternalApiBoundaryTest.java index d41cc995f4e5..e9f9dbadc4ad 100644 --- a/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/InternalApiBoundaryTest.java +++ b/test/architecture-tests/src/test/java/software/amazon/awssdk/archtests/InternalApiBoundaryTest.java @@ -17,7 +17,6 @@ import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; -import com.tngtech.archunit.core.domain.Dependency; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; @@ -39,6 +38,7 @@ import software.amazon.awssdk.core.internal.waiters.WaiterAttribute; import software.amazon.awssdk.http.auth.aws.internal.signer.util.ChecksumUtil; import software.amazon.awssdk.utils.internal.EnumUtils; +import software.amazon.awssdk.utils.internal.SystemSettingUtils; /** * Ensure classes annotated with SdkInternalApis are not accessible outside the module. @@ -53,7 +53,8 @@ public class InternalApiBoundaryTest { */ private static final Set> ALLOWED_INTERNAL_API_ACROSS_MODULE_SUPPRESSION = new HashSet<>( Arrays.asList(WaiterAttribute.class, RequestCompression.class, RequestCompression.Builder.class, EnumUtils.class, - AwsServiceProtocol.class, AwsProtocolMetadata.class, MetricUtils.class, ChecksumUtil.class)); + AwsServiceProtocol.class, AwsProtocolMetadata.class, MetricUtils.class, SystemSettingUtils.class, + ChecksumUtil.class)); @Test void internalApi_shouldNotUsedAcrossModule() { diff --git a/test/sdk-benchmarks/pom.xml b/test/sdk-benchmarks/pom.xml index 367971c128fd..8858b820108a 100644 --- a/test/sdk-benchmarks/pom.xml +++ b/test/sdk-benchmarks/pom.xml @@ -205,11 +205,28 @@ ${awsjavasdk.version} compile + + software.amazon.awssdk + emf-metric-logging-publisher + ${awsjavasdk.version} + commons-cli commons-cli compile + + software.amazon.awssdk + cloudwatch + ${awsjavasdk.version} + compile + + + software.amazon.awssdk + cloudwatch-metric-publisher + ${awsjavasdk.version} + compile + diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/BenchmarkRunner.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/BenchmarkRunner.java index ca98ce400b6d..31fb84f78c22 100644 --- a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/BenchmarkRunner.java +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/BenchmarkRunner.java @@ -59,6 +59,7 @@ import software.amazon.awssdk.benchmark.enhanced.dynamodb.EnhancedClientQueryV1MapperComparisonBenchmark; import software.amazon.awssdk.benchmark.enhanced.dynamodb.EnhancedClientScanV1MapperComparisonBenchmark; import software.amazon.awssdk.benchmark.enhanced.dynamodb.EnhancedClientUpdateV1MapperComparisonBenchmark; +import software.amazon.awssdk.benchmark.metricpublisher.emf.EmfMetricPublisherBenchmark; import software.amazon.awssdk.benchmark.stats.SdkBenchmarkResult; import software.amazon.awssdk.benchmark.utils.BenchmarkProcessorOutput; import software.amazon.awssdk.utils.Logger; @@ -96,7 +97,13 @@ public class BenchmarkRunner { EnhancedClientQueryV1MapperComparisonBenchmark.class.getSimpleName() ); - private static final List METRIC_BENCHMARKS = Arrays.asList(MetricsEnabledBenchmark.class.getSimpleName()); + private static final List METRIC_BENCHMARKS = Arrays.asList( + MetricsEnabledBenchmark.class.getSimpleName() + ); + + private static final List METRIC_PUBLISHER_BENCHMARKS = Arrays.asList( + EmfMetricPublisherBenchmark.class.getSimpleName() + ); private static final Logger log = Logger.loggerFor(BenchmarkRunner.class); @@ -116,7 +123,9 @@ public static void main(String... args) throws Exception { benchmarksToRun.addAll(ASYNC_BENCHMARKS); benchmarksToRun.addAll(PROTOCOL_BENCHMARKS); benchmarksToRun.addAll(COLD_START_BENCHMARKS); - log.info(() -> "Skipping tests, to reduce benchmark times: \n" + MAPPER_BENCHMARKS + "\n" + METRIC_BENCHMARKS); + benchmarksToRun.addAll(METRIC_BENCHMARKS); + benchmarksToRun.addAll(METRIC_PUBLISHER_BENCHMARKS); + log.info(() -> "Skipping tests, to reduce benchmark times: \n" + MAPPER_BENCHMARKS); BenchmarkRunner runner = new BenchmarkRunner(benchmarksToRun, parseOptions(args)); diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/MetricsEnabledBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/MetricsEnabledBenchmark.java index c75876c8686b..42a7ed2fa41a 100644 --- a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/MetricsEnabledBenchmark.java +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/apicall/MetricsEnabledBenchmark.java @@ -60,7 +60,7 @@ public void setup() throws Exception { enabledMetricsAsyncClient = enableMetrics(asyncClientBuilder()).build(); } - private > T enableMetrics(T syncClientBuilder) { + protected > T enableMetrics(T syncClientBuilder) { return syncClientBuilder.overrideConfiguration(c -> c.addMetricPublisher(new EnabledPublisher())); } diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/metricpublisher/cloudwatch/CloudWatchMetricPublisherBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/metricpublisher/cloudwatch/CloudWatchMetricPublisherBenchmark.java new file mode 100644 index 000000000000..55d0a8911d34 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/metricpublisher/cloudwatch/CloudWatchMetricPublisherBenchmark.java @@ -0,0 +1,58 @@ +/* + * 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.benchmark.metricpublisher.cloudwatch; + +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import software.amazon.awssdk.benchmark.apicall.MetricsEnabledBenchmark; +import software.amazon.awssdk.benchmark.utils.NoOpCloudWatchAsyncClient; +import software.amazon.awssdk.core.client.builder.SdkClientBuilder; +import software.amazon.awssdk.metrics.publishers.cloudwatch.CloudWatchMetricPublisher; +import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.Throughput) +public class CloudWatchMetricPublisherBenchmark extends MetricsEnabledBenchmark { + private CloudWatchMetricPublisher cwPublisher; + + @Override + @Setup(Level.Trial) + public void setup() throws Exception { + super.setup(); + CloudWatchAsyncClient noOpClient = new NoOpCloudWatchAsyncClient(); + cwPublisher = CloudWatchMetricPublisher.builder() + .cloudWatchClient(noOpClient) + .namespace("CloudWatchMetricPublisherBenchmark") + .build(); + } + + @Override + protected > T enableMetrics(T clientBuilder) { + return clientBuilder.overrideConfiguration(c -> c.addMetricPublisher(cwPublisher)); + } + + @Override + @TearDown(Level.Trial) + public void tearDown() throws Exception { + super.tearDown(); + cwPublisher.close(); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/metricpublisher/emf/EmfMetricPublisherBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/metricpublisher/emf/EmfMetricPublisherBenchmark.java new file mode 100644 index 000000000000..2188d1fac4a2 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/metricpublisher/emf/EmfMetricPublisherBenchmark.java @@ -0,0 +1,55 @@ +/* + * 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.benchmark.metricpublisher.emf; + +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import software.amazon.awssdk.benchmark.apicall.MetricsEnabledBenchmark; +import software.amazon.awssdk.core.client.builder.SdkClientBuilder; +import software.amazon.awssdk.metrics.publishers.emf.EmfMetricLoggingPublisher; + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.Throughput) +public class EmfMetricPublisherBenchmark extends MetricsEnabledBenchmark { + private EmfMetricLoggingPublisher emfMetricLoggingPublisher; + + @Override + @Setup(Level.Trial) + public void setup() throws Exception { + emfMetricLoggingPublisher = EmfMetricLoggingPublisher.builder() + .namespace("EmfMetricPublisherBenchmark") + .logGroupName("LogGroupName") + .build(); + super.setup(); + } + + @Override + protected > T enableMetrics(T clientBuilder) { + return clientBuilder.overrideConfiguration(c -> c.addMetricPublisher(emfMetricLoggingPublisher)); + } + + @Override + @TearDown(Level.Trial) + public void tearDown() throws Exception { + super.tearDown(); + emfMetricLoggingPublisher.close(); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/metricpublisher/logging/LoggingMetricPublisherBenchmark.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/metricpublisher/logging/LoggingMetricPublisherBenchmark.java new file mode 100644 index 000000000000..ccdd1f6b8751 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/metricpublisher/logging/LoggingMetricPublisherBenchmark.java @@ -0,0 +1,52 @@ +/* + * 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.benchmark.metricpublisher.logging; + +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import software.amazon.awssdk.benchmark.apicall.MetricsEnabledBenchmark; +import software.amazon.awssdk.core.client.builder.SdkClientBuilder; +import software.amazon.awssdk.metrics.LoggingMetricPublisher; + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.Throughput) +public class LoggingMetricPublisherBenchmark extends MetricsEnabledBenchmark { + private LoggingMetricPublisher loggingMetricPublisher; + + @Override + @Setup(Level.Trial) + public void setup() throws Exception { + loggingMetricPublisher = LoggingMetricPublisher.create(); + super.setup(); + } + + @Override + protected > T enableMetrics(T clientBuilder) { + return clientBuilder.overrideConfiguration(c -> c.addMetricPublisher(loggingMetricPublisher)); + } + + @Override + @TearDown(Level.Trial) + public void tearDown() throws Exception { + super.tearDown(); + loggingMetricPublisher.close(); + } +} diff --git a/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/utils/NoOpCloudWatchAsyncClient.java b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/utils/NoOpCloudWatchAsyncClient.java new file mode 100644 index 000000000000..0fa4093c8cb3 --- /dev/null +++ b/test/sdk-benchmarks/src/main/java/software/amazon/awssdk/benchmark/utils/NoOpCloudWatchAsyncClient.java @@ -0,0 +1,38 @@ +/* + * 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. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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.awssdk.benchmark.utils; + +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient; +import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataRequest; +import software.amazon.awssdk.services.cloudwatch.model.PutMetricDataResponse; + +public class NoOpCloudWatchAsyncClient implements CloudWatchAsyncClient { + + @Override + public CompletableFuture putMetricData(PutMetricDataRequest request) { + return CompletableFuture.completedFuture(PutMetricDataResponse.builder().build()); + } + + @Override + public String serviceName() { + return "cloudwatch"; + } + + @Override + public void close() { + } +} diff --git a/test/tests-coverage-reporting/pom.xml b/test/tests-coverage-reporting/pom.xml index ac1908d44bd3..6b35471e5307 100644 --- a/test/tests-coverage-reporting/pom.xml +++ b/test/tests-coverage-reporting/pom.xml @@ -227,6 +227,11 @@ software.amazon.awssdk ${awsjavasdk.version} + + emf-metric-logging-publisher + software.amazon.awssdk + ${awsjavasdk.version} + iam-policy-builder software.amazon.awssdk diff --git a/metric-publishers/cloudwatch-metric-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/cloudwatch/internal/transform/MetricValueNormalizer.java b/utils/src/main/java/software/amazon/awssdk/utils/MetricValueNormalizer.java similarity index 86% rename from metric-publishers/cloudwatch-metric-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/cloudwatch/internal/transform/MetricValueNormalizer.java rename to utils/src/main/java/software/amazon/awssdk/utils/MetricValueNormalizer.java index 2767c39379a9..a8472db74a73 100644 --- a/metric-publishers/cloudwatch-metric-publisher/src/main/java/software/amazon/awssdk/metrics/publishers/cloudwatch/internal/transform/MetricValueNormalizer.java +++ b/utils/src/main/java/software/amazon/awssdk/utils/MetricValueNormalizer.java @@ -13,12 +13,12 @@ * permissions and limitations under the License. */ -package software.amazon.awssdk.metrics.publishers.cloudwatch.internal.transform; +package software.amazon.awssdk.utils; -import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.SdkProtectedApi; -@SdkInternalApi -class MetricValueNormalizer { +@SdkProtectedApi +public final class MetricValueNormalizer { /** * Really small values (close to 0) result in CloudWatch failing with an "unsupported value" error. Make sure that we floor * those values to 0 to prevent that error.