diff --git a/annotations/build.gradle b/annotations/build.gradle new file mode 100644 index 00000000..5ccfea33 --- /dev/null +++ b/annotations/build.gradle @@ -0,0 +1,70 @@ +plugins { + id 'java' + id 'java-library' + id "io.freefair.aspectj.post-compile-weaving" version "6.4.3" + id 'com.diffplug.spotless' version '5.8.2' +} + +repositories { + jcenter() + mavenCentral() +} + +dependencies { + implementation rootProject + + annotationProcessor 'org.projectlombok:lombok:1.18.24' + compileOnly 'org.projectlombok:lombok:1.18.12' + + implementation "org.aspectj:aspectjrt:1.9.22" + implementation "org.aspectj:aspectjweaver:1.9.8.RC3" + + implementation 'com.fasterxml.jackson.core:jackson-core:2.14.2' + implementation 'com.fasterxml.jackson.core:jackson-annotations:2.14.2' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.2' + implementation 'org.slf4j:slf4j-api:2.0.6' + implementation 'org.javatuples:javatuples:1.2' + implementation 'org.apache.commons:commons-lang3:3.12.0' + + // Use JUnit test framework + testImplementation 'software.amazon.awssdk:cloudwatch:2.20.13' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.2' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.2' + testImplementation 'org.junit.vintage:junit-vintage-engine:5.9.2' + testImplementation 'org.mockito:mockito-core:2.+' + testImplementation 'org.powermock:powermock-module-junit4:2.0.9' + testImplementation 'org.powermock:powermock-api-mockito2:2.0.9' + testImplementation 'com.github.javafaker:javafaker:1.0.2' + testImplementation 'com.github.tomakehurst:wiremock-jre8:2.35.0' + testCompileOnly 'org.projectlombok:lombok:1.18.26' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.26' +} + +compileTestJava.ajc.options.aspectpath.from sourceSets.main.output + +compileJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +spotless { + format 'misc', { + target '*.gradle', '*.md', '.gitignore' + + trimTrailingWhitespace() + indentWithTabs() + endWithNewline() + } + + java { + importOrder() + googleJavaFormat('1.7').aosp() + removeUnusedImports() + } +} + +test { + outputs.upToDateWhen {false} + useJUnitPlatform() +} diff --git a/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/CountMetric.java b/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/CountMetric.java new file mode 100644 index 00000000..882f6cc0 --- /dev/null +++ b/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/CountMetric.java @@ -0,0 +1,31 @@ +package software.amazon.cloudwatchlogs.emf.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is used to put a count metric to CloudWatch Metrics. By default, when the + * annotated method is called, the value 1.0 will be published with the metric name + * "[ClassName].[methodName].Count". The value and metric name can be overridden, and the "applies" + * field can be used to only publish for invocations when failures are/aren't thrown by the + * annotated method. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Repeatable(CountMetrics.class) +public @interface CountMetric { + String name() default ""; + + boolean logSuccess() default true; + + Class[] logExceptions() default {Throwable.class}; + + double value() default 1.0d; + + String logger() default "_defaultLogger"; + + boolean flush() default false; +} diff --git a/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/CountMetrics.java b/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/CountMetrics.java new file mode 100644 index 00000000..9fa2f526 --- /dev/null +++ b/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/CountMetrics.java @@ -0,0 +1,13 @@ +package software.amazon.cloudwatchlogs.emf.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Allows multiple {@link CountMetric} annotations on a single method. */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface CountMetrics { + CountMetric[] value(); +} diff --git a/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/MetricAnnotationMediator.java b/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/MetricAnnotationMediator.java new file mode 100644 index 00000000..d8b422d2 --- /dev/null +++ b/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/MetricAnnotationMediator.java @@ -0,0 +1,81 @@ +/* + * 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.annotations; + +import java.util.HashMap; +import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; + +/** */ +public class MetricAnnotationMediator { + public static MetricAnnotationMediator getInstance() { + return SINGLETON; + } + + private static final MetricAnnotationMediator SINGLETON = new MetricAnnotationMediator(); + + private static final String defaultLoggerKey = "_defaultLogger"; + + // protected instead of private for testing purposes + protected static HashMap loggers; + + private MetricAnnotationMediator() { + loggers = new HashMap<>(); + loggers.put(defaultLoggerKey, new MetricsLogger()); + } + + /** @return the default logger this singleton uses */ + public static MetricsLogger getDefaultLogger() { + return loggers.get(defaultLoggerKey); + } + + /** + * @param name the name of the logger to get + * @return the logger with the specified name if it exists, otherwise will return the default + * logger + * @see MetricAnnotationMediator#getDefaultLogger() getDefaultLogger() + */ + public static MetricsLogger getLogger(String name) { + if (name.isEmpty()) { + return getDefaultLogger(); + } + return loggers.getOrDefault(name, getDefaultLogger()); + } + + /** + * Adds a logger to this singleton if no other logger has the same name. + * + * @param name the desired name of the logger + * @param logger the logger to be added + * @return true if the logger was successfully added and false if annother logger already has + * the same name + */ + public static boolean addLogger(String name, MetricsLogger logger) { + if (loggers.containsKey(name)) { + return false; + } + + loggers.put(name, logger); + return true; + } + + /** Flushes all loggers added to this singleton */ + public static void flushAll() { + for (MetricsLogger logger : loggers.values()) { + logger.flush(); + } + } +} diff --git a/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/MetricAnnotationProcessor.java b/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/MetricAnnotationProcessor.java new file mode 100644 index 00000000..7a0feba7 --- /dev/null +++ b/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/MetricAnnotationProcessor.java @@ -0,0 +1,206 @@ +package software.amazon.cloudwatchlogs.emf.annotations; + +import java.lang.reflect.Method; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import software.amazon.cloudwatchlogs.emf.logger.MetricsLogger; +import software.amazon.cloudwatchlogs.emf.model.Unit; + +@Aspect +public class MetricAnnotationProcessor { + /** private struct used to translate all annotations to be handled the same */ + @AllArgsConstructor + @Builder // For testing + @Getter + protected static class AnnotationTranslator { + private final String name; + private final Boolean logSuccess; + private final Class[] logExceptions; + private final Boolean flush; + private final String logger; + private final double value; + private final String defaultName; + private final Unit unit; + + public AnnotationTranslator(CountMetric annotation) { + this.name = annotation.name(); + this.logSuccess = annotation.logSuccess(); + this.logExceptions = annotation.logExceptions(); + this.flush = annotation.flush(); + this.logger = annotation.logger(); + this.value = annotation.value(); + this.defaultName = "Count"; + this.unit = Unit.COUNT; + } + + public AnnotationTranslator(TimeMetric annotation, double time) { + this.name = annotation.name(); + this.logSuccess = annotation.logSuccess(); + this.logExceptions = annotation.logExceptions(); + this.flush = annotation.flush(); + this.logger = annotation.logger(); + this.value = time; + this.defaultName = "Time"; + this.unit = Unit.MILLISECONDS; + } + } + + /** + * Puts a metric with the method count based on the parameters provided in the annotation. + * + * @param point The point for the annotated method. + * @return The result of the method call. + * @throws Throwable if the method fails. + */ + @Around( + "execution(* *(..)) && @annotation(software.amazon.cloudwatchlogs.emf.annotations.CountMetric)") + public Object aroundCountMetric(final ProceedingJoinPoint point) throws Throwable { + + // Execute the method and capture whether a throwable is thrown. + Throwable throwable = null; + try { + return point.proceed(); + } catch (final Throwable t) { + throwable = t; + throw t; + } finally { + final Method method = ((MethodSignature) point.getSignature()).getMethod(); + final CountMetric countMetricAnnotation = method.getAnnotation(CountMetric.class); + + handle(throwable, method, new AnnotationTranslator(countMetricAnnotation)); + } + } + + /** + * Puts a metric with the method count based on the parameters provided in the annotation. + * + * @param point The point for the annotated method. + * @return The result of the method call. + * @throws Throwable if the method fails. + */ + @Around( + "execution(* *(..)) && @annotation(software.amazon.cloudwatchlogs.emf.annotations.CountMetrics)") + public Object aroundCountMetrics(final ProceedingJoinPoint point) throws Throwable { + + // Execute the method and capture whether a throwable is thrown. + Throwable throwable = null; + try { + return point.proceed(); + } catch (final Throwable t) { + throwable = t; + throw t; + } finally { + final Method method = ((MethodSignature) point.getSignature()).getMethod(); + final CountMetrics countMetricsAnnotation = method.getAnnotation(CountMetrics.class); + + for (CountMetric countMetricAnnotation : countMetricsAnnotation.value()) { + handle(throwable, method, new AnnotationTranslator(countMetricAnnotation)); + } + } + } + + /** + * Puts a metric with the method time based on the parameters provided in the annotation. + * + * @param point The point for the annotated method. + * @return The result of the method call. + * @throws Throwable if the method fails. + */ + @Around( + "execution(* *(..)) && @annotation(software.amazon.cloudwatchlogs.emf.annotations.TimeMetric)") + public Object aroundTimeMetric(final ProceedingJoinPoint point) throws Throwable { + + // Execute the method and capture whether a throwable is thrown. + final double startTime = System.currentTimeMillis(); // capture the start time + Throwable throwable = null; + try { + return point.proceed(); + } catch (final Throwable t) { + throwable = t; + throw t; + } finally { + final double time = System.currentTimeMillis() - startTime; // capture the total time + final Method method = ((MethodSignature) point.getSignature()).getMethod(); + final TimeMetric timeMetricAnnotation = method.getAnnotation(TimeMetric.class); + + handle(throwable, method, new AnnotationTranslator(timeMetricAnnotation, time)); + } + } + + /** + * Puts a metric with the method time based on the parameters provided in the annotation. + * + * @param point The point for the annotated method. + * @return The result of the method call. + * @throws Throwable if the method fails. + */ + @Around( + "execution(* *(..)) && @annotation(software.amazon.cloudwatchlogs.emf.annotations.TimeMetrics)") + public Object aroundTimeMetrics(final ProceedingJoinPoint point) throws Throwable { + + // Execute the method and capture whether a throwable is thrown. + final double startTime = System.currentTimeMillis(); // capture the start time + Throwable throwable = null; + try { + return point.proceed(); + } catch (final Throwable t) { + throwable = t; + throw t; + } finally { + final double time = System.currentTimeMillis() - startTime; // capture the total time + final Method method = ((MethodSignature) point.getSignature()).getMethod(); + final TimeMetrics timeMetricsAnnotation = method.getAnnotation(TimeMetrics.class); + + for (TimeMetric timeMetricAnnotation : timeMetricsAnnotation.value()) { + handle(throwable, method, new AnnotationTranslator(timeMetricAnnotation, time)); + } + } + } + + /** Handles the logging of all metrics related to an annotation and method */ + protected static void handle( + Throwable throwable, Method method, AnnotationTranslator translator) { + if (shouldLog(throwable, translator)) { + final String metricName = getName(method, translator); + final double value = translator.getValue(); + + MetricsLogger logger = MetricAnnotationMediator.getLogger(translator.getLogger()); + logger.putMetric(metricName, value, translator.getUnit()); + + if (translator.getFlush()) { + MetricAnnotationMediator.flushAll(); + } + } + } + + /** + * @return true if this annotation's metric should be logged according to the rules defined in + * the annotation. + */ + protected static boolean shouldLog(Throwable throwable, AnnotationTranslator translator) { + boolean shouldLog = false; + for (final Class failureClass : translator.getLogExceptions()) { + shouldLog |= failureClass.isInstance(throwable); + } + + shouldLog |= throwable == null && translator.getLogSuccess(); + + return shouldLog; + } + + /** @return the name of the metric based on the annotation and method */ + protected static String getName(Method method, AnnotationTranslator translator) { + return translator.getName().isEmpty() + ? String.format( + "%s.%s.%s", + method.getDeclaringClass().getSimpleName(), + method.getName(), + translator.getDefaultName()) + : translator.getName(); + } +} diff --git a/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/TimeMetric.java b/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/TimeMetric.java new file mode 100644 index 00000000..0ecb14f4 --- /dev/null +++ b/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/TimeMetric.java @@ -0,0 +1,28 @@ +package software.amazon.cloudwatchlogs.emf.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation is used to put a duration metric to CloudWatch Metrics. By default, when the + * annotated method is called, the duration (in milliseconds) will be published with the metric name + * "[ClassName].[methodName].Time". The metric name can be overridden, and the "applies" field can + * be used to only publish for invocations when failures are/aren't thrown by the annotated method. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Repeatable(TimeMetrics.class) +public @interface TimeMetric { + String name() default ""; + + boolean logSuccess() default true; + + Class[] logExceptions() default {Throwable.class}; + + String logger() default "_defaultLogger"; + + boolean flush() default false; +} diff --git a/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/TimeMetrics.java b/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/TimeMetrics.java new file mode 100644 index 00000000..3ecd9f9f --- /dev/null +++ b/annotations/src/main/java/software/amazon/cloudwatchlogs/emf/annotations/TimeMetrics.java @@ -0,0 +1,13 @@ +package software.amazon.cloudwatchlogs.emf.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Allows multiple {@link TimeMetric} annotations on a single method. */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface TimeMetrics { + TimeMetric[] value(); +} diff --git a/annotations/src/test/java/software/amazon/cloudwatchlogs/emf/annotations/MetricAnnotationMediatorTest.java b/annotations/src/test/java/software/amazon/cloudwatchlogs/emf/annotations/MetricAnnotationMediatorTest.java new file mode 100644 index 00000000..1cdcb7cc --- /dev/null +++ b/annotations/src/test/java/software/amazon/cloudwatchlogs/emf/annotations/MetricAnnotationMediatorTest.java @@ -0,0 +1,284 @@ +/* + * 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.annotations; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.json.JsonMapper; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.cloudwatchlogs.emf.environment.Environment; +import software.amazon.cloudwatchlogs.emf.environment.EnvironmentProvider; +import software.amazon.cloudwatchlogs.emf.exception.DimensionSetExceededException; +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.logger.MetricsLogger; +import software.amazon.cloudwatchlogs.emf.model.Unit; +import software.amazon.cloudwatchlogs.emf.sinks.SinkShunt; + +class MetricAnnotationMediatorTest { + private MetricsLogger logger; + private EnvironmentProvider envProvider; + private SinkShunt sink; + private Environment environment; + private final Random random = new Random(); + + @BeforeEach + public void setUp() { + envProvider = mock(EnvironmentProvider.class); + environment = mock(Environment.class); + sink = new SinkShunt(); + + when(envProvider.resolveEnvironment()) + .thenReturn(CompletableFuture.completedFuture(environment)); + when(environment.getSink()).thenReturn(sink); + when(environment.getLogGroupName()).thenReturn("test-log-group"); + when(environment.getName()).thenReturn("test-env-name"); + when(environment.getType()).thenReturn("test-env-type"); + + logger = new MetricsLogger(envProvider); + MetricAnnotationMediator.loggers.put("_defaultLogger", logger); + } + + @Test + void testCountMetricAnnotation() + throws InvalidDimensionException, DimensionSetExceededException, + JsonProcessingException { + for (int i = 0; i < 10; i++) { + countMethod(); + } + + MetricAnnotationMediator.flushAll(); + + for (String log : sink.getLogEvents()) { + System.out.println(log); + ArrayList metricNames = parseMetricNames(log); + Assertions.assertEquals( + "MetricAnnotationMediatorTest.countMethod.Count", metricNames.get(0)); + Assertions.assertEquals( + Arrays.asList(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0), + (ArrayList) parseMetricByName(log, metricNames.get(0))); + } + } + + @Test + void testTimeMetricAnnotation() throws JsonProcessingException { + MetricAnnotationMediator.addLogger("example logger", new MetricsLogger(envProvider)); + + for (int i = 0; i < 5; i++) { + timeMethod(); + } + + multiAnnotationMethod(); + MetricAnnotationMediator.flushAll(); + + for (String log : sink.getLogEvents()) { + System.out.println(log); + ArrayList metricNames = parseMetricNames(log); + Assertions.assertEquals( + "MetricAnnotationMediatorTest.timeMethod.Time", metricNames.get(0)); + ArrayList metricValues = + (ArrayList) parseMetricByName(log, metricNames.get(0)); + assertTrue( + metricValues.stream() + .allMatch(value -> value >= 20 && value <= 300)); // room for error + } + } + + @Test + void testNamedMetricAnnotation() throws JsonProcessingException { + MetricsLogger namedLogger = new MetricsLogger(envProvider); + MetricAnnotationMediator.addLogger("example logger", namedLogger); + + for (int i = 0; i < 10; i++) { + countNamedLogger(); + } + + MetricAnnotationMediator.flushAll(); + + boolean anyMatch = false; + System.out.println(sink.getLogEvents()); + for (String log : sink.getLogEvents()) { + System.out.println(log); + try { + ArrayList metricNames = parseMetricNames(log); + Assertions.assertEquals( + "MetricAnnotationMediatorTest.countNamedLogger.Count", metricNames.get(0)); + Assertions.assertEquals( + Arrays.asList(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0), + (ArrayList) parseMetricByName(log, metricNames.get(0))); + anyMatch = true; + } catch (Exception e) { + } + } + assertTrue(anyMatch); + } + + @Test + void testMultiAnnotationMethod() throws JsonProcessingException { + multiCountMethod(); + + MetricAnnotationMediator.flushAll(); + + ArrayList metricNames = parseMetricNames(sink.getLogEvents().get(0)); + Assertions.assertEquals( + Stream.of("multiCount1", "multiCount2", "multiCount3").collect(Collectors.toSet()), + metricNames.stream().collect(Collectors.toSet())); + } + + @Test + void testCountAndTimeMethod() throws JsonProcessingException { + countAndTimeMethod(); + + MetricAnnotationMediator.flushAll(); + + ArrayList metricNames = parseMetricNames(sink.getLogEvents().get(0)); + Assertions.assertEquals( + Stream.of("Time", "Count").collect(Collectors.toSet()), + metricNames.stream().collect(Collectors.toSet())); + } + + @Test + void testDontLogSuccess() throws NoSuchMethodException { + MetricAnnotationProcessor.AnnotationTranslator translator = + getAnnotationTranslatorBuilder().logSuccess(false).build(); + Method method = MetricAnnotationMediatorTest.class.getMethod("dummyMethod"); + + MetricAnnotationProcessor.handle(null, method, translator); + + assertTrue(sink.getLogEvents().isEmpty()); + } + + @Test + void testLogException() throws NoSuchMethodException { + MetricAnnotationProcessor.AnnotationTranslator translator = + getAnnotationTranslatorBuilder() + .logSuccess(false) + .logExceptions( + new Class[] { + InvalidMetricException.class, InvalidNamespaceException.class + }) + .build(); + Method method = MetricAnnotationMediatorTest.class.getMethod("dummyMethod"); + + MetricAnnotationProcessor.handle(new InvalidDimensionException(""), method, translator); + + assertTrue(sink.getLogEvents().isEmpty()); + sink.getLogEvents().clear(); + + MetricAnnotationProcessor.handle(new InvalidMetricException(""), method, translator); + + Assertions.assertFalse(sink.getLogEvents().isEmpty()); + sink.getLogEvents().clear(); + + MetricAnnotationProcessor.handle(new InvalidNamespaceException(""), method, translator); + + Assertions.assertFalse(sink.getLogEvents().isEmpty()); + } + + @CountMetric + void countMethod() {} + + @CountMetric(logger = "example logger") + void countNamedLogger() {} + + @CountMetric(name = "multiCount1") + @CountMetric(name = "multiCount2") + @CountMetric(name = "multiCount3") + void multiCountMethod() {} + + @CountMetric(name = "Count") + @TimeMetric(name = "Time") + void countAndTimeMethod() { + waitRandom(200, 20); + } + + @TimeMetric + void timeMethod() { + waitRandom(200, 20); + } + + @TimeMetric(logger = "example logger") + void multiAnnotationMethod() { + waitRandom(200, 20); + } + + public void dummyMethod() {} + + private void waitRandom(int max, int min) { + int waitTime = random.nextInt(max - min) + min; + try { + Thread.sleep(waitTime); + } catch (InterruptedException e) { + // Handle interruption if needed + Thread.currentThread().interrupt(); + } + } + + protected MetricAnnotationProcessor.AnnotationTranslator.AnnotationTranslatorBuilder + getAnnotationTranslatorBuilder() { + return MetricAnnotationProcessor.AnnotationTranslator.builder() + .name("Translator") + .logSuccess(true) + .logExceptions(new Class[] {Throwable.class}) + .flush(true) + .logger("") + .value(1) + .defaultName("defaultName") + .unit(Unit.NONE); + } + + @SuppressWarnings("unchecked") + private ArrayList parseMetricNames(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 metricNames = new ArrayList<>(); + for (Map metric : metrics) { + metricNames.add(metric.get("Name")); + } + return metricNames; + } + + @SuppressWarnings("unchecked") + private Object parseMetricByName(String event, String name) throws JsonProcessingException { + Map rootNode = parseRootNode(event); + return rootNode.get(name); + } + + private Map parseRootNode(String event) throws JsonProcessingException { + return new JsonMapper().readValue(event, new TypeReference>() {}); + } +} diff --git a/annotations/src/test/java/software/amazon/cloudwatchlogs/emf/sinks/SinkShunt.java b/annotations/src/test/java/software/amazon/cloudwatchlogs/emf/sinks/SinkShunt.java new file mode 100644 index 00000000..91be478f --- /dev/null +++ b/annotations/src/test/java/software/amazon/cloudwatchlogs/emf/sinks/SinkShunt.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. + * 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.sinks; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import software.amazon.cloudwatchlogs.emf.model.MetricsContext; + +public class SinkShunt implements ISink { + + private MetricsContext context; + + private List logEvents = new ArrayList<>(); + + @Override + public void accept(MetricsContext context) { + this.context = context; + try { + this.logEvents.addAll(context.serialize()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public CompletableFuture shutdown() { + return CompletableFuture.completedFuture(null); + } + + public MetricsContext getContext() { + return context; + } + + public List getLogEvents() { + return this.logEvents; + } +} diff --git a/build.gradle b/build.gradle index 4391aaba..a07c452a 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,7 @@ plugins { + id 'java' id 'java-library' id 'com.diffplug.spotless' version '5.8.2' id 'maven-publish' @@ -100,7 +101,6 @@ spotless { importOrder() googleJavaFormat('1.7').aosp() removeUnusedImports() - } } diff --git a/settings.gradle b/settings.gradle index 2344f0c3..69450096 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,7 @@ rootProject.name = 'aws-embedded-metrics' +include 'annotations' include 'examples:agent' include 'examples:ecs-firelens' include 'examples:lambda'