diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/base/ContractTestBase.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/base/ContractTestBase.java index 3dd7e89a24..bb8a2255f3 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/base/ContractTestBase.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/base/ContractTestBase.java @@ -48,16 +48,19 @@ public abstract class ContractTestBase { private final Logger collectorLogger = LoggerFactory.getLogger("collector " + getApplicationOtelServiceName()); - private final Logger applicationLogger = + protected final Logger applicationLogger = LoggerFactory.getLogger("application " + getApplicationOtelServiceName()); - private static final String AGENT_PATH = + protected static final String AGENT_PATH = System.getProperty("io.awsobservability.instrumentation.contracttests.agentPath"); + protected static final String MOUNT_PATH = "/opentelemetry-javaagent-all.jar"; protected final Network network = Network.newNetwork(); private static final String COLLECTOR_HOSTNAME = "collector"; private static final int COLLECTOR_PORT = 4317; + protected static final String COLLECTOR_HTTP_ENDPOINT = + "http://" + COLLECTOR_HOSTNAME + ":" + COLLECTOR_PORT; protected final GenericContainer mockCollector = new GenericContainer<>("aws-appsignals-mock-collector") @@ -67,30 +70,7 @@ public abstract class ContractTestBase { .withNetwork(network) .withNetworkAliases(COLLECTOR_HOSTNAME); - protected final GenericContainer application = - new GenericContainer<>(getApplicationImageName()) - .dependsOn(getDependsOn()) - .withExposedPorts(getApplicationPort()) - .withNetwork(network) - .withLogConsumer(new Slf4jLogConsumer(applicationLogger)) - .withCopyFileToContainer( - MountableFile.forHostPath(AGENT_PATH), "/opentelemetry-javaagent-all.jar") - .waitingFor(getApplicationWaitCondition()) - .withEnv("JAVA_TOOL_OPTIONS", "-javaagent:/opentelemetry-javaagent-all.jar") - .withEnv("OTEL_METRIC_EXPORT_INTERVAL", "100") // 100 ms - .withEnv("OTEL_AWS_APPLICATION_SIGNALS_ENABLED", "true") - .withEnv("OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED", isRuntimeEnabled()) - .withEnv("OTEL_METRICS_EXPORTER", "none") - .withEnv("OTEL_BSP_SCHEDULE_DELAY", "0") // Don't wait to export spans to the collector - .withEnv( - "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT", - "http://" + COLLECTOR_HOSTNAME + ":" + COLLECTOR_PORT) - .withEnv( - "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", - "http://" + COLLECTOR_HOSTNAME + ":" + COLLECTOR_PORT) - .withEnv("OTEL_RESOURCE_ATTRIBUTES", getApplicationOtelResourceAttributes()) - .withEnv(getApplicationExtraEnvironmentVariables()) - .withNetworkAliases(getApplicationNetworkAliases().toArray(new String[0])); + protected final GenericContainer application = getApplicationContainer(); protected MockCollectorClient mockCollectorClient; protected WebClient appClient; @@ -109,10 +89,8 @@ private void stopCollector() { protected void setupClients() { application.start(); - appClient = WebClient.of("http://localhost:" + application.getMappedPort(8080)); - mockCollectorClient = - new MockCollectorClient( - WebClient.of("http://localhost:" + mockCollector.getMappedPort(4317))); + appClient = getApplicationClient(); + mockCollectorClient = getMockCollectorClient(); } @AfterEach @@ -128,11 +106,55 @@ private List getDependsOn() { return dependencies; } + protected WebClient getApplicationClient() { + return WebClient.of("http://localhost:" + application.getMappedPort(8080)); + } + + protected MockCollectorClient getMockCollectorClient() { + return new MockCollectorClient( + WebClient.of("http://localhost:" + mockCollector.getMappedPort(4317))); + } + + protected GenericContainer getApplicationContainer() { + return new GenericContainer<>(getApplicationImageName()) + .dependsOn(getDependsOn()) + .withExposedPorts(getApplicationPort()) + .withNetwork(network) + .withLogConsumer(new Slf4jLogConsumer(applicationLogger)) + .withCopyFileToContainer(MountableFile.forHostPath(AGENT_PATH), MOUNT_PATH) + .waitingFor(getApplicationWaitCondition()) + .withEnv(getApplicationEnvironmentVariables()) + .withEnv(getApplicationExtraEnvironmentVariables()) + .withNetworkAliases(getApplicationNetworkAliases().toArray(new String[0])); + } + /** Methods that should be overridden in sub classes * */ protected int getApplicationPort() { return 8080; } + protected Map getApplicationEnvironmentVariables() { + return Map.of( + "JAVA_TOOL_OPTIONS", + "-javaagent:" + MOUNT_PATH, + "OTEL_METRIC_EXPORT_INTERVAL", + "100", // 100 ms + "OTEL_AWS_APPLICATION_SIGNALS_ENABLED", + "true", + "OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED", + isRuntimeEnabled(), + "OTEL_METRICS_EXPORTER", + "none", + "OTEL_BSP_SCHEDULE_DELAY", + "0", // Don't wait to export spans to the collector + "OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT", + COLLECTOR_HTTP_ENDPOINT, + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + COLLECTOR_HTTP_ENDPOINT, + "OTEL_RESOURCE_ATTRIBUTES", + getApplicationOtelResourceAttributes()); + } + protected Map getApplicationExtraEnvironmentVariables() { return Map.of(); } diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/base/JMXMetricsContractTestBase.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/base/JMXMetricsContractTestBase.java new file mode 100644 index 0000000000..e6d9d76b06 --- /dev/null +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/base/JMXMetricsContractTestBase.java @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * 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.opentelemetry.appsignals.test.base; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.proto.metrics.v1.NumberDataPoint; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import software.amazon.opentelemetry.appsignals.test.utils.JMXMetricsConstants; + +public abstract class JMXMetricsContractTestBase extends ContractTestBase { + + @Override + protected Map getApplicationEnvironmentVariables() { + return Map.of( + "JAVA_TOOL_OPTIONS", "-javaagent:" + MOUNT_PATH, + "OTEL_METRIC_EXPORT_INTERVAL", "100", // 100 ms + "OTEL_METRICS_EXPORTER", "none", + "OTEL_LOGS_EXPORTER", "none", + "OTEL_TRACES_EXPORTER", "none", + "OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf", + "OTEL_JMX_ENABLED", "true", + "OTEL_AWS_JMX_EXPORTER_METRICS_ENDPOINT", COLLECTOR_HTTP_ENDPOINT + "/v1/metrics"); + } + + protected void doTestMetrics() { + var response = appClient.get("/success").aggregate().join(); + + assertThat(response.status().isSuccess()).isTrue(); + assertMetrics(); + } + + protected void assertMetrics() { + var metrics = mockCollectorClient.getRuntimeMetrics(getExpectedMetrics()); + metrics.forEach( + metric -> { + var dataPoints = metric.getMetric().getGauge().getDataPointsList(); + assertGreaterThanOrEqual(dataPoints, getThreshold(metric.getMetric().getName())); + }); + } + + protected abstract Set getExpectedMetrics(); + + protected long getThreshold(String metricName) { + long threshold = 0; + switch (metricName) { + // If maximum memory size is undefined, then value is -1 + // https://docs.oracle.com/en/java/javase/17/docs/api/java.management/java/lang/management/MemoryUsage.html#getMax() + case JMXMetricsConstants.JVM_HEAP_MAX: + case JMXMetricsConstants.JVM_NON_HEAP_MAX: + case JMXMetricsConstants.JVM_POOL_MAX: + threshold = -1; + default: + } + return threshold; + } + + private void assertGreaterThanOrEqual(List dps, long threshold) { + assertDataPoints(dps, (value) -> assertThat(value).isGreaterThanOrEqualTo(threshold)); + } + + private void assertDataPoints(List dps, Consumer consumer) { + dps.forEach(datapoint -> consumer.accept(datapoint.getAsInt())); + } +} diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/JVMMetricsTest.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/JVMMetricsTest.java new file mode 100644 index 0000000000..369ec20cda --- /dev/null +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/JVMMetricsTest.java @@ -0,0 +1,57 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * 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.opentelemetry.appsignals.test.misc.jmx; + +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.junit.jupiter.Testcontainers; +import software.amazon.opentelemetry.appsignals.test.base.JMXMetricsContractTestBase; +import software.amazon.opentelemetry.appsignals.test.utils.JMXMetricsConstants; + +/** + * Tests in this class validate that the SDK will emit JVM metrics when Application Signals runtime + * metrics are enabled. + */ +@Testcontainers(disabledWithoutDocker = true) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class JVMMetricsTest extends JMXMetricsContractTestBase { + @Test + void testJVMMetrics() { + doTestMetrics(); + } + + @Override + protected String getApplicationImageName() { + return "aws-appsignals-tests-http-server-spring-mvc"; + } + + @Override + protected String getApplicationWaitPattern() { + return ".*Started Application.*"; + } + + @Override + protected Set getExpectedMetrics() { + return JMXMetricsConstants.JVM_METRICS_SET; + } + + @Override + protected Map getApplicationExtraEnvironmentVariables() { + return Map.of("OTEL_JMX_TARGET_SYSTEM", "jvm"); + } +} diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/KafkaBrokerMetricsTest.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/KafkaBrokerMetricsTest.java new file mode 100644 index 0000000000..2d734c4abe --- /dev/null +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/KafkaBrokerMetricsTest.java @@ -0,0 +1,96 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * 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.opentelemetry.appsignals.test.misc.jmx; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.*; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.images.PullPolicy; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; +import software.amazon.opentelemetry.appsignals.test.base.JMXMetricsContractTestBase; +import software.amazon.opentelemetry.appsignals.test.utils.JMXMetricsConstants; + +@Testcontainers(disabledWithoutDocker = true) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class KafkaBrokerMetricsTest extends JMXMetricsContractTestBase { + @Test + void testKafkaMetrics() { + assertMetrics(); + } + + @Override + protected GenericContainer getApplicationContainer() { + return new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0")) + .withImagePullPolicy(PullPolicy.alwaysPull()) + .withNetworkAliases("kafkaBroker") + .withNetwork(network) + .withLogConsumer(new Slf4jLogConsumer(applicationLogger)) + .withCopyFileToContainer(MountableFile.forHostPath(AGENT_PATH), MOUNT_PATH) + .withEnv(getApplicationEnvironmentVariables()) + .withEnv(getApplicationExtraEnvironmentVariables()) + .waitingFor(getApplicationWaitCondition()) + .withKraft(); + } + + @BeforeAll + public void setup() throws IOException, InterruptedException { + application.start(); + application.execInContainer( + "/bin/sh", + "-c", + "/usr/bin/kafka-topics --bootstrap-server=localhost:9092 --create --topic kafka_topic --partitions 3 --replication-factor 1"); + mockCollectorClient = getMockCollectorClient(); + } + + // don't use the default clients + @BeforeEach + @Override + protected void setupClients() {} + + @Override + protected String getApplicationImageName() { + return "aws-appsignals-tests-kafka"; + } + + @Override + protected String getApplicationWaitPattern() { + return ".* Kafka Server started .*"; + } + + @Override + protected Set getExpectedMetrics() { + return JMXMetricsConstants.KAFKA_METRICS_SET; + } + + @Override + protected Map getApplicationExtraEnvironmentVariables() { + return Map.of( + "JAVA_TOOL_OPTIONS", // kafka broker container will not complete startup if agent is set + "", + "KAFKA_OPTS", // replace java tool options with kafka opts + "-javaagent:" + MOUNT_PATH, + "KAFKA_AUTO_CREATE_TOPICS_ENABLE", + "false", + "OTEL_JMX_TARGET_SYSTEM", + "kafka"); + } +} diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/KafkaConsumerMetricsTest.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/KafkaConsumerMetricsTest.java new file mode 100644 index 0000000000..d32ca53b0d --- /dev/null +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/KafkaConsumerMetricsTest.java @@ -0,0 +1,95 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * 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.opentelemetry.appsignals.test.misc.jmx; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.PullPolicy; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.lifecycle.Startable; +import org.testcontainers.utility.DockerImageName; +import software.amazon.opentelemetry.appsignals.test.base.JMXMetricsContractTestBase; +import software.amazon.opentelemetry.appsignals.test.utils.JMXMetricsConstants; + +/** + * Tests in this class validate that the SDK will emit JVM metrics when Application Signals runtime + * metrics are enabled. + */ +@Testcontainers(disabledWithoutDocker = true) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class KafkaConsumerMetricsTest extends JMXMetricsContractTestBase { + private KafkaContainer kafka; + + @Test + void testKafkaConsumerMetrics() { + doTestMetrics(); + } + + @Override + protected List getApplicationDependsOnContainers() { + kafka = + new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0")) + .withImagePullPolicy(PullPolicy.alwaysPull()) + .withEnv("KAFKA_AUTO_CREATE_TOPICS_ENABLE", "false") + .withNetworkAliases("kafkaBroker") + .withNetwork(network) + .waitingFor(Wait.forLogMessage(".* Kafka Server started .*", 1)) + .withKraft(); + return List.of(kafka); + } + + @BeforeAll + public void setup() throws IOException, InterruptedException { + kafka.start(); + kafka.execInContainer( + "/bin/sh", + "-c", + "/usr/bin/kafka-topics --bootstrap-server=localhost:9092 --create --topic kafka_topic --partitions 1 --replication-factor 1"); + } + + @AfterAll + public void tearDown() { + kafka.stop(); + } + + @Override + protected String getApplicationImageName() { + return "aws-appsignals-tests-kafka-kafka-consumers"; + } + + @Override + protected String getApplicationWaitPattern() { + return ".*Routes ready.*"; + } + + @Override + protected Set getExpectedMetrics() { + return JMXMetricsConstants.KAFKA_CONSUMER_METRICS_SET; + } + + @Override + protected Map getApplicationExtraEnvironmentVariables() { + return Map.of("OTEL_JMX_TARGET_SYSTEM", "kafka-consumer"); + } +} diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/KafkaProducerMetricsTest.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/KafkaProducerMetricsTest.java new file mode 100644 index 0000000000..60ff25ddc6 --- /dev/null +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/KafkaProducerMetricsTest.java @@ -0,0 +1,102 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * 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.opentelemetry.appsignals.test.misc.jmx; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.PullPolicy; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.lifecycle.Startable; +import org.testcontainers.utility.DockerImageName; +import software.amazon.opentelemetry.appsignals.test.base.JMXMetricsContractTestBase; +import software.amazon.opentelemetry.appsignals.test.utils.JMXMetricsConstants; + +/** + * Tests in this class validate that the SDK will emit JVM metrics when Application Signals runtime + * metrics are enabled. + */ +@Testcontainers(disabledWithoutDocker = true) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class KafkaProducerMetricsTest extends JMXMetricsContractTestBase { + private KafkaContainer kafka; + + @Test + void testKafkaProducerMetrics() { + for (int i = 0; i < 50; i++) { + var response = appClient.get("/success").aggregate().join(); + + assertThat(response.status().isSuccess()).isTrue(); + } + assertMetrics(); + } + + @Override + protected List getApplicationDependsOnContainers() { + kafka = + new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0")) + .withImagePullPolicy(PullPolicy.alwaysPull()) + .withEnv("KAFKA_AUTO_CREATE_TOPICS_ENABLE", "false") + .withNetworkAliases("kafkaBroker") + .withNetwork(network) + .waitingFor(Wait.forLogMessage(".* Kafka Server started .*", 1)) + .withKraft(); + return List.of(kafka); + } + + @BeforeAll + public void setup() throws IOException, InterruptedException { + kafka.start(); + kafka.execInContainer( + "/bin/sh", + "-c", + "/usr/bin/kafka-topics --bootstrap-server=localhost:9092 --create --topic kafka_topic --partitions 1 --replication-factor 1"); + } + + @AfterAll + public void tearDown() { + kafka.stop(); + } + + @Override + protected String getApplicationImageName() { + return "aws-appsignals-tests-kafka-kafka-producers"; + } + + @Override + protected String getApplicationWaitPattern() { + return ".*Routes ready.*"; + } + + @Override + protected Set getExpectedMetrics() { + return JMXMetricsConstants.KAFKA_PRODUCER_METRICS_SET; + } + + @Override + protected Map getApplicationExtraEnvironmentVariables() { + return Map.of("OTEL_JMX_TARGET_SYSTEM", "kafka-producer"); + } +} diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/TomcatMetricsTest.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/TomcatMetricsTest.java new file mode 100644 index 0000000000..d3e324da63 --- /dev/null +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/TomcatMetricsTest.java @@ -0,0 +1,57 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * 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.opentelemetry.appsignals.test.misc.jmx; + +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.junit.jupiter.Testcontainers; +import software.amazon.opentelemetry.appsignals.test.base.JMXMetricsContractTestBase; +import software.amazon.opentelemetry.appsignals.test.utils.JMXMetricsConstants; + +/** + * Tests in this class validate that the SDK will emit JVM metrics when Application Signals runtime + * metrics are enabled and if tomcat is also enabled, it will emit those metrics. + */ +@Testcontainers(disabledWithoutDocker = true) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class TomcatMetricsTest extends JMXMetricsContractTestBase { + @Test + void testTomcatMetrics() { + doTestMetrics(); + } + + @Override + protected String getApplicationImageName() { + return "aws-appsignals-tests-http-server-tomcat"; + } + + @Override + protected String getApplicationWaitPattern() { + return ".*Starting ProtocolHandler.*"; + } + + @Override + protected Set getExpectedMetrics() { + return JMXMetricsConstants.TOMCAT_METRICS_SET; + } + + @Override + protected Map getApplicationExtraEnvironmentVariables() { + return Map.of("OTEL_JMX_TARGET_SYSTEM", "tomcat"); + } +} diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/JMXMetricsConstants.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/JMXMetricsConstants.java new file mode 100644 index 0000000000..104dbc36bd --- /dev/null +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/JMXMetricsConstants.java @@ -0,0 +1,198 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * 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.opentelemetry.appsignals.test.utils; + +import java.util.Set; + +public class JMXMetricsConstants { + // JVM Metrics + public static final String JVM_CLASS_LOADED = "jvm.classes.loaded"; + public static final String JVM_GC_COUNT = "jvm.gc.collections.count"; + public static final String JVM_GC_METRIC = "jvm.gc.collections.elapsed"; + public static final String JVM_HEAP_INIT = "jvm.memory.heap.init"; + public static final String JVM_HEAP_USED = "jvm.memory.heap.used"; + public static final String JVM_HEAP_COMMITTED = "jvm.memory.heap.committed"; + public static final String JVM_HEAP_MAX = "jvm.memory.heap.max"; + public static final String JVM_NON_HEAP_INIT = "jvm.memory.nonheap.init"; + public static final String JVM_NON_HEAP_USED = "jvm.memory.nonheap.used"; + public static final String JVM_NON_HEAP_COMMITTED = "jvm.memory.nonheap.committed"; + public static final String JVM_NON_HEAP_MAX = "jvm.memory.nonheap.max"; + public static final String JVM_POOL_INIT = "jvm.memory.pool.init"; + public static final String JVM_POOL_USED = "jvm.memory.pool.used"; + public static final String JVM_POOL_COMMITTED = "jvm.memory.pool.committed"; + public static final String JVM_POOL_MAX = "jvm.memory.pool.max"; + public static final String JVM_THREADS_COUNT = "jvm.threads.count"; + public static final String JVM_DAEMON_THREADS_COUNT = "jvm.daemon_threads.count"; + public static final String JVM_SYSTEM_SWAP_TOTAL = "jvm.system.swap.space.total"; + public static final String JVM_SYSTEM_SWAP_FREE = "jvm.system.swap.space.free"; + public static final String JVM_SYSTEM_MEM_TOTAL = "jvm.system.physical.memory.total"; + public static final String JVM_SYSTEM_MEM_FREE = "jvm.system.physical.memory.free"; + public static final String JVM_SYSTEM_AVAILABLE_PROCESSORS = "jvm.system.available.processors"; + public static final String JVM_SYSTEM_CPU_UTILIZATION = "jvm.system.cpu.utilization"; + public static final String JVM_CPU_UTILIZATION = "jvm.cpu.recent_utilization"; + public static final String JVM_FILE_DESCRIPTORS = "jvm.open_file_descriptor.count"; + + public static final Set JVM_METRICS_SET = + Set.of( + JVM_CLASS_LOADED, + JVM_GC_COUNT, + JVM_GC_METRIC, + JVM_HEAP_INIT, + JVM_HEAP_USED, + JVM_HEAP_COMMITTED, + JVM_HEAP_MAX, + JVM_NON_HEAP_INIT, + JVM_NON_HEAP_USED, + JVM_NON_HEAP_COMMITTED, + JVM_NON_HEAP_MAX, + JVM_POOL_INIT, + JVM_POOL_USED, + JVM_POOL_COMMITTED, + JVM_POOL_MAX, + JVM_THREADS_COUNT, + JVM_DAEMON_THREADS_COUNT, + JVM_SYSTEM_SWAP_TOTAL, + JVM_SYSTEM_SWAP_FREE, + JVM_SYSTEM_MEM_TOTAL, + JVM_SYSTEM_MEM_FREE, + JVM_SYSTEM_AVAILABLE_PROCESSORS, + JVM_SYSTEM_CPU_UTILIZATION, + JVM_CPU_UTILIZATION, + JVM_FILE_DESCRIPTORS); + + // Tomcat Metrics + public static final String TOMCAT_SESSION = "tomcat.sessions"; + public static final String TOMCAT_REJECTED_SESSION = "tomcat.rejected_sessions"; + public static final String TOMCAT_ERRORS = "tomcat.errors"; + public static final String TOMCAT_REQUEST_COUNT = "tomcat.request_count"; + public static final String TOMCAT_MAX_TIME = "tomcat.max_time"; + public static final String TOMCAT_PROCESSING_TIME = "tomcat.processing_time"; + public static final String TOMCAT_TRAFFIC = "tomcat.traffic"; + public static final String TOMCAT_THREADS = "tomcat.threads"; + + public static final Set TOMCAT_METRICS_SET = + Set.of( + TOMCAT_SESSION, + TOMCAT_REJECTED_SESSION, + TOMCAT_ERRORS, + TOMCAT_REQUEST_COUNT, + TOMCAT_MAX_TIME, + TOMCAT_PROCESSING_TIME, + TOMCAT_TRAFFIC, + TOMCAT_THREADS); + + // Kafka Metrics + public static final String KAFKA_MESSAGE_COUNT = "kafka.message.count"; + public static final String KAFKA_REQUEST_COUNT = "kafka.request.count"; + public static final String KAFKA_REQUEST_FAILED = "kafka.request.failed"; + public static final String KAFKA_REQUEST_TIME_TOTAL = "kafka.request.time.total"; + public static final String KAFKA_REQUEST_TIME_50P = "kafka.request.time.50p"; + public static final String KAFKA_REQUEST_TIME_99P = "kafka.request.time.99p"; + public static final String KAFKA_REQUEST_TIME_AVG = "kafka.request.time.avg"; + public static final String KAFKA_NETWORK_IO = "kafka.network.io"; + public static final String KAFKA_PURGATORY_SIZE = "kafka.purgatory.size"; + public static final String KAFKA_PARTITION_COUNT = "kafka.partition.count"; + public static final String KAFKA_PARTITION_OFFLINE = "kafka.partition.offline"; + public static final String KAFKA_PARTITION_UNDER_REPLICATED = "kafka.partition.under_replicated"; + public static final String KAFKA_ISR_OPERATION_COUNT = "kafka.isr.operation.count"; + public static final String KAFKA_MAX_LAG = "kafka.max.lag"; + public static final String KAFKA_CONTROLLER_ACTIVE_COUNT = "kafka.controller.active.count"; + public static final String KAFKA_LEADER_ELECTION_RATE = "kafka.leader.election.rate"; + public static final String KAFKA_UNCLEAN_ELECTION_RATE = "kafka.unclean.election.rate"; + public static final String KAFKA_REQUEST_QUEUE = "kafka.request.queue"; + public static final String KAFKA_LOGS_FLUSH_TIME_COUNT = "kafka.logs.flush.time.count"; + public static final String KAFKA_LOGS_FLUSH_TIME_MEDIAN = "kafka.logs.flush.time.median"; + public static final String KAFKA_LOGS_FLUSH_TIME_99P = "kafka.logs.flush.time.99p"; + + public static final Set KAFKA_METRICS_SET = + Set.of( + KAFKA_MESSAGE_COUNT, + KAFKA_REQUEST_COUNT, + KAFKA_REQUEST_FAILED, + KAFKA_REQUEST_TIME_TOTAL, + KAFKA_REQUEST_TIME_50P, + KAFKA_REQUEST_TIME_99P, + KAFKA_REQUEST_TIME_AVG, + KAFKA_NETWORK_IO, + KAFKA_PURGATORY_SIZE, + KAFKA_PARTITION_COUNT, + KAFKA_PARTITION_OFFLINE, + KAFKA_PARTITION_UNDER_REPLICATED, + KAFKA_ISR_OPERATION_COUNT, + KAFKA_MAX_LAG, + KAFKA_CONTROLLER_ACTIVE_COUNT, + // TODO: Add test case for leader election. + // KAFKA_LEADER_ELECTION_RATE, + // KAFKA_UNCLEAN_ELECTION_RATE, + KAFKA_REQUEST_QUEUE, + KAFKA_LOGS_FLUSH_TIME_COUNT, + KAFKA_LOGS_FLUSH_TIME_MEDIAN, + KAFKA_LOGS_FLUSH_TIME_99P); + + // Kafka Consumer Metrics + public static final String KAFKA_CONSUMER_FETCH_RATE = "kafka.consumer.fetch-rate"; + public static final String KAFKA_CONSUMER_RECORDS_LAG_MAX = "kafka.consumer.records-lag-max"; + public static final String KAFKA_CONSUMER_TOTAL_BYTES_CONSUMED_RATE = + "kafka.consumer.total.bytes-consumed-rate"; + public static final String KAFKA_CONSUMER_TOTAL_FETCH_SIZE_AVG = + "kafka.consumer.total.fetch-size-avg"; + public static final String KAFKA_CONSUMER_TOTAL_RECORDS_CONSUMED_RATE = + "kafka.consumer.total.records-consumed-rate"; + public static final String KAFKA_CONSUMER_BYTES_CONSUMED_RATE = + "kafka.consumer.bytes-consumed-rate"; + public static final String KAFKA_CONSUMER_FETCH_SIZE_AVG = "kafka.consumer.fetch-size-avg"; + public static final String KAFKA_CONSUMER_RECORDS_CONSUMED_RATE = + "kafka.consumer.records-consumed-rate"; + + public static final Set KAFKA_CONSUMER_METRICS_SET = + Set.of( + KAFKA_CONSUMER_FETCH_RATE, + KAFKA_CONSUMER_RECORDS_LAG_MAX, + KAFKA_CONSUMER_TOTAL_BYTES_CONSUMED_RATE, + KAFKA_CONSUMER_TOTAL_FETCH_SIZE_AVG, + KAFKA_CONSUMER_TOTAL_RECORDS_CONSUMED_RATE, + KAFKA_CONSUMER_BYTES_CONSUMED_RATE, + KAFKA_CONSUMER_FETCH_SIZE_AVG, + KAFKA_CONSUMER_RECORDS_CONSUMED_RATE); + + // Kafka Producer Metrics + public static final String KAFKA_PRODUCER_IO_WAIT_TIME_NS_AVG = + "kafka.producer.io-wait-time-ns-avg"; + public static final String KAFKA_PRODUCER_OUTGOING_BYTE_RATE = + "kafka.producer.outgoing-byte-rate"; + public static final String KAFKA_PRODUCER_REQUEST_LATENCY_AVG = + "kafka.producer.request-latency-avg"; + public static final String KAFKA_PRODUCER_REQUEST_RATE = "kafka-producer.request-rate"; + public static final String KAFKA_PRODUCER_RESPONSE_RATE = "kafka.producer.response-rate"; + public static final String KAFKA_PRODUCER_BYTE_RATE = "kafka.producer.byte-rate"; + public static final String KAFKA_PRODUCER_COMPRESSION_RATE = "kafka.producer.compression-rate"; + public static final String KAFKA_PRODUCER_RECORD_ERROR_RATE = "kafka.producer.record-error-rate"; + public static final String KAFKA_PRODUCER_RECORD_RETRY_RATE = "kafka.producer.record-retry-rate"; + public static final String KAFKA_PRODUCER_RECORD_SEND_RATE = "kafka.producer.record-send-rate"; + + public static final Set KAFKA_PRODUCER_METRICS_SET = + Set.of( + KAFKA_PRODUCER_IO_WAIT_TIME_NS_AVG, + KAFKA_PRODUCER_OUTGOING_BYTE_RATE, + KAFKA_PRODUCER_REQUEST_LATENCY_AVG, + KAFKA_PRODUCER_REQUEST_RATE, + KAFKA_PRODUCER_RESPONSE_RATE, + KAFKA_PRODUCER_BYTE_RATE, + KAFKA_PRODUCER_COMPRESSION_RATE, + KAFKA_PRODUCER_RECORD_ERROR_RATE, + KAFKA_PRODUCER_RECORD_RETRY_RATE, + KAFKA_PRODUCER_RECORD_SEND_RATE); +} diff --git a/appsignals-tests/images/kafka/kafka-consumers/src/main/java/App.java b/appsignals-tests/images/kafka/kafka-consumers/src/main/java/App.java index db0faa52b3..082a061dfe 100644 --- a/appsignals-tests/images/kafka/kafka-consumers/src/main/java/App.java +++ b/appsignals-tests/images/kafka/kafka-consumers/src/main/java/App.java @@ -19,7 +19,7 @@ import static spark.Spark.port; import java.time.Duration; -import java.util.Arrays; +import java.util.List; import java.util.Properties; import java.util.UUID; import org.apache.kafka.clients.consumer.ConsumerConfig; @@ -39,7 +39,6 @@ public class App { public static final Logger log = LoggerFactory.getLogger(App.class); public static void main(String[] args) { - String bootstrapServers = "kafkaBroker:9092"; String topic = "kafka_topic"; @@ -53,7 +52,7 @@ public static void main(String[] args) { ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); producerProperties.setProperty(ProducerConfig.MAX_BLOCK_MS_CONFIG, "15000"); - // produce and send record to kafa_topic + // produce and send record to kafka_topic KafkaProducer producer = new KafkaProducer<>(producerProperties); // create a producer_record ProducerRecord producer_record = new ProducerRecord<>(topic, "success"); @@ -74,6 +73,17 @@ public static void main(String[] args) { consumerProperties.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); consumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG, UUID.randomUUID().toString()); + // initialized and reused to expose the kafka consumer beans for JMX + KafkaConsumer consumer = new KafkaConsumer<>(consumerProperties); + consumer.subscribe(List.of(topic)); + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + log.info("Shutting down Kafka consumer..."); + consumer.close(); + })); + // spark server port(Integer.parseInt("8080")); ipAddress("0.0.0.0"); @@ -83,8 +93,6 @@ public static void main(String[] args) { "/success", (req, res) -> { // create consumer - KafkaConsumer consumer = new KafkaConsumer<>(consumerProperties); - consumer.subscribe(Arrays.asList(topic)); ConsumerRecords records = consumer.poll(Duration.ofMillis(10000)); String consumedRecord = null; @@ -93,12 +101,11 @@ public static void main(String[] args) { consumedRecord = record.value(); } } - consumer.close(); if (consumedRecord != null && consumedRecord.equals("success")) { res.status(HttpStatus.OK_200); res.body("success"); } else { - log.info("consumer is unable to consumer right message"); + log.info("consumer is unable to consume right message"); res.status(HttpStatus.INTERNAL_SERVER_ERROR_500); } return res.body(); diff --git a/appsignals-tests/images/kafka/kafka-producers/src/main/java/com/amazon/sampleapp/App.java b/appsignals-tests/images/kafka/kafka-producers/src/main/java/com/amazon/sampleapp/App.java index 31bf62bbc6..7e16b685ee 100644 --- a/appsignals-tests/images/kafka/kafka-producers/src/main/java/com/amazon/sampleapp/App.java +++ b/appsignals-tests/images/kafka/kafka-producers/src/main/java/com/amazon/sampleapp/App.java @@ -47,21 +47,28 @@ public static void main(String[] args) { properties.setProperty( ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.setProperty(ProducerConfig.MAX_BLOCK_MS_CONFIG, "10000"); + + // create the producer + // initialized and reused to expose the kafka producer beans for JMX + KafkaProducer producer = new KafkaProducer<>(properties); + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + log.info("Shutting down Kafka producer..."); + producer.close(); + })); + // rest endpoints get( "/success", (req, res) -> { - // create the producer - KafkaProducer producer = new KafkaProducer<>(properties); - // create a record ProducerRecord record = new ProducerRecord<>("kafka_topic", "success"); // send data - asynchronous producer.send(record); // flush data - synchronous producer.flush(); - // close producer - producer.close(); res.status(HttpStatus.OK_200); res.body("success"); @@ -70,8 +77,6 @@ public static void main(String[] args) { get( "/fault", (req, res) -> { - // create the producer - KafkaProducer producer = new KafkaProducer<>(properties); // create a record & send data to a topic that does not exist- asynchronous ProducerRecord producerRecord = new ProducerRecord<>("fault_do_not_exist", "fault"); producer.send( @@ -91,8 +96,6 @@ public void onCompletion(RecordMetadata recordMetadata, Exception e) { }); // flush data - synchronous producer.flush(); - // close producer - producer.close(); res.body("fault"); return res.body(); }); diff --git a/appsignals-tests/images/mock-collector/src/main/java/software/amazon/opentelemetry/appsignals/test/images/mockcollector/Main.java b/appsignals-tests/images/mock-collector/src/main/java/software/amazon/opentelemetry/appsignals/test/images/mockcollector/Main.java index 3777e92e4d..b8d69ef096 100644 --- a/appsignals-tests/images/mock-collector/src/main/java/software/amazon/opentelemetry/appsignals/test/images/mockcollector/Main.java +++ b/appsignals-tests/images/mock-collector/src/main/java/software/amazon/opentelemetry/appsignals/test/images/mockcollector/Main.java @@ -120,6 +120,7 @@ public static void main(String[] args) { HttpStatus.OK, MediaType.JSON, HttpData.wrap(buf.buffer())); }) .service("/health", HealthCheckService.of()) + .annotatedService(metricsCollector.HTTP_INSTANCE) .build(); server.start().join(); diff --git a/appsignals-tests/images/mock-collector/src/main/java/software/amazon/opentelemetry/appsignals/test/images/mockcollector/MockCollectorMetricsService.java b/appsignals-tests/images/mock-collector/src/main/java/software/amazon/opentelemetry/appsignals/test/images/mockcollector/MockCollectorMetricsService.java index b7f584fda3..da9629c2cd 100644 --- a/appsignals-tests/images/mock-collector/src/main/java/software/amazon/opentelemetry/appsignals/test/images/mockcollector/MockCollectorMetricsService.java +++ b/appsignals-tests/images/mock-collector/src/main/java/software/amazon/opentelemetry/appsignals/test/images/mockcollector/MockCollectorMetricsService.java @@ -16,16 +16,25 @@ package software.amazon.opentelemetry.appsignals.test.images.mockcollector; import com.google.common.collect.ImmutableList; +import com.linecorp.armeria.common.AggregatedHttpRequest; +import com.linecorp.armeria.common.annotation.Nullable; +import com.linecorp.armeria.server.ServiceRequestContext; +import com.linecorp.armeria.server.annotation.Post; +import com.linecorp.armeria.server.annotation.RequestConverter; +import com.linecorp.armeria.server.annotation.RequestConverterFunction; import io.grpc.stub.StreamObserver; import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse; import io.opentelemetry.proto.collector.metrics.v1.MetricsServiceGrpc; +import java.lang.reflect.ParameterizedType; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; class MockCollectorMetricsService extends MetricsServiceGrpc.MetricsServiceImplBase { + protected final HttpService HTTP_INSTANCE = new HttpService(); + private final BlockingQueue exportRequests = new LinkedBlockingDeque<>(); @@ -45,4 +54,30 @@ public void export( responseObserver.onNext(ExportMetricsServiceResponse.getDefaultInstance()); responseObserver.onCompleted(); } + + class HttpService { + @Post("/v1/metrics") + @RequestConverter(ExportMetricsServiceRequestConverter.class) + public void consumeMetrics(ExportMetricsServiceRequest request) { + exportRequests.add(request); + } + } + + static class ExportMetricsServiceRequestConverter implements RequestConverterFunction { + + @Override + public @Nullable Object convertRequest( + ServiceRequestContext ctx, + AggregatedHttpRequest request, + Class expectedResultType, + @Nullable ParameterizedType expectedParameterizedResultType) + throws Exception { + if (expectedResultType == ExportMetricsServiceRequest.class) { + try (var content = request.content()) { + return ExportMetricsServiceRequest.parseFrom(content.array()); + } + } + return RequestConverterFunction.fallthrough(); + } + } }