From 2aa03d5dbf5dc76cc33fc28b3c801a641e6521ee Mon Sep 17 00:00:00 2001 From: Jeffrey Chien Date: Fri, 1 Nov 2024 16:24:58 -0400 Subject: [PATCH 01/11] Add JMX contract tests (#935) *Description of changes:* Adds contract tests to verify the expected behavior of the following changes: - https://github.com/aws-observability/aws-otel-java-instrumentation/pull/817 - https://github.com/aws-observability/aws-otel-java-instrumentation/pull/898 - https://github.com/aws-observability/aws-otel-java-instrumentation/pull/901 Starts up sample apps with the instrumented SDK and verifies that metrics are received for each of the supported JMX target systems. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --- .../test/base/ContractTestBase.java | 82 +++++--- .../test/base/JMXMetricsContractTestBase.java | 81 +++++++ .../test/misc/jmx/JVMMetricsTest.java | 57 +++++ .../test/misc/jmx/KafkaBrokerMetricsTest.java | 96 +++++++++ .../misc/jmx/KafkaConsumerMetricsTest.java | 95 +++++++++ .../misc/jmx/KafkaProducerMetricsTest.java | 102 +++++++++ .../test/misc/jmx/TomcatMetricsTest.java | 57 +++++ .../test/utils/JMXMetricsConstants.java | 198 ++++++++++++++++++ .../kafka-consumers/src/main/java/App.java | 21 +- .../main/java/com/amazon/sampleapp/App.java | 21 +- .../test/images/mockcollector/Main.java | 1 + .../MockCollectorMetricsService.java | 35 ++++ 12 files changed, 800 insertions(+), 46 deletions(-) create mode 100644 appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/base/JMXMetricsContractTestBase.java create mode 100644 appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/JVMMetricsTest.java create mode 100644 appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/KafkaBrokerMetricsTest.java create mode 100644 appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/KafkaConsumerMetricsTest.java create mode 100644 appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/KafkaProducerMetricsTest.java create mode 100644 appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/misc/jmx/TomcatMetricsTest.java create mode 100644 appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/JMXMetricsConstants.java 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(); + } + } } From c81b3f8641a159706f961b1c72e83eaf64ecec6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 09:56:01 -0800 Subject: [PATCH 02/11] Bump docker/library/rust from 1.75 to 1.82 (#932) Bumps [docker/library/rust](https://github.com/rust-lang/docker-rust) from 1.75 to 1.82.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/library/rust&package-manager=docker&previous-version=1.75&new-version=1.82)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vastin <3690049+vastin@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6396f9e94b..739874e9ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ # permissions and limitations under the License. # Stage 1: Build the cp-utility binary -FROM public.ecr.aws/docker/library/rust:1.75 as builder +FROM public.ecr.aws/docker/library/rust:1.82 as builder WORKDIR /usr/src/cp-utility COPY ./tools/cp-utility . From 61164e883d712038a8195140609140f18110a910 Mon Sep 17 00:00:00 2001 From: "John L. Peterson (Jack)" Date: Tue, 5 Nov 2024 17:05:11 -0500 Subject: [PATCH 03/11] Fix link to AWS Resources in README.md (#896) *Issue #, if available:* *Description of changes:* update resource link in README.md since it has moved to [opentelemetry-java-contrib](https://github.com/open-telemetry/opentelemetry-java-contrib) repository By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f515b791f..4af75998eb 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ This configuration includes being able to reconfigure the [IdsGenerator](https:/ which we need to support X-Ray compatible trace IDs. Because the SDK uses SPI, it is sufficient for the custom implementation to be on the classpath to be recognized. The AWS distribution of the OpenTelemetry Java Agent repackages the upstream agent by simply adding our SPI implementation for -reconfiguring the ID generator. In addition, it includes [AWS resource providers](https://github.com/open-telemetry/opentelemetry-java/tree/main/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource) +reconfiguring the ID generator. In addition, it includes [AWS resource providers](https://github.com/open-telemetry/opentelemetry-java-contrib/tree/main/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource) by default, and it sets a system property to configure the agent to use multiple trace ID propagators, defaulting to maximum interoperability. From ffc6afea33baf48d0af760cdbd32e666ec845ca4 Mon Sep 17 00:00:00 2001 From: Prashant Srivastava <50466688+srprash@users.noreply.github.com> Date: Wed, 6 Nov 2024 21:18:02 -0800 Subject: [PATCH 04/11] Adding UdpExporter for Otlp spans (#944) --- awsagentprovider/build.gradle.kts | 3 + .../providers/OtlpUdpSpanExporter.java | 97 ++++++++++++ .../providers/OtlpUdpSpanExporterBuilder.java | 77 +++++++++ .../javaagent/providers/UdpSender.java | 76 +++++++++ .../javaagent/providers/UdpExporterTest.java | 147 ++++++++++++++++++ 5 files changed, 400 insertions(+) create mode 100644 awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/OtlpUdpSpanExporter.java create mode 100644 awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/OtlpUdpSpanExporterBuilder.java create mode 100644 awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/UdpSender.java create mode 100644 awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/UdpExporterTest.java diff --git a/awsagentprovider/build.gradle.kts b/awsagentprovider/build.gradle.kts index 3e92ad8b1a..e052e5eb47 100644 --- a/awsagentprovider/build.gradle.kts +++ b/awsagentprovider/build.gradle.kts @@ -40,12 +40,15 @@ dependencies { implementation("com.amazonaws:aws-java-sdk-core:1.12.773") // Export configuration compileOnly("io.opentelemetry:opentelemetry-exporter-otlp") + // For Udp emitter + compileOnly("io.opentelemetry:opentelemetry-exporter-otlp-common") testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") testImplementation("io.opentelemetry:opentelemetry-extension-aws") testImplementation("io.opentelemetry:opentelemetry-extension-trace-propagators") testImplementation("com.google.guava:guava") + testRuntimeOnly("io.opentelemetry:opentelemetry-exporter-otlp-common") compileOnly("com.google.code.findbugs:jsr305:3.0.2") testImplementation("org.mockito:mockito-core:5.3.1") diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/OtlpUdpSpanExporter.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/OtlpUdpSpanExporter.java new file mode 100644 index 0000000000..fd130e54a6 --- /dev/null +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/OtlpUdpSpanExporter.java @@ -0,0 +1,97 @@ +/* + * 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.javaagent.providers; + +import io.opentelemetry.exporter.internal.otlp.traces.TraceRequestMarshaler; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.concurrent.Immutable; + +/** + * Exports spans via UDP, using OpenTelemetry's protobuf model. The protobuf modelled spans are + * Base64 encoded and prefixed with AWS X-Ray specific information before being sent over to {@link + * UdpSender}. + * + *

This exporter is NOT meant for generic use since the payload is prefixed with AWS X-Ray + * specific information. + */ +@Immutable +class OtlpUdpSpanExporter implements SpanExporter { + + private static final Logger logger = Logger.getLogger(OtlpUdpSpanExporter.class.getName()); + + private final AtomicBoolean isShutdown = new AtomicBoolean(); + + private final UdpSender sender; + private final String payloadPrefix; + + OtlpUdpSpanExporter(UdpSender sender, String payloadPrefix) { + this.sender = sender; + this.payloadPrefix = payloadPrefix; + } + + @Override + public CompletableResultCode export(Collection spans) { + if (isShutdown.get()) { + return CompletableResultCode.ofFailure(); + } + + TraceRequestMarshaler exportRequest = TraceRequestMarshaler.create(spans); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + exportRequest.writeBinaryTo(baos); + String payload = payloadPrefix + Base64.getEncoder().encodeToString(baos.toByteArray()); + sender.send(payload.getBytes(StandardCharsets.UTF_8)); + return CompletableResultCode.ofSuccess(); + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to export spans. Error: " + e.getMessage(), e); + return CompletableResultCode.ofFailure(); + } + } + + @Override + public CompletableResultCode flush() { + // TODO: implement + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + if (!isShutdown.compareAndSet(false, true)) { + logger.log(Level.INFO, "Calling shutdown() multiple times."); + return CompletableResultCode.ofSuccess(); + } + return sender.shutdown(); + } + + // Visible for testing + UdpSender getSender() { + return sender; + } + + // Visible for testing + String getPayloadPrefix() { + return payloadPrefix; + } +} diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/OtlpUdpSpanExporterBuilder.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/OtlpUdpSpanExporterBuilder.java new file mode 100644 index 0000000000..33cc1dca9f --- /dev/null +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/OtlpUdpSpanExporterBuilder.java @@ -0,0 +1,77 @@ +/* + * 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.javaagent.providers; + +import static java.util.Objects.requireNonNull; + +final class OtlpUdpSpanExporterBuilder { + + private static final String DEFAULT_HOST = "127.0.0.1"; + private static final int DEFAULT_PORT = 2000; + + // The protocol header and delimiter is required for sending data to X-Ray Daemon or when running + // in Lambda. + // https://docs.aws.amazon.com/xray/latest/devguide/xray-api-sendingdata.html#xray-api-daemon + private static final String PROTOCOL_HEADER = "{\"format\": \"json\", \"version\": 1}"; + private static final char PROTOCOL_DELIMITER = '\n'; + + // These prefixes help the backend identify if the spans payload is sampled or not. + private static final String FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX = "T1S"; + private static final String FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX = "T1U"; + + private UdpSender sender; + private String tracePayloadPrefix = FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX; + + public OtlpUdpSpanExporterBuilder setEndpoint(String endpoint) { + requireNonNull(endpoint, "endpoint must not be null"); + try { + String[] parts = endpoint.split(":"); + String host = parts[0]; + int port = Integer.parseInt(parts[1]); + this.sender = new UdpSender(host, port); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid endpoint, must be a valid URL: " + endpoint, e); + } + return this; + } + + public OtlpUdpSpanExporterBuilder setPayloadSampleDecision(TracePayloadSampleDecision decision) { + this.tracePayloadPrefix = + decision == TracePayloadSampleDecision.SAMPLED + ? FORMAT_OTEL_SAMPLED_TRACES_BINARY_PREFIX + : FORMAT_OTEL_UNSAMPLED_TRACES_BINARY_PREFIX; + return this; + } + + public OtlpUdpSpanExporter build() { + if (sender == null) { + this.sender = new UdpSender(DEFAULT_HOST, DEFAULT_PORT); + } + return new OtlpUdpSpanExporter( + this.sender, PROTOCOL_HEADER + PROTOCOL_DELIMITER + tracePayloadPrefix); + } + + // Only for testing + OtlpUdpSpanExporterBuilder setSender(UdpSender sender) { + this.sender = sender; + return this; + } +} + +enum TracePayloadSampleDecision { + SAMPLED, + UNSAMPLED +} diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/UdpSender.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/UdpSender.java new file mode 100644 index 0000000000..deb9327e39 --- /dev/null +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/UdpSender.java @@ -0,0 +1,76 @@ +/* + * 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.javaagent.providers; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class represents a UDP sender that sends data to a specified endpoint. It is used to send + * data to a remote host and port using UDP protocol. + */ +class UdpSender { + private static final Logger logger = Logger.getLogger(UdpSender.class.getName()); + + private DatagramSocket socket; + private final InetSocketAddress endpoint; + + public UdpSender(String host, int port) { + this.endpoint = new InetSocketAddress(host, port); + try { + this.socket = new DatagramSocket(); + } catch (SocketException e) { + logger.log(Level.SEVERE, "Exception while instantiating UdpSender socket.", e); + } + } + + public CompletableResultCode shutdown() { + try { + if (socket == null) { + return CompletableResultCode.ofSuccess(); + } + socket.close(); + return CompletableResultCode.ofSuccess(); + } catch (Exception e) { + logger.log(Level.SEVERE, "Exception while closing UdpSender socket.", e); + return CompletableResultCode.ofFailure(); + } + } + + public void send(byte[] data) { + if (socket == null) { + logger.log(Level.WARNING, "UdpSender socket is null. Cannot send data."); + return; + } + DatagramPacket packet = new DatagramPacket(data, data.length, endpoint); + try { + socket.send(packet); + } catch (IOException e) { + logger.log(Level.SEVERE, "Exception while sending data.", e); + } + } + + // Visible for testing + InetSocketAddress getEndpoint() { + return endpoint; + } +} diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/UdpExporterTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/UdpExporterTest.java new file mode 100644 index 0000000000..1494b30c98 --- /dev/null +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/UdpExporterTest.java @@ -0,0 +1,147 @@ +/* + * 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.javaagent.providers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.*; +import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +public class UdpExporterTest { + + @Test + public void testUdpExporterWithDefaults() { + OtlpUdpSpanExporter exporter = new OtlpUdpSpanExporterBuilder().build(); + UdpSender sender = exporter.getSender(); + assertThat(sender.getEndpoint().getHostName()) + .isEqualTo("localhost"); // getHostName implicitly converts 127.0.0.1 to localhost + assertThat(sender.getEndpoint().getPort()).isEqualTo(2000); + assertThat(exporter.getPayloadPrefix()).endsWith("T1S"); + } + + @Test + public void testUdpExporterWithCustomEndpointAndSample() { + OtlpUdpSpanExporter exporter = + new OtlpUdpSpanExporterBuilder() + .setEndpoint("somehost:1000") + .setPayloadSampleDecision(TracePayloadSampleDecision.UNSAMPLED) + .build(); + UdpSender sender = exporter.getSender(); + assertThat(sender.getEndpoint().getHostName()).isEqualTo("somehost"); + assertThat(sender.getEndpoint().getPort()).isEqualTo(1000); + assertThat(exporter.getPayloadPrefix()).endsWith("T1U"); + } + + @Test + public void testUdpExporterWithInvalidEndpoint() { + assertThatThrownBy( + () -> { + new OtlpUdpSpanExporterBuilder().setEndpoint("invalidhost"); + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid endpoint, must be a valid URL: invalidhost"); + } + + @Test + public void testExportDefaultBehavior() { + UdpSender senderMock = mock(UdpSender.class); + + // mock SpanData + SpanData spanData = buildSpanDataMock(); + + OtlpUdpSpanExporter exporter = new OtlpUdpSpanExporterBuilder().setSender(senderMock).build(); + exporter.export(Collections.singletonList(spanData)); + + // assert that the senderMock.send is called once + verify(senderMock, times(1)).send(any(byte[].class)); + verify(senderMock) + .send( + argThat( + (byte[] bytes) -> { + assertThat(bytes.length).isGreaterThan(0); + String payload = new String(bytes, StandardCharsets.UTF_8); + assertThat(payload) + .startsWith("{\"format\": \"json\", \"version\": 1}" + "\n" + "T1S"); + return true; + })); + } + + @Test + public void testExportWithSampledFalse() { + UdpSender senderMock = mock(UdpSender.class); + + // mock SpanData + SpanData spanData = buildSpanDataMock(); + + OtlpUdpSpanExporter exporter = + new OtlpUdpSpanExporterBuilder() + .setSender(senderMock) + .setPayloadSampleDecision(TracePayloadSampleDecision.UNSAMPLED) + .build(); + exporter.export(Collections.singletonList(spanData)); + + verify(senderMock, times(1)).send(any(byte[].class)); + verify(senderMock) + .send( + argThat( + (byte[] bytes) -> { + assertThat(bytes.length).isGreaterThan(0); + String payload = new String(bytes, StandardCharsets.UTF_8); + assertThat(payload) + .startsWith("{\"format\": \"json\", \"version\": 1}" + "\n" + "T1U"); + return true; + })); + } + + private SpanData buildSpanDataMock() { + SpanData mockSpanData = mock(SpanData.class); + + Attributes spanAttributes = + Attributes.of(AttributeKey.stringKey("original key"), "original value"); + when(mockSpanData.getAttributes()).thenReturn(spanAttributes); + when(mockSpanData.getTotalAttributeCount()).thenReturn(spanAttributes.size()); + when(mockSpanData.getKind()).thenReturn(SpanKind.SERVER); + + SpanContext parentSpanContextMock = mock(SpanContext.class); + when(mockSpanData.getParentSpanContext()).thenReturn(parentSpanContextMock); + + SpanContext spanContextMock = mock(SpanContext.class); + when(spanContextMock.isValid()).thenReturn(true); + when(mockSpanData.getSpanContext()).thenReturn(spanContextMock); + + TraceState traceState = TraceState.builder().build(); + when(spanContextMock.getTraceState()).thenReturn(traceState); + + when(mockSpanData.getStatus()).thenReturn(StatusData.unset()); + when(mockSpanData.getInstrumentationScopeInfo()) + .thenReturn(InstrumentationScopeInfo.create("Dummy Scope")); + + Resource testResource = Resource.empty(); + when(mockSpanData.getResource()).thenReturn(testResource); + + return mockSpanData; + } +} From 2f6490b636cc119cce1d48e1c5de6c5f09f364c2 Mon Sep 17 00:00:00 2001 From: Prashant Srivastava <50466688+srprash@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:08:33 -0800 Subject: [PATCH 05/11] Adding AwsUnsampledOnlySpanProcessor to export batches of unsampled spans (#948) --- .../javaagent/providers/AwsAttributeKeys.java | 3 + .../AwsUnsampledOnlySpanProcessor.java | 85 +++++++++ .../AwsUnsampledOnlySpanProcessorBuilder.java | 46 +++++ .../AwsUnsampledOnlySpanProcessorTest.java | 163 ++++++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessor.java create mode 100644 awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorBuilder.java create mode 100644 awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorTest.java diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java index b73794b9db..f9791a31ee 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java @@ -70,6 +70,9 @@ private AwsAttributeKeys() {} static final AttributeKey AWS_LAMBDA_RESOURCE_ID = AttributeKey.stringKey("aws.lambda.resource_mapping.id"); + static final AttributeKey AWS_TRACE_FLAG_SAMPLED = + AttributeKey.booleanKey("aws.trace.flag.sampled"); + // use the same AWS Resource attribute name defined by OTel java auto-instr for aws_sdk_v_1_1 // TODO: all AWS specific attributes should be defined in semconv package and reused cross all // otel packages. Related sim - diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessor.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessor.java new file mode 100644 index 0000000000..3848016f3b --- /dev/null +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessor.java @@ -0,0 +1,85 @@ +/* + * 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.javaagent.providers; + +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; + +/** + * {@link SpanProcessor} that only exports unsampled spans in a batch via a delegated @{link + * BatchSpanProcessor}. The processor also adds an attribute to each processed span to indicate that + * it was sampled or not. + */ +final class AwsUnsampledOnlySpanProcessor implements SpanProcessor { + + private final SpanProcessor delegate; + + AwsUnsampledOnlySpanProcessor(SpanProcessor delegate) { + this.delegate = delegate; + } + + public static AwsUnsampledOnlySpanProcessorBuilder builder() { + return new AwsUnsampledOnlySpanProcessorBuilder(); + } + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) { + if (!span.getSpanContext().isSampled()) { + span.setAttribute(AwsAttributeKeys.AWS_TRACE_FLAG_SAMPLED, false); + } + delegate.onStart(parentContext, span); + } + + @Override + public void onEnd(ReadableSpan span) { + if (!span.getSpanContext().isSampled()) { + delegate.onEnd(span); + } + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public boolean isEndRequired() { + return true; + } + + @Override + public CompletableResultCode shutdown() { + return delegate.shutdown(); + } + + @Override + public CompletableResultCode forceFlush() { + return delegate.forceFlush(); + } + + @Override + public void close() { + delegate.close(); + } + + // Visible for testing + SpanProcessor getDelegate() { + return delegate; + } +} diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorBuilder.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorBuilder.java new file mode 100644 index 0000000000..89efbcf3b4 --- /dev/null +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorBuilder.java @@ -0,0 +1,46 @@ +/* + * 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.javaagent.providers; + +import static java.util.Objects.requireNonNull; + +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; + +final class AwsUnsampledOnlySpanProcessorBuilder { + + // Default exporter is OtlpUdpSpanExporter with unsampled payload prefix + private SpanExporter exporter = + new OtlpUdpSpanExporterBuilder() + .setPayloadSampleDecision(TracePayloadSampleDecision.UNSAMPLED) + .build(); + + public AwsUnsampledOnlySpanProcessorBuilder setSpanExporter(SpanExporter exporter) { + requireNonNull(exporter, "exporter cannot be null"); + this.exporter = exporter; + return this; + } + + public AwsUnsampledOnlySpanProcessor build() { + BatchSpanProcessor bsp = + BatchSpanProcessor.builder(exporter).setExportUnsampledSpans(true).build(); + return new AwsUnsampledOnlySpanProcessor(bsp); + } + + SpanExporter getSpanExporter() { + return exporter; + } +} diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorTest.java new file mode 100644 index 0000000000..ba41740bd6 --- /dev/null +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorTest.java @@ -0,0 +1,163 @@ +/* + * 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.javaagent.providers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.ArrayList; +import java.util.Collection; +import org.junit.jupiter.api.Test; + +public class AwsUnsampledOnlySpanProcessorTest { + + @Test + public void testIsStartRequired() { + SpanProcessor processor = AwsUnsampledOnlySpanProcessor.builder().build(); + assertThat(processor.isStartRequired()).isTrue(); + } + + @Test + public void testIsEndRequired() { + SpanProcessor processor = AwsUnsampledOnlySpanProcessor.builder().build(); + assertThat(processor.isEndRequired()).isTrue(); + } + + @Test + public void testDefaultSpanProcessor() { + AwsUnsampledOnlySpanProcessorBuilder builder = AwsUnsampledOnlySpanProcessor.builder(); + AwsUnsampledOnlySpanProcessor unsampledSP = builder.build(); + + assertThat(builder.getSpanExporter()).isInstanceOf(OtlpUdpSpanExporter.class); + SpanProcessor delegate = unsampledSP.getDelegate(); + assertThat(delegate).isInstanceOf(BatchSpanProcessor.class); + BatchSpanProcessor delegateBsp = (BatchSpanProcessor) delegate; + String delegateBspString = delegateBsp.toString(); + assertThat(delegateBspString) + .contains( + "spanExporter=software.amazon.opentelemetry.javaagent.providers.OtlpUdpSpanExporter"); + assertThat(delegateBspString).contains("exportUnsampledSpans=true"); + } + + @Test + public void testSpanProcessorWithExporter() { + AwsUnsampledOnlySpanProcessorBuilder builder = + AwsUnsampledOnlySpanProcessor.builder().setSpanExporter(InMemorySpanExporter.create()); + AwsUnsampledOnlySpanProcessor unsampledSP = builder.build(); + + assertThat(builder.getSpanExporter()).isInstanceOf(InMemorySpanExporter.class); + SpanProcessor delegate = unsampledSP.getDelegate(); + assertThat(delegate).isInstanceOf(BatchSpanProcessor.class); + BatchSpanProcessor delegateBsp = (BatchSpanProcessor) delegate; + String delegateBspString = delegateBsp.toString(); + assertThat(delegateBspString) + .contains("spanExporter=io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter"); + assertThat(delegateBspString).contains("exportUnsampledSpans=true"); + } + + @Test + public void testStartAddsAttributeToSampledSpan() { + SpanContext mockSpanContext = mock(SpanContext.class); + when(mockSpanContext.isSampled()).thenReturn(true); + Context parentContextMock = mock(Context.class); + ReadWriteSpan spanMock = mock(ReadWriteSpan.class); + when(spanMock.getSpanContext()).thenReturn(mockSpanContext); + + AwsUnsampledOnlySpanProcessor processor = AwsUnsampledOnlySpanProcessor.builder().build(); + processor.onStart(parentContextMock, spanMock); + + // verify setAttribute was never called + verify(spanMock, never()).setAttribute(any(), anyBoolean()); + } + + @Test + public void testStartAddsAttributeToUnsampledSpan() { + SpanContext mockSpanContext = mock(SpanContext.class); + when(mockSpanContext.isSampled()).thenReturn(false); + Context parentContextMock = mock(Context.class); + ReadWriteSpan spanMock = mock(ReadWriteSpan.class); + when(spanMock.getSpanContext()).thenReturn(mockSpanContext); + + AwsUnsampledOnlySpanProcessor processor = AwsUnsampledOnlySpanProcessor.builder().build(); + processor.onStart(parentContextMock, spanMock); + + // verify setAttribute was called with the correct arguments + verify(spanMock, times(1)).setAttribute(AwsAttributeKeys.AWS_TRACE_FLAG_SAMPLED, false); + } + + @Test + public void testExportsOnlyUnsampledSpans() { + SpanExporter mockExporter = mock(SpanExporter.class); + when(mockExporter.export(anyCollection())).thenReturn(CompletableResultCode.ofSuccess()); + + TestDelegateProcessor delegate = new TestDelegateProcessor(); + AwsUnsampledOnlySpanProcessor processor = new AwsUnsampledOnlySpanProcessor(delegate); + + // unsampled span + SpanContext mockSpanContextUnsampled = mock(SpanContext.class); + when(mockSpanContextUnsampled.isSampled()).thenReturn(false); + ReadableSpan mockSpanUnsampled = mock(ReadableSpan.class); + when(mockSpanUnsampled.getSpanContext()).thenReturn(mockSpanContextUnsampled); + + // sampled span + SpanContext mockSpanContextSampled = mock(SpanContext.class); + when(mockSpanContextSampled.isSampled()).thenReturn(true); + ReadableSpan mockSpanSampled = mock(ReadableSpan.class); + when(mockSpanSampled.getSpanContext()).thenReturn(mockSpanContextSampled); + + processor.onEnd(mockSpanSampled); + processor.onEnd(mockSpanUnsampled); + + // validate that only the unsampled span was delegated + assertThat(delegate.getEndedSpans()).containsExactly(mockSpanUnsampled); + } + + private static class TestDelegateProcessor implements SpanProcessor { + // keep a queue of Readable spans added when onEnd is called + Collection endedSpans = new ArrayList<>(); + + @Override + public void onStart(Context parentContext, ReadWriteSpan span) {} + + @Override + public boolean isStartRequired() { + return false; + } + + @Override + public void onEnd(ReadableSpan span) { + endedSpans.add(span); + } + + @Override + public boolean isEndRequired() { + return false; + } + + public Collection getEndedSpans() { + return endedSpans; + } + } +} From b58b94f1dd64d8f4b7d5658afc009d2bea0567a3 Mon Sep 17 00:00:00 2001 From: Prashant Srivastava <50466688+srprash@users.noreply.github.com> Date: Fri, 15 Nov 2024 13:00:47 -0800 Subject: [PATCH 06/11] Configure SDK when running in Lambda environment (#950) --- ...sApplicationSignalsCustomizerProvider.java | 78 +++++++++++++++++++ .../providers/AwsSpanProcessingUtil.java | 8 +- .../AwsUnsampledOnlySpanProcessor.java | 4 - .../AwsUnsampledOnlySpanProcessorBuilder.java | 17 +++- .../AwsUnsampledOnlySpanProcessorTest.java | 27 +++++-- 5 files changed, 122 insertions(+), 12 deletions(-) diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java index 4c4415a89d..c1ffc13884 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java @@ -20,8 +20,10 @@ import io.opentelemetry.contrib.awsxray.AlwaysRecordSampler; import io.opentelemetry.contrib.awsxray.ResourceHolder; import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.exporter.otlp.internal.OtlpConfigUtil; import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; @@ -45,6 +47,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,6 +69,8 @@ */ public class AwsApplicationSignalsCustomizerProvider implements AutoConfigurationCustomizerProvider { + static final String AWS_LAMBDA_FUNCTION_NAME_CONFIG = "AWS_LAMBDA_FUNCTION_NAME"; + private static final Duration DEFAULT_METRIC_EXPORT_INTERVAL = Duration.ofMinutes(1); private static final Logger logger = Logger.getLogger(AwsApplicationSignalsCustomizerProvider.class.getName()); @@ -85,9 +90,23 @@ public class AwsApplicationSignalsCustomizerProvider "otel.aws.application.signals.exporter.endpoint"; private static final String OTEL_JMX_TARGET_SYSTEM_CONFIG = "otel.jmx.target.system"; + private static final String OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_CONFIG = + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"; + private static final String AWS_XRAY_DAEMON_ADDRESS_CONFIG = "AWS_XRAY_DAEMON_ADDRESS"; + private static final String DEFAULT_UDP_ENDPOINT = "127.0.0.1:2000"; + private static final String OTEL_DISABLED_RESOURCE_PROVIDERS_CONFIG = + "otel.java.disabled.resource.providers"; + private static final String OTEL_BSP_MAX_EXPORT_BATCH_SIZE_CONFIG = + "otel.bsp.max.export.batch.size"; + + // UDP packet can be upto 64KB. To limit the packet size, we limit the exported batch size. + // This is a bit of a magic number, as there is no simple way to tell how many spans can make a + // 64KB batch since spans can vary in size. + private static final int LAMBDA_SPAN_EXPORT_BATCH_SIZE = 10; public void customize(AutoConfigurationCustomizer autoConfiguration) { autoConfiguration.addPropertiesCustomizer(this::customizeProperties); + autoConfiguration.addPropertiesCustomizer(this::customizeLambdaEnvProperties); autoConfiguration.addResourceCustomizer(this::customizeResource); autoConfiguration.addSamplerCustomizer(this::customizeSampler); autoConfiguration.addTracerProviderCustomizer(this::customizeTracerProviderBuilder); @@ -95,6 +114,10 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { autoConfiguration.addSpanExporterCustomizer(this::customizeSpanExporter); } + static boolean isLambdaEnvironment() { + return System.getenv(AWS_LAMBDA_FUNCTION_NAME_CONFIG) != null; + } + private boolean isApplicationSignalsEnabled(ConfigProperties configProps) { return configProps.getBoolean( APPLICATION_SIGNALS_ENABLED_CONFIG, @@ -126,6 +149,30 @@ private Map customizeProperties(ConfigProperties configProps) { return Collections.emptyMap(); } + private Map customizeLambdaEnvProperties(ConfigProperties configProperties) { + if (isLambdaEnvironment()) { + Map propsOverride = new HashMap<>(2); + + // Disable other AWS Resource Providers + List list = configProperties.getList(OTEL_DISABLED_RESOURCE_PROVIDERS_CONFIG); + List disabledResourceProviders = new ArrayList<>(list); + disabledResourceProviders.add( + "io.opentelemetry.contrib.aws.resource.BeanstalkResourceProvider"); + disabledResourceProviders.add("io.opentelemetry.contrib.aws.resource.Ec2ResourceProvider"); + disabledResourceProviders.add("io.opentelemetry.contrib.aws.resource.EcsResourceProvider"); + disabledResourceProviders.add("io.opentelemetry.contrib.aws.resource.EksResourceProvider"); + propsOverride.put( + OTEL_DISABLED_RESOURCE_PROVIDERS_CONFIG, String.join(",", disabledResourceProviders)); + + // Set the max export batch size for BatchSpanProcessors + propsOverride.put( + OTEL_BSP_MAX_EXPORT_BATCH_SIZE_CONFIG, String.valueOf(LAMBDA_SPAN_EXPORT_BATCH_SIZE)); + + return propsOverride; + } + return Collections.emptyMap(); + } + private Resource customizeResource(Resource resource, ConfigProperties configProps) { if (isApplicationSignalsEnabled(configProps)) { AttributesBuilder builder = Attributes.builder(); @@ -156,6 +203,17 @@ private SdkTracerProviderBuilder customizeTracerProviderBuilder( // Construct and set local and remote attributes span processor tracerProviderBuilder.addSpanProcessor( AttributePropagatingSpanProcessorBuilder.create().build()); + + // If running on Lambda, we just need to export 100% spans and skip generating any Application + // Signals metrics. + if (isLambdaEnvironment()) { + tracerProviderBuilder.addSpanProcessor( + AwsUnsampledOnlySpanProcessorBuilder.create() + .setMaxExportBatchSize(LAMBDA_SPAN_EXPORT_BATCH_SIZE) + .build()); + return tracerProviderBuilder; + } + // Construct meterProvider MetricExporter metricsExporter = ApplicationSignalsExporterProvider.INSTANCE.createExporter(configProps); @@ -207,6 +265,21 @@ private SdkMeterProviderBuilder customizeMeterProvider( private SpanExporter customizeSpanExporter( SpanExporter spanExporter, ConfigProperties configProps) { + // When running in Lambda, override the default OTLP exporter with UDP exporter + if (isLambdaEnvironment()) { + if (isOtlpSpanExporter(spanExporter) + && System.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT_CONFIG) == null) { + String tracesEndpoint = + Optional.ofNullable(System.getenv(AWS_XRAY_DAEMON_ADDRESS_CONFIG)) + .orElse(DEFAULT_UDP_ENDPOINT); + spanExporter = + new OtlpUdpSpanExporterBuilder() + .setPayloadSampleDecision(TracePayloadSampleDecision.SAMPLED) + .setEndpoint(tracesEndpoint) + .build(); + } + } + if (isApplicationSignalsEnabled(configProps)) { return AwsMetricAttributesSpanExporterBuilder.create( spanExporter, ResourceHolder.getResource()) @@ -216,6 +289,11 @@ private SpanExporter customizeSpanExporter( return spanExporter; } + private boolean isOtlpSpanExporter(SpanExporter spanExporter) { + return spanExporter instanceof OtlpGrpcSpanExporter + || spanExporter instanceof OtlpHttpSpanExporter; + } + private enum ApplicationSignalsExporterProvider { INSTANCE; diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtil.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtil.java index 1f0a75705c..4a0b22858f 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtil.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtil.java @@ -23,6 +23,8 @@ import static io.opentelemetry.semconv.SemanticAttributes.MESSAGING_OPERATION; import static io.opentelemetry.semconv.SemanticAttributes.MessagingOperationValues.PROCESS; import static io.opentelemetry.semconv.SemanticAttributes.RPC_SYSTEM; +import static software.amazon.opentelemetry.javaagent.providers.AwsApplicationSignalsCustomizerProvider.AWS_LAMBDA_FUNCTION_NAME_CONFIG; +import static software.amazon.opentelemetry.javaagent.providers.AwsApplicationSignalsCustomizerProvider.isLambdaEnvironment; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LOCAL_OPERATION; import com.fasterxml.jackson.core.type.TypeReference; @@ -82,9 +84,13 @@ static List getDialectKeywords() { /** * Ingress operation (i.e. operation for Server and Consumer spans) will be generated from * "http.method + http.target/with the first API path parameter" if the default span name equals - * null, UnknownOperation or http.method value. + * null, UnknownOperation or http.method value. If running in Lambda, the ingress operation will + * be the function name + /FunctionHandler. */ static String getIngressOperation(SpanData span) { + if (isLambdaEnvironment()) { + return System.getenv(AWS_LAMBDA_FUNCTION_NAME_CONFIG) + "/FunctionHandler"; + } String operation = span.getName(); if (shouldUseInternalOperation(span)) { operation = INTERNAL_OPERATION; diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessor.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessor.java index 3848016f3b..6b7ee1b20d 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessor.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessor.java @@ -34,10 +34,6 @@ final class AwsUnsampledOnlySpanProcessor implements SpanProcessor { this.delegate = delegate; } - public static AwsUnsampledOnlySpanProcessorBuilder builder() { - return new AwsUnsampledOnlySpanProcessorBuilder(); - } - @Override public void onStart(Context parentContext, ReadWriteSpan span) { if (!span.getSpanContext().isSampled()) { diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorBuilder.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorBuilder.java index 89efbcf3b4..c0227d4e9d 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorBuilder.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorBuilder.java @@ -21,6 +21,9 @@ import io.opentelemetry.sdk.trace.export.SpanExporter; final class AwsUnsampledOnlySpanProcessorBuilder { + public static AwsUnsampledOnlySpanProcessorBuilder create() { + return new AwsUnsampledOnlySpanProcessorBuilder(); + } // Default exporter is OtlpUdpSpanExporter with unsampled payload prefix private SpanExporter exporter = @@ -28,18 +31,30 @@ final class AwsUnsampledOnlySpanProcessorBuilder { .setPayloadSampleDecision(TracePayloadSampleDecision.UNSAMPLED) .build(); + // Default batch size to be same as Otel BSP default + private int maxExportBatchSize = 512; + public AwsUnsampledOnlySpanProcessorBuilder setSpanExporter(SpanExporter exporter) { requireNonNull(exporter, "exporter cannot be null"); this.exporter = exporter; return this; } + public AwsUnsampledOnlySpanProcessorBuilder setMaxExportBatchSize(int maxExportBatchSize) { + this.maxExportBatchSize = maxExportBatchSize; + return this; + } + public AwsUnsampledOnlySpanProcessor build() { BatchSpanProcessor bsp = - BatchSpanProcessor.builder(exporter).setExportUnsampledSpans(true).build(); + BatchSpanProcessor.builder(exporter) + .setExportUnsampledSpans(true) + .setMaxExportBatchSize(maxExportBatchSize) + .build(); return new AwsUnsampledOnlySpanProcessor(bsp); } + // Visible for testing SpanExporter getSpanExporter() { return exporter; } diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorTest.java index ba41740bd6..ca411fefaf 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsUnsampledOnlySpanProcessorTest.java @@ -35,19 +35,19 @@ public class AwsUnsampledOnlySpanProcessorTest { @Test public void testIsStartRequired() { - SpanProcessor processor = AwsUnsampledOnlySpanProcessor.builder().build(); + SpanProcessor processor = AwsUnsampledOnlySpanProcessorBuilder.create().build(); assertThat(processor.isStartRequired()).isTrue(); } @Test public void testIsEndRequired() { - SpanProcessor processor = AwsUnsampledOnlySpanProcessor.builder().build(); + SpanProcessor processor = AwsUnsampledOnlySpanProcessorBuilder.create().build(); assertThat(processor.isEndRequired()).isTrue(); } @Test public void testDefaultSpanProcessor() { - AwsUnsampledOnlySpanProcessorBuilder builder = AwsUnsampledOnlySpanProcessor.builder(); + AwsUnsampledOnlySpanProcessorBuilder builder = AwsUnsampledOnlySpanProcessorBuilder.create(); AwsUnsampledOnlySpanProcessor unsampledSP = builder.build(); assertThat(builder.getSpanExporter()).isInstanceOf(OtlpUdpSpanExporter.class); @@ -59,12 +59,14 @@ public void testDefaultSpanProcessor() { .contains( "spanExporter=software.amazon.opentelemetry.javaagent.providers.OtlpUdpSpanExporter"); assertThat(delegateBspString).contains("exportUnsampledSpans=true"); + assertThat(delegateBspString).contains("maxExportBatchSize=512"); } @Test public void testSpanProcessorWithExporter() { AwsUnsampledOnlySpanProcessorBuilder builder = - AwsUnsampledOnlySpanProcessor.builder().setSpanExporter(InMemorySpanExporter.create()); + AwsUnsampledOnlySpanProcessorBuilder.create() + .setSpanExporter(InMemorySpanExporter.create()); AwsUnsampledOnlySpanProcessor unsampledSP = builder.build(); assertThat(builder.getSpanExporter()).isInstanceOf(InMemorySpanExporter.class); @@ -77,6 +79,19 @@ public void testSpanProcessorWithExporter() { assertThat(delegateBspString).contains("exportUnsampledSpans=true"); } + @Test + public void testSpanProcessorWithBatchSize() { + AwsUnsampledOnlySpanProcessorBuilder builder = + AwsUnsampledOnlySpanProcessorBuilder.create().setMaxExportBatchSize(100); + AwsUnsampledOnlySpanProcessor unsampledSP = builder.build(); + + SpanProcessor delegate = unsampledSP.getDelegate(); + assertThat(delegate).isInstanceOf(BatchSpanProcessor.class); + BatchSpanProcessor delegateBsp = (BatchSpanProcessor) delegate; + String delegateBspString = delegateBsp.toString(); + assertThat(delegateBspString).contains("maxExportBatchSize=100"); + } + @Test public void testStartAddsAttributeToSampledSpan() { SpanContext mockSpanContext = mock(SpanContext.class); @@ -85,7 +100,7 @@ public void testStartAddsAttributeToSampledSpan() { ReadWriteSpan spanMock = mock(ReadWriteSpan.class); when(spanMock.getSpanContext()).thenReturn(mockSpanContext); - AwsUnsampledOnlySpanProcessor processor = AwsUnsampledOnlySpanProcessor.builder().build(); + AwsUnsampledOnlySpanProcessor processor = AwsUnsampledOnlySpanProcessorBuilder.create().build(); processor.onStart(parentContextMock, spanMock); // verify setAttribute was never called @@ -100,7 +115,7 @@ public void testStartAddsAttributeToUnsampledSpan() { ReadWriteSpan spanMock = mock(ReadWriteSpan.class); when(spanMock.getSpanContext()).thenReturn(mockSpanContext); - AwsUnsampledOnlySpanProcessor processor = AwsUnsampledOnlySpanProcessor.builder().build(); + AwsUnsampledOnlySpanProcessor processor = AwsUnsampledOnlySpanProcessorBuilder.create().build(); processor.onStart(parentContextMock, spanMock); // verify setAttribute was called with the correct arguments From 84699838fc0bcb58a72baa8152027f3e1e6cd364 Mon Sep 17 00:00:00 2001 From: Michael He <53622546+yiyuan-he@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:13:59 -0800 Subject: [PATCH 07/11] Add Contract Tests for LLM Attributes and Models (#952) *Description of changes:* Add new contract tests for Gen AI attributes and models. *Test Plan:* contract-tests-pr By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --- .../opentelemetry-java-instrumentation.patch | 2947 +++++------------ .../test/awssdk/base/AwsSdkBaseTest.java | 374 ++- .../test/awssdk/v1/AwsSdkV1Test.java | 29 +- .../test/awssdk/v2/AwsSdkV2Test.java | 37 +- .../utils/SemanticConventionsConstants.java | 6 + .../main/java/com/amazon/sampleapp/Utils.java | 66 +- .../main/java/com/amazon/sampleapp/App.java | 173 + .../main/java/com/amazon/sampleapp/App.java | 166 + 8 files changed, 1750 insertions(+), 2048 deletions(-) diff --git a/.github/patches/opentelemetry-java-instrumentation.patch b/.github/patches/opentelemetry-java-instrumentation.patch index 98f517c572..45e804a057 100644 --- a/.github/patches/opentelemetry-java-instrumentation.patch +++ b/.github/patches/opentelemetry-java-instrumentation.patch @@ -1,5 +1,5 @@ diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts -index f357a19f88..8a78577580 100644 +index f357a19f88..fa90530579 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts @@ -47,6 +47,14 @@ dependencies { @@ -8,7 +8,7 @@ index f357a19f88..8a78577580 100644 testLibrary("com.amazonaws:aws-java-sdk-sns:1.11.106") + testLibrary("com.amazonaws:aws-java-sdk-sqs:1.11.106") + testLibrary("com.amazonaws:aws-java-sdk-secretsmanager:1.11.309") -+ // testLibrary("com.amazonaws:aws-java-sdk-stepfunctions:1.11.230") ++ testLibrary("com.amazonaws:aws-java-sdk-stepfunctions:1.11.230") + testLibrary("com.amazonaws:aws-java-sdk-lambda:1.11.678") + testLibrary("com.amazonaws:aws-java-sdk-bedrock:1.12.744") + testLibrary("com.amazonaws:aws-java-sdk-bedrockagent:1.12.744") @@ -18,1765 +18,63 @@ index f357a19f88..8a78577580 100644 testImplementation(project(":instrumentation:aws-sdk:aws-sdk-1.11:testing")) diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/S3TracingTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/S3TracingTest.groovy -index 987a50ed95..a39b216252 100644 +index 987a50ed95..889c856a7c 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/S3TracingTest.groovy +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/S3TracingTest.groovy -@@ -19,679 +19,679 @@ class S3TracingTest extends AgentInstrumentationSpecification { - awsConnector.disconnect() - } - -- def "S3 upload triggers SQS message"() { -- setup: -- String queueName = "s3ToSqsTestQueue" -- String bucketName = "otel-s3-to-sqs-test-bucket" -+ //def "S3 upload triggers SQS message"() { -+ // setup: -+ // String queueName = "s3ToSqsTestQueue" -+ // String bucketName = "otel-s3-to-sqs-test-bucket" -+ // -+ // String queueUrl = awsConnector.createQueue(queueName) -+ // awsConnector.createBucket(bucketName) -+ // -+ // String queueArn = awsConnector.getQueueArn(queueUrl) -+ // awsConnector.setQueuePublishingPolicy(queueUrl, queueArn) -+ // awsConnector.enableS3ToSqsNotifications(bucketName, queueArn) -+ // -+ // when: -+ // // test message, auto created by AWS -+ // awsConnector.receiveMessage(queueUrl) -+ // awsConnector.putSampleData(bucketName) -+ // // traced message -+ // def receiveMessageResult = awsConnector.receiveMessage(queueUrl) -+ // receiveMessageResult.messages.each {message -> -+ // runWithSpan("process child") {} -+ // } -+ // -+ // // cleanup -+ // awsConnector.deleteBucket(bucketName) -+ // awsConnector.purgeQueue(queueUrl) -+ // -+ // then: -+ // assertTraces(10) { -+ // trace(0, 1) { -+ // -+ // span(0) { -+ // name "SQS.CreateQueue" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "CreateQueue" -+ // "aws.queue.name" queueName -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(1, 1) { -+ // -+ // span(0) { -+ // name "S3.CreateBucket" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "CreateBucket" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "Amazon S3" -+ // "aws.bucket.name" bucketName -+ // "http.method" "PUT" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(2, 1) { -+ // -+ // span(0) { -+ // name "SQS.GetQueueAttributes" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "GetQueueAttributes" -+ // "aws.queue.url" queueUrl -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(3, 1) { -+ // -+ // span(0) { -+ // name "SQS.SetQueueAttributes" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "SetQueueAttributes" -+ // "aws.queue.url" queueUrl -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(4, 1) { -+ // -+ // span(0) { -+ // name "S3.SetBucketNotificationConfiguration" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "SetBucketNotificationConfiguration" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "Amazon S3" -+ // "aws.bucket.name" bucketName -+ // "http.method" "PUT" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(5, 3) { -+ // span(0) { -+ // name "S3.PutObject" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "PutObject" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "Amazon S3" -+ // "aws.bucket.name" bucketName -+ // "http.method" "PUT" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // span(1) { -+ // name "s3ToSqsTestQueue process" -+ // kind CONSUMER -+ // childOf span(0) -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "ReceiveMessage" -+ // "aws.queue.url" queueUrl -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "http.method" "POST" -+ // "http.url" String -+ // "net.peer.name" String -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.MESSAGING_SYSTEM" "AmazonSQS" -+ // "$SemanticAttributes.MESSAGING_DESTINATION_NAME" "s3ToSqsTestQueue" -+ // "$SemanticAttributes.MESSAGING_OPERATION" "process" -+ // "$SemanticAttributes.MESSAGING_MESSAGE_ID" String -+ // } -+ // } -+ // span(2) { -+ // name "process child" -+ // childOf span(1) -+ // attributes { -+ // } -+ // } -+ // } -+ // trace(6, 1) { -+ // span(0) { -+ // name "S3.ListObjects" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "ListObjects" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "Amazon S3" -+ // "aws.bucket.name" bucketName -+ // "http.method" "GET" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(7, 1) { -+ // span(0) { -+ // name "S3.DeleteObject" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "DeleteObject" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "Amazon S3" -+ // "aws.bucket.name" bucketName -+ // "http.method" "DELETE" -+ // "http.status_code" 204 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(8, 1) { -+ // span(0) { -+ // name "S3.DeleteBucket" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "DeleteBucket" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "Amazon S3" -+ // "aws.bucket.name" bucketName -+ // "http.method" "DELETE" -+ // "http.status_code" 204 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(9, 1) { -+ // span(0) { -+ // name "SQS.PurgeQueue" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "PurgeQueue" -+ // "aws.queue.url" queueUrl -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // } -+ //} - -- String queueUrl = awsConnector.createQueue(queueName) -- awsConnector.createBucket(bucketName) -- -- String queueArn = awsConnector.getQueueArn(queueUrl) -- awsConnector.setQueuePublishingPolicy(queueUrl, queueArn) -- awsConnector.enableS3ToSqsNotifications(bucketName, queueArn) -- -- when: -- // test message, auto created by AWS -- awsConnector.receiveMessage(queueUrl) -- awsConnector.putSampleData(bucketName) -- // traced message -- def receiveMessageResult = awsConnector.receiveMessage(queueUrl) -- receiveMessageResult.messages.each {message -> -- runWithSpan("process child") {} -- } -- -- // cleanup -- awsConnector.deleteBucket(bucketName) -- awsConnector.purgeQueue(queueUrl) -- -- then: -- assertTraces(10) { -- trace(0, 1) { -- -- span(0) { -- name "SQS.CreateQueue" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "CreateQueue" -- "aws.queue.name" queueName -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(1, 1) { -- -- span(0) { -- name "S3.CreateBucket" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "CreateBucket" -- "rpc.system" "aws-api" -- "rpc.service" "Amazon S3" -- "aws.bucket.name" bucketName -- "http.method" "PUT" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(2, 1) { -- -- span(0) { -- name "SQS.GetQueueAttributes" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "GetQueueAttributes" -- "aws.queue.url" queueUrl -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(3, 1) { -- -- span(0) { -- name "SQS.SetQueueAttributes" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "SetQueueAttributes" -- "aws.queue.url" queueUrl -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(4, 1) { -- -- span(0) { -- name "S3.SetBucketNotificationConfiguration" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "SetBucketNotificationConfiguration" -- "rpc.system" "aws-api" -- "rpc.service" "Amazon S3" -- "aws.bucket.name" bucketName -- "http.method" "PUT" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(5, 3) { -- span(0) { -- name "S3.PutObject" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "PutObject" -- "rpc.system" "aws-api" -- "rpc.service" "Amazon S3" -- "aws.bucket.name" bucketName -- "http.method" "PUT" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- span(1) { -- name "s3ToSqsTestQueue process" -- kind CONSUMER -- childOf span(0) -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "ReceiveMessage" -- "aws.queue.url" queueUrl -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "http.method" "POST" -- "http.url" String -- "net.peer.name" String -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.MESSAGING_SYSTEM" "AmazonSQS" -- "$SemanticAttributes.MESSAGING_DESTINATION_NAME" "s3ToSqsTestQueue" -- "$SemanticAttributes.MESSAGING_OPERATION" "process" -- "$SemanticAttributes.MESSAGING_MESSAGE_ID" String -- } -- } -- span(2) { -- name "process child" -- childOf span(1) -- attributes { -- } -- } -- } -- trace(6, 1) { -- span(0) { -- name "S3.ListObjects" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "ListObjects" -- "rpc.system" "aws-api" -- "rpc.service" "Amazon S3" -- "aws.bucket.name" bucketName -- "http.method" "GET" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(7, 1) { -- span(0) { -- name "S3.DeleteObject" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "DeleteObject" -- "rpc.system" "aws-api" -- "rpc.service" "Amazon S3" -- "aws.bucket.name" bucketName -- "http.method" "DELETE" -- "http.status_code" 204 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(8, 1) { -- span(0) { -- name "S3.DeleteBucket" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "DeleteBucket" -- "rpc.system" "aws-api" -- "rpc.service" "Amazon S3" -- "aws.bucket.name" bucketName -- "http.method" "DELETE" -- "http.status_code" 204 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(9, 1) { -- span(0) { -- name "SQS.PurgeQueue" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "PurgeQueue" -- "aws.queue.url" queueUrl -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- } -- } -- -- def "S3 upload triggers SNS topic notification, then creates SQS message"() { -- setup: -- String queueName = "s3ToSnsToSqsTestQueue" -- String bucketName = "otel-s3-sns-sqs-test-bucket" -- String topicName = "s3ToSnsToSqsTestTopic" -- -- String queueUrl = awsConnector.createQueue(queueName) -- String queueArn = awsConnector.getQueueArn(queueUrl) -- awsConnector.createBucket(bucketName) -- String topicArn = awsConnector.createTopicAndSubscribeQueue(topicName, queueArn) -- -- awsConnector.setQueuePublishingPolicy(queueUrl, queueArn) -- awsConnector.setTopicPublishingPolicy(topicArn) -- awsConnector.enableS3ToSnsNotifications(bucketName, topicArn) -- -- when: -- // test message, auto created by AWS -- awsConnector.receiveMessage(queueUrl) -- awsConnector.putSampleData(bucketName) -- // traced message -- def receiveMessageResult = awsConnector.receiveMessage(queueUrl) -- receiveMessageResult.messages.each {message -> -- runWithSpan("process child") {} -- } -- // cleanup -- awsConnector.deleteBucket(bucketName) -- awsConnector.purgeQueue(queueUrl) -- -- then: -- assertTraces(14) { -- trace(0, 1) { -- span(0) { -- name "SQS.CreateQueue" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "CreateQueue" -- "aws.queue.name" queueName -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(1, 1) { -- span(0) { -- name "SQS.GetQueueAttributes" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "GetQueueAttributes" -- "aws.queue.url" queueUrl -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(2, 1) { -- span(0) { -- name "S3.CreateBucket" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "CreateBucket" -- "rpc.system" "aws-api" -- "rpc.service" "Amazon S3" -- "aws.bucket.name" bucketName -- "http.method" "PUT" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(3, 1) { -- span(0) { -- name "SNS.CreateTopic" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "CreateTopic" -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSNS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(4, 1) { -- span(0) { -- name "SNS.Subscribe" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "Subscribe" -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSNS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(5, 1) { -- span(0) { -- name "SQS.SetQueueAttributes" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "SetQueueAttributes" -- "aws.queue.url" queueUrl -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(6, 1) { -- span(0) { -- name "SNS.SetTopicAttributes" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "SetTopicAttributes" -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSNS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(7, 1) { -- span(0) { -- name "S3.SetBucketNotificationConfiguration" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "SetBucketNotificationConfiguration" -- "rpc.system" "aws-api" -- "rpc.service" "Amazon S3" -- "aws.bucket.name" bucketName -- "http.method" "PUT" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(8, 1) { -- span(0) { -- name "S3.PutObject" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "PutObject" -- "rpc.system" "aws-api" -- "rpc.service" "Amazon S3" -- "aws.bucket.name" bucketName -- "http.method" "PUT" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(9, 2) { -- span(0) { -- name "s3ToSnsToSqsTestQueue process" -- kind CONSUMER -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "ReceiveMessage" -- "aws.queue.url" queueUrl -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "http.method" "POST" -- "http.url" String -- "net.peer.name" String -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.MESSAGING_SYSTEM" "AmazonSQS" -- "$SemanticAttributes.MESSAGING_DESTINATION_NAME" "s3ToSnsToSqsTestQueue" -- "$SemanticAttributes.MESSAGING_OPERATION" "process" -- "$SemanticAttributes.MESSAGING_MESSAGE_ID" String -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- span(1) { -- name "process child" -- childOf span(0) -- attributes { -- } -- } -- } -- trace(10, 1) { -- span(0) { -- name "S3.ListObjects" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "ListObjects" -- "rpc.system" "aws-api" -- "rpc.service" "Amazon S3" -- "aws.bucket.name" bucketName -- "http.method" "GET" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(11, 1) { -- span(0) { -- name "S3.DeleteObject" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "DeleteObject" -- "rpc.system" "aws-api" -- "rpc.service" "Amazon S3" -- "aws.bucket.name" bucketName -- "http.method" "DELETE" -- "http.status_code" 204 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(12, 1) { -- span(0) { -- name "S3.DeleteBucket" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "DeleteBucket" -- "rpc.system" "aws-api" -- "rpc.service" "Amazon S3" -- "aws.bucket.name" bucketName -- "http.method" "DELETE" -- "http.status_code" 204 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- trace(13, 1) { -- span(0) { -- name "SQS.PurgeQueue" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "PurgeQueue" -- "aws.queue.url" queueUrl -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -- } -- } -- } -- } -- } -+ //def "S3 upload triggers SNS topic notification, then creates SQS message"() { -+ // setup: -+ // String queueName = "s3ToSnsToSqsTestQueue" -+ // String bucketName = "otel-s3-sns-sqs-test-bucket" -+ // String topicName = "s3ToSnsToSqsTestTopic" -+ // -+ // String queueUrl = awsConnector.createQueue(queueName) -+ // String queueArn = awsConnector.getQueueArn(queueUrl) -+ // awsConnector.createBucket(bucketName) -+ // String topicArn = awsConnector.createTopicAndSubscribeQueue(topicName, queueArn) -+ // -+ // awsConnector.setQueuePublishingPolicy(queueUrl, queueArn) -+ // awsConnector.setTopicPublishingPolicy(topicArn) -+ // awsConnector.enableS3ToSnsNotifications(bucketName, topicArn) -+ // -+ // when: -+ // // test message, auto created by AWS -+ // awsConnector.receiveMessage(queueUrl) -+ // awsConnector.putSampleData(bucketName) -+ // // traced message -+ // def receiveMessageResult = awsConnector.receiveMessage(queueUrl) -+ // receiveMessageResult.messages.each {message -> -+ // runWithSpan("process child") {} -+ // } -+ // // cleanup -+ // awsConnector.deleteBucket(bucketName) -+ // awsConnector.purgeQueue(queueUrl) -+ // -+ // then: -+ // assertTraces(14) { -+ // trace(0, 1) { -+ // span(0) { -+ // name "SQS.CreateQueue" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "CreateQueue" -+ // "aws.queue.name" queueName -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(1, 1) { -+ // span(0) { -+ // name "SQS.GetQueueAttributes" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "GetQueueAttributes" -+ // "aws.queue.url" queueUrl -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(2, 1) { -+ // span(0) { -+ // name "S3.CreateBucket" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "CreateBucket" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "Amazon S3" -+ // "aws.bucket.name" bucketName -+ // "http.method" "PUT" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(3, 1) { -+ // span(0) { -+ // name "SNS.CreateTopic" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "CreateTopic" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSNS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(4, 1) { -+ // span(0) { -+ // name "SNS.Subscribe" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "Subscribe" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSNS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(5, 1) { -+ // span(0) { -+ // name "SQS.SetQueueAttributes" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "SetQueueAttributes" -+ // "aws.queue.url" queueUrl -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(6, 1) { -+ // span(0) { -+ // name "SNS.SetTopicAttributes" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "SetTopicAttributes" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSNS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(7, 1) { -+ // span(0) { -+ // name "S3.SetBucketNotificationConfiguration" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "SetBucketNotificationConfiguration" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "Amazon S3" -+ // "aws.bucket.name" bucketName -+ // "http.method" "PUT" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(8, 1) { -+ // span(0) { -+ // name "S3.PutObject" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "PutObject" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "Amazon S3" -+ // "aws.bucket.name" bucketName -+ // "http.method" "PUT" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(9, 2) { -+ // span(0) { -+ // name "s3ToSnsToSqsTestQueue process" -+ // kind CONSUMER -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "ReceiveMessage" -+ // "aws.queue.url" queueUrl -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "http.method" "POST" -+ // "http.url" String -+ // "net.peer.name" String -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.MESSAGING_SYSTEM" "AmazonSQS" -+ // "$SemanticAttributes.MESSAGING_DESTINATION_NAME" "s3ToSnsToSqsTestQueue" -+ // "$SemanticAttributes.MESSAGING_OPERATION" "process" -+ // "$SemanticAttributes.MESSAGING_MESSAGE_ID" String -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // span(1) { -+ // name "process child" -+ // childOf span(0) -+ // attributes { -+ // } -+ // } -+ // } -+ // trace(10, 1) { -+ // span(0) { -+ // name "S3.ListObjects" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "ListObjects" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "Amazon S3" -+ // "aws.bucket.name" bucketName -+ // "http.method" "GET" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(11, 1) { -+ // span(0) { -+ // name "S3.DeleteObject" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "DeleteObject" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "Amazon S3" -+ // "aws.bucket.name" bucketName -+ // "http.method" "DELETE" -+ // "http.status_code" 204 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(12, 1) { -+ // span(0) { -+ // name "S3.DeleteBucket" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "DeleteBucket" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "Amazon S3" -+ // "aws.bucket.name" bucketName -+ // "http.method" "DELETE" -+ // "http.status_code" 204 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // trace(13, 1) { -+ // span(0) { -+ // name "SQS.PurgeQueue" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "PurgeQueue" -+ // "aws.queue.url" queueUrl -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } -+ // } -+ // } -+ // } -+ // } -+ //} - } +@@ -444,6 +444,7 @@ class S3TracingTest extends AgentInstrumentationSpecification { + "net.peer.port" { it == null || Number } + "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } + "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } ++ "aws.sns.topic.arn" "$topicArn" + } + } + } +@@ -467,6 +468,7 @@ class S3TracingTest extends AgentInstrumentationSpecification { + "net.peer.port" { it == null || Number } + "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } + "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } ++ "aws.sns.topic.arn" "$topicArn" + } + } + } +@@ -514,6 +516,7 @@ class S3TracingTest extends AgentInstrumentationSpecification { + "net.peer.port" { it == null || Number } + "$SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH" { it == null || it instanceof Long } + "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" { it == null || it instanceof Long } ++ "aws.sns.topic.arn" "$topicArn" + } + } + } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/SnsTracingTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/SnsTracingTest.groovy -index 97749cf085..f7402c1e4b 100644 +index 97749cf085..a0b83ca870 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/SnsTracingTest.groovy +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/groovy/SnsTracingTest.groovy -@@ -20,192 +20,192 @@ class SnsTracingTest extends AgentInstrumentationSpecification { - awsConnector.disconnect() - } - -- def "SNS notification triggers SQS message consumed with AWS SDK"() { -- setup: -- String queueName = "snsToSqsTestQueue" -- String topicName = "snsToSqsTestTopic" -- -- String queueUrl = awsConnector.createQueue(queueName) -- String queueArn = awsConnector.getQueueArn(queueUrl) -- awsConnector.setQueuePublishingPolicy(queueUrl, queueArn) -- String topicArn = awsConnector.createTopicAndSubscribeQueue(topicName, queueArn) -- -- when: -- awsConnector.publishSampleNotification(topicArn) -- def receiveMessageResult = awsConnector.receiveMessage(queueUrl) -- receiveMessageResult.messages.each {message -> -- runWithSpan("process child") {} -- } -- -- then: -- assertTraces(6) { -- trace(0, 1) { -- -- span(0) { -- name "SQS.CreateQueue" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "CreateQueue" -- "aws.queue.name" queueName -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long -- } -- } -- } -- trace(1, 1) { -- -- span(0) { -- name "SQS.GetQueueAttributes" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "GetQueueAttributes" -- "aws.queue.url" queueUrl -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long -- } -- } -- } -- trace(2, 1) { -- -- span(0) { -- name "SQS.SetQueueAttributes" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "SetQueueAttributes" -- "aws.queue.url" queueUrl -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long -- } -- } -- } -- trace(3, 1) { -- -- span(0) { -- name "SNS.CreateTopic" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "CreateTopic" -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSNS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long -- } -- } -- } -- trace(4, 1) { -- -- span(0) { -- name "SNS.Subscribe" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "Subscribe" -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSNS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long -- } -- } -- } -- trace(5, 3) { -- span(0) { -- name "SNS.Publish" -- kind CLIENT -- hasNoParent() -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "rpc.method" "Publish" -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSNS" -- "http.method" "POST" -- "http.status_code" 200 -- "http.url" String -- "net.peer.name" String -- "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -- "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long -- } -- } -- span(1) { -- name "snsToSqsTestQueue process" -- kind CONSUMER -- childOf span(0) -- attributes { -- "aws.agent" "java-aws-sdk" -- "aws.endpoint" String -- "aws.queue.url" queueUrl -- "rpc.system" "aws-api" -- "rpc.service" "AmazonSQS" -- "rpc.method" "ReceiveMessage" -- "http.method" "POST" -- "http.url" String -- "net.peer.name" String -- "net.peer.port" { it == null || Number } -- "$SemanticAttributes.MESSAGING_SYSTEM" "AmazonSQS" -- "$SemanticAttributes.MESSAGING_DESTINATION_NAME" "snsToSqsTestQueue" -- "$SemanticAttributes.MESSAGING_OPERATION" "process" -- "$SemanticAttributes.MESSAGING_MESSAGE_ID" String -- } -- } -- span(2) { -- name "process child" -- childOf span(1) -- attributes { -- } -- } -- } -- } -- } -+ //def "SNS notification triggers SQS message consumed with AWS SDK"() { -+ // setup: -+ // String queueName = "snsToSqsTestQueue" -+ // String topicName = "snsToSqsTestTopic" -+ // -+ // String queueUrl = awsConnector.createQueue(queueName) -+ // String queueArn = awsConnector.getQueueArn(queueUrl) -+ // awsConnector.setQueuePublishingPolicy(queueUrl, queueArn) -+ // String topicArn = awsConnector.createTopicAndSubscribeQueue(topicName, queueArn) -+ // -+ // when: -+ // awsConnector.publishSampleNotification(topicArn) -+ // def receiveMessageResult = awsConnector.receiveMessage(queueUrl) -+ // receiveMessageResult.messages.each {message -> -+ // runWithSpan("process child") {} -+ // } -+ // -+ // then: -+ // assertTraces(6) { -+ // trace(0, 1) { -+ // -+ // span(0) { -+ // name "SQS.CreateQueue" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "CreateQueue" -+ // "aws.queue.name" queueName -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long -+ // } -+ // } -+ // } -+ // trace(1, 1) { -+ // -+ // span(0) { -+ // name "SQS.GetQueueAttributes" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "GetQueueAttributes" -+ // "aws.queue.url" queueUrl -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long -+ // } -+ // } -+ // } -+ // trace(2, 1) { -+ // -+ // span(0) { -+ // name "SQS.SetQueueAttributes" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "SetQueueAttributes" -+ // "aws.queue.url" queueUrl -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long -+ // } -+ // } -+ // } -+ // trace(3, 1) { -+ // -+ // span(0) { -+ // name "SNS.CreateTopic" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "CreateTopic" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSNS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long -+ // } -+ // } -+ // } -+ // trace(4, 1) { -+ // -+ // span(0) { -+ // name "SNS.Subscribe" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "Subscribe" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSNS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long -+ // } -+ // } -+ // } -+ // trace(5, 3) { -+ // span(0) { -+ // name "SNS.Publish" -+ // kind CLIENT -+ // hasNoParent() -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "rpc.method" "Publish" -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSNS" -+ // "http.method" "POST" -+ // "http.status_code" 200 -+ // "http.url" String -+ // "net.peer.name" String -+ // "$SemanticAttributes.NET_PROTOCOL_NAME" "http" -+ // "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long -+ // } -+ // } -+ // span(1) { -+ // name "snsToSqsTestQueue process" -+ // kind CONSUMER -+ // childOf span(0) -+ // attributes { -+ // "aws.agent" "java-aws-sdk" -+ // "aws.endpoint" String -+ // "aws.queue.url" queueUrl -+ // "rpc.system" "aws-api" -+ // "rpc.service" "AmazonSQS" -+ // "rpc.method" "ReceiveMessage" -+ // "http.method" "POST" -+ // "http.url" String -+ // "net.peer.name" String -+ // "net.peer.port" { it == null || Number } -+ // "$SemanticAttributes.MESSAGING_SYSTEM" "AmazonSQS" -+ // "$SemanticAttributes.MESSAGING_DESTINATION_NAME" "snsToSqsTestQueue" -+ // "$SemanticAttributes.MESSAGING_OPERATION" "process" -+ // "$SemanticAttributes.MESSAGING_MESSAGE_ID" String -+ // } -+ // } -+ // span(2) { -+ // name "process child" -+ // childOf span(1) -+ // attributes { -+ // } -+ // } -+ // } -+ // } -+ //} - } -diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test_before_1_11_106/groovy/Aws0ClientTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test_before_1_11_106/groovy/Aws0ClientTest.groovy -index 543b6e8e8e..e4703eac17 100644 ---- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test_before_1_11_106/groovy/Aws0ClientTest.groovy -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test_before_1_11_106/groovy/Aws0ClientTest.groovy -@@ -133,8 +133,8 @@ class Aws0ClientTest extends AgentInstrumentationSpecification { - - where: - service | operation | method | path | handlerCount | client | additionalAttributes | call | body -- "S3" | "CreateBucket" | "PUT" | "/testbucket/" | 1 | new AmazonS3Client().withEndpoint("${server.httpUri()}") | ["aws.bucket.name": "testbucket"] | { c -> c.setS3ClientOptions(S3ClientOptions.builder().setPathStyleAccess(true).build()); c.createBucket("testbucket") } | "" -- "S3" | "GetObject" | "GET" | "/someBucket/someKey" | 1 | new AmazonS3Client().withEndpoint("${server.httpUri()}") | ["aws.bucket.name": "someBucket"] | { c -> c.getObject("someBucket", "someKey") } | "" -+ //"S3" | "CreateBucket" | "PUT" | "/testbucket/" | 1 | new AmazonS3Client().withEndpoint("${server.httpUri()}") | ["aws.bucket.name": "testbucket"] | { c -> c.setS3ClientOptions(S3ClientOptions.builder().setPathStyleAccess(true).build()); c.createBucket("testbucket") } | "" -+ //"S3" | "GetObject" | "GET" | "/someBucket/someKey" | 1 | new AmazonS3Client().withEndpoint("${server.httpUri()}") | ["aws.bucket.name": "someBucket"] | { c -> c.getObject("someBucket", "someKey") } | "" - "EC2" | "AllocateAddress" | "POST" | "/" | 4 | new AmazonEC2Client().withEndpoint("${server.httpUri()}") | [:] | { c -> c.allocateAddress() } | """ - - 59dbff89-35bd-4eac-99ed-be587EXAMPLE +@@ -131,6 +131,7 @@ class SnsTracingTest extends AgentInstrumentationSpecification { + "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" + "net.peer.port" { it == null || Number } + "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long ++ "aws.sns.topic.arn" "$topicArn" + } + } + } +@@ -154,6 +155,7 @@ class SnsTracingTest extends AgentInstrumentationSpecification { + "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" + "net.peer.port" { it == null || Number } + "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long ++ "aws.sns.topic.arn" "$topicArn" + } + } + } +@@ -176,6 +178,7 @@ class SnsTracingTest extends AgentInstrumentationSpecification { + "$SemanticAttributes.NET_PROTOCOL_VERSION" "1.1" + "net.peer.port" { it == null || Number } + "$SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH" Long ++ "aws.sns.topic.arn" "$topicArn" + } + } + span(1) { diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/build.gradle.kts -index 6cf49a21c4..d2f9267072 100644 +index 6cf49a21c4..3705634153 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/build.gradle.kts @@ -18,6 +18,13 @@ dependencies { @@ -1784,7 +82,7 @@ index 6cf49a21c4..d2f9267072 100644 testLibrary("com.amazonaws:aws-java-sdk-sns:1.11.106") testLibrary("com.amazonaws:aws-java-sdk-sqs:1.11.106") + testLibrary("com.amazonaws:aws-java-sdk-secretsmanager:1.11.309") -+ // testLibrary("com.amazonaws:aws-java-sdk-stepfunctions:1.11.230") ++ testLibrary("com.amazonaws:aws-java-sdk-stepfunctions:1.11.230") + testLibrary("com.amazonaws:aws-java-sdk-lambda:1.11.678") + testLibrary("com.amazonaws:aws-java-sdk-bedrock:1.12.744") + testLibrary("com.amazonaws:aws-java-sdk-bedrockagent:1.12.744") @@ -1794,7 +92,7 @@ index 6cf49a21c4..d2f9267072 100644 // last version that does not use json protocol latestDepTestLibrary("com.amazonaws:aws-java-sdk-sqs:1.12.583") diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts -index bfe844e413..a2cedc9fa2 100644 +index bfe844e413..dec4935b55 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts @@ -17,6 +17,14 @@ dependencies { @@ -1803,7 +101,7 @@ index bfe844e413..a2cedc9fa2 100644 testLibrary("com.amazonaws:aws-java-sdk-sns:1.11.106") + testLibrary("com.amazonaws:aws-java-sdk-sqs:1.11.106") + testLibrary("com.amazonaws:aws-java-sdk-secretsmanager:1.11.309") -+ // testLibrary("com.amazonaws:aws-java-sdk-stepfunctions:1.11.230") ++ testLibrary("com.amazonaws:aws-java-sdk-stepfunctions:1.11.230") + testLibrary("com.amazonaws:aws-java-sdk-lambda:1.11.678") + testLibrary("com.amazonaws:aws-java-sdk-bedrock:1.12.744") + testLibrary("com.amazonaws:aws-java-sdk-bedrockagent:1.12.744") @@ -1952,10 +250,10 @@ index 0000000000..e890cb3c0f + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsExperimentalAttributes.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsExperimentalAttributes.java -index 3e8fddec5c..70e8eeae7f 100644 +index 3e8fddec5c..8f86a67b39 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsExperimentalAttributes.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsExperimentalAttributes.java -@@ -18,6 +18,32 @@ final class AwsExperimentalAttributes { +@@ -18,6 +18,49 @@ final class AwsExperimentalAttributes { static final AttributeKey AWS_STREAM_NAME = stringKey("aws.stream.name"); static final AttributeKey AWS_TABLE_NAME = stringKey("aws.table.name"); static final AttributeKey AWS_REQUEST_ID = stringKey("aws.requestId"); @@ -1971,6 +269,23 @@ index 3e8fddec5c..70e8eeae7f 100644 + stringKey("gen_ai.request.model"); + static final AttributeKey AWS_BEDROCK_SYSTEM = stringKey("gen_ai.system"); + ++ static final AttributeKey GEN_AI_REQUEST_MAX_TOKENS = ++ stringKey("gen_ai.request.max_tokens"); ++ ++ static final AttributeKey GEN_AI_REQUEST_TEMPERATURE = ++ stringKey("gen_ai.request.temperature"); ++ ++ static final AttributeKey GEN_AI_REQUEST_TOP_P = stringKey("gen_ai.request.top_p"); ++ ++ static final AttributeKey GEN_AI_RESPONSE_FINISH_REASONS = ++ stringKey("gen_ai.response.finish_reasons"); ++ ++ static final AttributeKey GEN_AI_USAGE_INPUT_TOKENS = ++ stringKey("gen_ai.usage.input_tokens"); ++ ++ static final AttributeKey GEN_AI_USAGE_OUTPUT_TOKENS = ++ stringKey("gen_ai.usage.output_tokens"); ++ + static final AttributeKey AWS_STATE_MACHINE_ARN = + stringKey("aws.stepfunctions.state_machine.arn"); + @@ -1989,10 +304,10 @@ index 3e8fddec5c..70e8eeae7f 100644 private AwsExperimentalAttributes() {} } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkExperimentalAttributesExtractor.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkExperimentalAttributesExtractor.java -index 245f09a5d8..157fd891c3 100644 +index 245f09a5d8..aef7936980 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkExperimentalAttributesExtractor.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkExperimentalAttributesExtractor.java -@@ -6,11 +6,23 @@ +@@ -6,13 +6,31 @@ package io.opentelemetry.instrumentation.awssdk.v1_11; import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_AGENT; @@ -2015,8 +330,16 @@ index 245f09a5d8..157fd891c3 100644 +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_STEP_FUNCTIONS_ACTIVITY_ARN; import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_STREAM_NAME; import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_TABLE_NAME; ++import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.GEN_AI_REQUEST_MAX_TOKENS; ++import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.GEN_AI_REQUEST_TEMPERATURE; ++import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.GEN_AI_REQUEST_TOP_P; ++import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.GEN_AI_RESPONSE_FINISH_REASONS; ++import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.GEN_AI_USAGE_INPUT_TOKENS; ++import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.GEN_AI_USAGE_OUTPUT_TOKENS; -@@ -21,12 +33,17 @@ import io.opentelemetry.api.common.AttributeKey; + import com.amazonaws.AmazonWebServiceResponse; + import com.amazonaws.Request; +@@ -21,12 +39,17 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; @@ -2034,7 +357,7 @@ index 245f09a5d8..157fd891c3 100644 @Override public void onStart(AttributesBuilder attributes, Context parentContext, Request request) { -@@ -34,21 +51,30 @@ class AwsSdkExperimentalAttributesExtractor +@@ -34,21 +57,30 @@ class AwsSdkExperimentalAttributesExtractor attributes.put(AWS_ENDPOINT, request.getEndpoint().toString()); Object originalRequest = request.getOriginalRequest(); @@ -2079,7 +402,7 @@ index 245f09a5d8..157fd891c3 100644 } } -@@ -59,12 +85,117 @@ class AwsSdkExperimentalAttributesExtractor +@@ -59,12 +91,136 @@ class AwsSdkExperimentalAttributesExtractor Request request, @Nullable Response response, @Nullable Throwable error) { @@ -2104,15 +427,15 @@ index 245f09a5d8..157fd891c3 100644 + if (requestId != null) { + attributes.put(AWS_REQUEST_ID, requestId); + } -+ } + } + // Get serviceName defined in the AWS Java SDK V1 Request class. + String serviceName = request.getServiceName(); + // Extract response attributes for Bedrock services + if (awsResp != null && isBedrockService(serviceName)) { + bedrockOnEnd(attributes, awsResp, serviceName); - } - } - } ++ } ++ } ++ } + + private static void bedrockOnStart( + AttributesBuilder attributes, @@ -2147,6 +470,14 @@ index 245f09a5d8..157fd891c3 100644 + Function getter = RequestAccess::getModelId; + String modelId = getter.apply(originalRequest); + attributes.put(AWS_BEDROCK_RUNTIME_MODEL_ID, modelId); ++ ++ setAttribute( ++ attributes, GEN_AI_REQUEST_MAX_TOKENS, originalRequest, RequestAccess::getMaxTokens); ++ setAttribute( ++ attributes, GEN_AI_REQUEST_TEMPERATURE, originalRequest, RequestAccess::getTemperature); ++ setAttribute(attributes, GEN_AI_REQUEST_TOP_P, originalRequest, RequestAccess::getTopP); ++ setAttribute( ++ attributes, GEN_AI_USAGE_INPUT_TOKENS, originalRequest, RequestAccess::getInputTokens); + break; + default: + break; @@ -2176,6 +507,17 @@ index 245f09a5d8..157fd891c3 100644 + setAttribute(attributes, AWS_AGENT_ID, awsResp, RequestAccess::getAgentId); + setAttribute(attributes, AWS_KNOWLEDGE_BASE_ID, awsResp, RequestAccess::getKnowledgeBaseId); + break; ++ case BEDROCK_RUNTIME_SERVICE: ++ if (!Objects.equals(awsResp.getClass().getSimpleName(), "InvokeModelResult")) { ++ break; ++ } ++ ++ setAttribute(attributes, GEN_AI_USAGE_INPUT_TOKENS, awsResp, RequestAccess::getInputTokens); ++ setAttribute( ++ attributes, GEN_AI_USAGE_OUTPUT_TOKENS, awsResp, RequestAccess::getOutputTokens); ++ setAttribute( ++ attributes, GEN_AI_RESPONSE_FINISH_REASONS, awsResp, RequestAccess::getFinishReasons); ++ break; + default: + break; + } @@ -2199,25 +541,242 @@ index 245f09a5d8..157fd891c3 100644 + String value = getter.apply(request); + if (value != null) { + attributes.put(key, value); -+ } -+ } + } + } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java -index bb2ae9266c..36e216047f 100644 +index bb2ae9266c..512d5345cc 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java -@@ -8,6 +8,7 @@ package io.opentelemetry.instrumentation.awssdk.v1_11; +@@ -5,9 +5,17 @@ + + package io.opentelemetry.instrumentation.awssdk.v1_11; + ++import com.fasterxml.jackson.databind.JsonNode; ++import com.fasterxml.jackson.databind.ObjectMapper; ++import java.io.IOException; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.lang.reflect.Method; ++import java.nio.ByteBuffer; ++import java.util.Arrays; ++import java.util.Objects; ++import java.util.stream.Stream; import javax.annotation.Nullable; final class RequestAccess { -@@ -20,36 +21,158 @@ final class RequestAccess { +@@ -20,36 +28,365 @@ final class RequestAccess { } }; ++ private static final ObjectMapper objectMapper = new ObjectMapper(); ++ ++ @Nullable ++ private static JsonNode parseTargetBody(ByteBuffer buffer) { ++ try { ++ byte[] bytes; ++ // Create duplicate to avoid mutating the original buffer position ++ ByteBuffer duplicate = buffer.duplicate(); ++ if (buffer.hasArray()) { ++ bytes = ++ Arrays.copyOfRange( ++ duplicate.array(), ++ duplicate.arrayOffset(), ++ duplicate.arrayOffset() + duplicate.remaining()); ++ } else { ++ bytes = new byte[buffer.remaining()]; ++ buffer.get(bytes); ++ } ++ return objectMapper.readTree(bytes); ++ } catch (IOException e) { ++ return null; ++ } ++ } ++ ++ @Nullable ++ private static JsonNode getJsonBody(Object target) { ++ if (target == null) { ++ return null; ++ } ++ ++ RequestAccess access = REQUEST_ACCESSORS.get(target.getClass()); ++ ByteBuffer bodyBuffer = invokeOrNullGeneric(access.getBody, target, ByteBuffer.class); ++ if (bodyBuffer == null) { ++ return null; ++ } ++ ++ return parseTargetBody(bodyBuffer); ++ } ++ ++ @Nullable ++ private static String findFirstMatchingPath(JsonNode jsonBody, String... paths) { ++ if (jsonBody == null) { ++ return null; ++ } ++ ++ return Stream.of(paths) ++ .map( ++ path -> { ++ JsonNode node = jsonBody.at(path); ++ if (node != null && !node.isMissingNode()) { ++ return node.asText(); ++ } ++ return null; ++ }) ++ .filter(Objects::nonNull) ++ .findFirst() ++ .orElse(null); ++ } ++ ++ @Nullable ++ private static String approximateTokenCount(JsonNode jsonBody, String... textPaths) { ++ if (jsonBody == null) { ++ return null; ++ } ++ ++ return Stream.of(textPaths) ++ .map( ++ path -> { ++ JsonNode node = jsonBody.at(path); ++ if (node != null && !node.isMissingNode()) { ++ int tokenEstimate = (int) Math.ceil(node.asText().length() / 6.0); ++ return Integer.toString(tokenEstimate); ++ } ++ return null; ++ }) ++ .filter(Objects::nonNull) ++ .findFirst() ++ .orElse(null); ++ } ++ ++ // Model -> Path Mapping: ++ // Amazon Titan -> "/textGenerationConfig/maxTokenCount" ++ // Anthropic Claude -> "/max_tokens" ++ // Cohere Command -> "/max_tokens" ++ // Cohere Command R -> "/max_tokens" ++ // AI21 Jamba -> "/max_tokens" ++ // Meta Llama -> "/max_gen_len" ++ // Mistral AI -> "/max_tokens" ++ @Nullable ++ static String getMaxTokens(Object target) { ++ return findFirstMatchingPath( ++ getJsonBody(target), "/textGenerationConfig/maxTokenCount", "/max_tokens", "/max_gen_len"); ++ } ++ ++ // Model -> Path Mapping: ++ // Amazon Titan -> "/textGenerationConfig/temperature" ++ // Anthropic Claude -> "/temperature" ++ // Cohere Command -> "/temperature" ++ // Cohere Command R -> "/temperature" ++ // AI21 Jamba -> "/temperature" ++ // Meta Llama -> "/temperature" ++ // Mistral AI -> "/temperature" ++ @Nullable ++ static String getTemperature(Object target) { ++ return findFirstMatchingPath( ++ getJsonBody(target), "/textGenerationConfig/temperature", "/temperature"); ++ } ++ ++ // Model -> Path Mapping: ++ // Amazon Titan -> "/textGenerationConfig/topP" ++ // Anthropic Claude -> "/top_p" ++ // Cohere Command -> "/p" ++ // Cohere Command R -> "/p" ++ // AI21 Jamba -> "/top_p" ++ // Meta Llama -> "/top_p" ++ // Mistral AI -> "/top_p" ++ @Nullable ++ static String getTopP(Object target) { ++ return findFirstMatchingPath(getJsonBody(target), "/textGenerationConfig/topP", "/top_p", "/p"); ++ } ++ ++ // Model -> Path Mapping: ++ // Amazon Titan -> "/inputTextTokenCount" ++ // Anthropic Claude -> "/usage/input_tokens" ++ // Cohere Command -> "/prompt" ++ // Cohere Command R -> "/message" ++ // AI21 Jamba -> "/usage/prompt_tokens" ++ // Meta Llama -> "/prompt_token_count" ++ // Mistral AI -> "/prompt" ++ @Nullable ++ static String getInputTokens(Object target) { ++ JsonNode jsonBody = getJsonBody(target); ++ if (jsonBody == null) { ++ return null; ++ } ++ ++ // Try direct tokens counts first ++ String directCount = ++ findFirstMatchingPath( ++ jsonBody, ++ "/inputTextTokenCount", ++ "/usage/input_tokens", ++ "/usage/prompt_tokens", ++ "/prompt_token_count"); ++ ++ if (directCount != null) { ++ return directCount; ++ } ++ ++ // Fall back to token approximation ++ return approximateTokenCount(jsonBody, "/prompt", "/message"); ++ } ++ ++ // Model -> Path Mapping: ++ // Amazon Titan -> "/results/0/tokenCount" ++ // Anthropic Claude -> "/usage/output_tokens" ++ // Cohere Command -> "/generations/0/text" ++ // Cohere Command R -> "/text" ++ // AI21 Jamba -> "/usage/completion_tokens" ++ // Meta Llama -> "/generation_token_count" ++ // Mistral AI -> "/outputs/0/text" ++ @Nullable ++ static String getOutputTokens(Object target) { ++ JsonNode jsonBody = getJsonBody(target); ++ if (jsonBody == null) { ++ return null; ++ } ++ ++ // Try direct token counts first ++ String directCount = ++ findFirstMatchingPath( ++ jsonBody, ++ "/results/0/tokenCount", ++ "/usage/output_tokens", ++ "/usage/completion_tokens", ++ "/generation_token_count"); ++ ++ if (directCount != null) { ++ return directCount; ++ } ++ ++ return approximateTokenCount(jsonBody, "/outputs/0/text", "/text"); ++ } ++ ++ // Model -> Path Mapping: ++ // Amazon Titan -> "/results/0/completionReason" ++ // Anthropic Claude -> "/stop_reason" ++ // Cohere Command -> "/generations/0/finish_reason" ++ // Cohere Command R -> "/finish_reason" ++ // AI21 Jamba -> "/choices/0/finish_reason" ++ // Meta Llama -> "/stop_reason" ++ // Mistral AI -> "/outputs/0/stop_reason" ++ @Nullable ++ static String getFinishReasons(Object target) { ++ String finishReason = ++ findFirstMatchingPath( ++ getJsonBody(target), ++ "/results/0/completionReason", ++ "/stop_reason", ++ "/generations/0/finish_reason", ++ "/choices/0/finish_reason", ++ "/outputs/0/stop_reason", ++ "/finish_reason"); ++ ++ return finishReason != null ? "[" + finishReason + "]" : null; ++ } ++ + @Nullable + static String getLambdaName(Object request) { + if (request == null) { @@ -2373,7 +932,25 @@ index bb2ae9266c..36e216047f 100644 @Nullable private static String invokeOrNull(@Nullable MethodHandle method, Object obj) { if (method == null) { -@@ -67,6 +190,17 @@ final class RequestAccess { +@@ -62,27 +399,82 @@ final class RequestAccess { + } + } + ++ @Nullable ++ private static T invokeOrNullGeneric( ++ @Nullable MethodHandle method, Object obj, Class returnType) { ++ if (method == null) { ++ return null; ++ } ++ try { ++ return returnType.cast(method.invoke(obj)); ++ } catch (Throwable e) { ++ return null; ++ } ++ } ++ + @Nullable private final MethodHandle getBucketName; + @Nullable private final MethodHandle getQueueUrl; @Nullable private final MethodHandle getQueueName; @Nullable private final MethodHandle getStreamName; @Nullable private final MethodHandle getTableName; @@ -2382,6 +959,7 @@ index bb2ae9266c..36e216047f 100644 + @Nullable private final MethodHandle getDataSourceId; + @Nullable private final MethodHandle getGuardrailId; + @Nullable private final MethodHandle getModelId; ++ @Nullable private final MethodHandle getBody; + @Nullable private final MethodHandle getStateMachineArn; + @Nullable private final MethodHandle getStepFunctionsActivityArn; + @Nullable private final MethodHandle getSnsTopicArn; @@ -2390,26 +968,39 @@ index bb2ae9266c..36e216047f 100644 + @Nullable private final MethodHandle getLambdaResourceId; private RequestAccess(Class clz) { - getBucketName = findAccessorOrNull(clz, "getBucketName"); -@@ -74,6 +208,17 @@ final class RequestAccess { - getQueueName = findAccessorOrNull(clz, "getQueueName"); - getStreamName = findAccessorOrNull(clz, "getStreamName"); - getTableName = findAccessorOrNull(clz, "getTableName"); -+ getAgentId = findAccessorOrNull(clz, "getAgentId"); -+ getKnowledgeBaseId = findAccessorOrNull(clz, "getKnowledgeBaseId"); -+ getDataSourceId = findAccessorOrNull(clz, "getDataSourceId"); -+ getGuardrailId = findAccessorOrNull(clz, "getGuardrailId"); -+ getModelId = findAccessorOrNull(clz, "getModelId"); -+ getStateMachineArn = findAccessorOrNull(clz, "getStateMachineArn"); -+ getStepFunctionsActivityArn = findAccessorOrNull(clz, "getActivityArn"); -+ getSnsTopicArn = findAccessorOrNull(clz, "getTopicArn"); -+ getSecretArn = findAccessorOrNull(clz, "getARN"); -+ getLambdaName = findAccessorOrNull(clz, "getFunctionName"); -+ getLambdaResourceId = findAccessorOrNull(clz, "getUUID"); +- getBucketName = findAccessorOrNull(clz, "getBucketName"); +- getQueueUrl = findAccessorOrNull(clz, "getQueueUrl"); +- getQueueName = findAccessorOrNull(clz, "getQueueName"); +- getStreamName = findAccessorOrNull(clz, "getStreamName"); +- getTableName = findAccessorOrNull(clz, "getTableName"); ++ getBucketName = findAccessorOrNull(clz, "getBucketName", String.class); ++ getQueueUrl = findAccessorOrNull(clz, "getQueueUrl", String.class); ++ getQueueName = findAccessorOrNull(clz, "getQueueName", String.class); ++ getStreamName = findAccessorOrNull(clz, "getStreamName", String.class); ++ getTableName = findAccessorOrNull(clz, "getTableName", String.class); ++ getAgentId = findAccessorOrNull(clz, "getAgentId", String.class); ++ getKnowledgeBaseId = findAccessorOrNull(clz, "getKnowledgeBaseId", String.class); ++ getDataSourceId = findAccessorOrNull(clz, "getDataSourceId", String.class); ++ getGuardrailId = findAccessorOrNull(clz, "getGuardrailId", String.class); ++ getModelId = findAccessorOrNull(clz, "getModelId", String.class); ++ getBody = findAccessorOrNull(clz, "getBody", ByteBuffer.class); ++ getStateMachineArn = findAccessorOrNull(clz, "getStateMachineArn", String.class); ++ getStepFunctionsActivityArn = findAccessorOrNull(clz, "getActivityArn", String.class); ++ getSnsTopicArn = findAccessorOrNull(clz, "getTopicArn", String.class); ++ getSecretArn = findAccessorOrNull(clz, "getARN", String.class); ++ getLambdaName = findAccessorOrNull(clz, "getFunctionName", String.class); ++ getLambdaResourceId = findAccessorOrNull(clz, "getUUID", String.class); } @Nullable -@@ -85,4 +230,21 @@ final class RequestAccess { +- private static MethodHandle findAccessorOrNull(Class clz, String methodName) { ++ private static MethodHandle findAccessorOrNull( ++ Class clz, String methodName, Class returnType) { + try { + return MethodHandles.publicLookup() +- .findVirtual(clz, methodName, MethodType.methodType(String.class)); ++ .findVirtual(clz, methodName, MethodType.methodType(returnType)); + } catch (Throwable t) { return null; } } @@ -2432,7 +1023,7 @@ index bb2ae9266c..36e216047f 100644 + } } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/testing/build.gradle.kts -index 548631e9f1..b31b01b87b 100644 +index 548631e9f1..51483839a7 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/build.gradle.kts @@ -14,6 +14,14 @@ dependencies { @@ -2440,7 +1031,7 @@ index 548631e9f1..b31b01b87b 100644 compileOnly("com.amazonaws:aws-java-sdk-sns:1.11.106") compileOnly("com.amazonaws:aws-java-sdk-sqs:1.11.106") + compileOnly("com.amazonaws:aws-java-sdk-secretsmanager:1.11.309") -+ // compileOnly("com.amazonaws:aws-java-sdk-stepfunctions:1.11.230") ++ compileOnly("com.amazonaws:aws-java-sdk-stepfunctions:1.11.230") + compileOnly("com.amazonaws:aws-java-sdk-lambda:1.11.678") + + compileOnly("com.amazonaws:aws-java-sdk-bedrock:1.12.744") @@ -2451,7 +1042,7 @@ index 548631e9f1..b31b01b87b 100644 // needed for SQS - using emq directly as localstack references emq v0.15.7 ie WITHOUT AWS trace header propagation implementation("org.elasticmq:elasticmq-rest-sqs_2.12:1.0.0") diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractAws1ClientTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractAws1ClientTest.groovy -index 95e6ed8985..25ff9f5a70 100644 +index 95e6ed8985..990fc177bc 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractAws1ClientTest.groovy +++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractAws1ClientTest.groovy @@ -27,6 +27,24 @@ import com.amazonaws.services.rds.AmazonRDSClientBuilder @@ -2466,9 +1057,9 @@ index 95e6ed8985..25ff9f5a70 100644 +import com.amazonaws.services.bedrock.model.GetGuardrailRequest +import com.amazonaws.services.bedrockruntime.AmazonBedrockRuntimeClientBuilder +import com.amazonaws.services.bedrockruntime.model.InvokeModelRequest -+//import com.amazonaws.services.stepfunctions.AWSStepFunctionsClientBuilder -+//import com.amazonaws.services.stepfunctions.model.DescribeStateMachineRequest -+//import com.amazonaws.services.stepfunctions.model.DescribeActivityRequest ++import com.amazonaws.services.stepfunctions.AWSStepFunctionsClientBuilder ++import com.amazonaws.services.stepfunctions.model.DescribeStateMachineRequest ++import com.amazonaws.services.stepfunctions.model.DescribeActivityRequest +import com.amazonaws.services.sns.AmazonSNSClientBuilder +import com.amazonaws.services.sns.model.PublishRequest +import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder @@ -2487,18 +1078,7 @@ index 95e6ed8985..25ff9f5a70 100644 import static io.opentelemetry.api.trace.SpanKind.CLIENT import static io.opentelemetry.api.trace.SpanKind.PRODUCER -@@ -130,8 +149,8 @@ abstract class AbstractAws1ClientTest extends InstrumentationSpecification { - - where: - service | operation | method | path | clientBuilder | call | additionalAttributes | body -- "S3" | "CreateBucket" | "PUT" | "/testbucket/" | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true) | { c -> c.createBucket("testbucket") } | ["aws.bucket.name": "testbucket"] | "" -- "S3" | "GetObject" | "GET" | "/someBucket/someKey" | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true) | { c -> c.getObject("someBucket", "someKey") } | ["aws.bucket.name": "someBucket"] | "" -+ //"S3" | "CreateBucket" | "PUT" | "/testbucket/" | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true) | { c -> c.createBucket("testbucket") } | ["aws.bucket.name": "testbucket"] | "" -+ //"S3" | "GetObject" | "GET" | "/someBucket/someKey" | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true) | { c -> c.getObject("someBucket", "someKey") } | ["aws.bucket.name": "someBucket"] | "" - "DynamoDBv2" | "CreateTable" | "POST" | "/" | AmazonDynamoDBClientBuilder.standard() | { c -> c.createTable(new CreateTableRequest("sometable", null)) } | ["aws.table.name": "sometable"] | "" - "Kinesis" | "DeleteStream" | "POST" | "/" | AmazonKinesisClientBuilder.standard() | { c -> c.deleteStream(new DeleteStreamRequest().withStreamName("somestream")) } | ["aws.stream.name": "somestream"] | "" - // Some users may implicitly subclass the request object to mimic a fluent style -@@ -156,6 +175,88 @@ abstract class AbstractAws1ClientTest extends InstrumentationSpecification { +@@ -156,6 +175,296 @@ abstract class AbstractAws1ClientTest extends InstrumentationSpecification { """ @@ -2534,25 +1114,233 @@ index 95e6ed8985..25ff9f5a70 100644 + "AWSBedrockAgent" | "GetAgent" | "GET" | "/" | AWSBedrockAgentClientBuilder.standard() | { c -> c.getAgent(new GetAgentRequest().withAgentId("agentId")) } | ["aws.bedrock.agent.id": "agentId"] | "" + "AWSBedrockAgent" | "GetKnowledgeBase" | "GET" | "/" | AWSBedrockAgentClientBuilder.standard() | { c -> c.getKnowledgeBase(new GetKnowledgeBaseRequest().withKnowledgeBaseId("knowledgeBaseId")) } | ["aws.bedrock.knowledge_base.id": "knowledgeBaseId"] | "" + "AWSBedrockAgent" | "GetDataSource" | "GET" | "/" | AWSBedrockAgentClientBuilder.standard() | { c -> c.getDataSource(new GetDataSourceRequest().withDataSourceId("datasourceId").withKnowledgeBaseId("knowledgeBaseId")) } | ["aws.bedrock.data_source.id": "datasourceId"] | "" -+ "BedrockRuntime" | "InvokeModel" | "POST" | "/" | AmazonBedrockRuntimeClientBuilder.standard() | -+ { c -> c.invokeModel( -+ new InvokeModelRequest().withModelId("anthropic.claude-v2").withBody(StandardCharsets.UTF_8.encode( -+ "{\"prompt\":\"Hello, world!\",\"temperature\":0.7,\"top_p\":0.9,\"max_tokens_to_sample\":100}\n" -+ ))) } | ["gen_ai.request.model": "anthropic.claude-v2", "gen_ai.system": "aws_bedrock"] | """ ++ "BedrockRuntime" | "InvokeModel" | "POST" | "/" | ++ AmazonBedrockRuntimeClientBuilder.standard() | ++ { c -> ++ c.invokeModel( ++ new InvokeModelRequest() ++ .withModelId("ai21.jamba-1-5-mini-v1:0") ++ .withBody(StandardCharsets.UTF_8.encode(''' ++ { ++ "messages": [{ ++ "role": "user", ++ "message": "Which LLM are you?" ++ }], ++ "max_tokens": 1000, ++ "top_p": 0.8, ++ "temperature": 0.7 ++ } ++ ''')) ++ ) ++ } | ++ [ ++ "gen_ai.request.model": "ai21.jamba-1-5-mini-v1:0", ++ "gen_ai.system": "aws_bedrock", ++ "gen_ai.request.max_tokens": "1000", ++ "gen_ai.request.temperature": "0.7", ++ "gen_ai.request.top_p": "0.8", ++ "gen_ai.response.finish_reasons": "[stop]", ++ "gen_ai.usage.input_tokens": "5", ++ "gen_ai.usage.output_tokens": "42" ++ ] | ++ ''' ++ { ++ "choices": [{ ++ "finish_reason": "stop" ++ }], ++ "usage": { ++ "prompt_tokens": 5, ++ "completion_tokens": 42 ++ } ++ } ++ ''' ++ "BedrockRuntime" | "InvokeModel" | "POST" | "/" | ++ AmazonBedrockRuntimeClientBuilder.standard() | ++ { c -> ++ c.invokeModel( ++ new InvokeModelRequest() ++ .withModelId("amazon.titan-text-premier-v1:0") ++ .withBody(StandardCharsets.UTF_8.encode(''' ++ { ++ "inputText": "Hello, world!", ++ "textGenerationConfig": { ++ "temperature": 0.7, ++ "topP": 0.9, ++ "maxTokenCount": 100, ++ "stopSequences": ["END"] ++ } ++ } ++ ''')) ++ ) ++ } | ++ [ ++ "gen_ai.request.model": "amazon.titan-text-premier-v1:0", ++ "gen_ai.system": "aws_bedrock", ++ "gen_ai.request.max_tokens": "100", ++ "gen_ai.request.temperature": "0.7", ++ "gen_ai.request.top_p": "0.9", ++ "gen_ai.response.finish_reasons": "[stop]", ++ "gen_ai.usage.input_tokens": "5", ++ "gen_ai.usage.output_tokens": "42" ++ ] | ++ ''' ++ { ++ "inputTextTokenCount": 5, ++ "results": [ + { -+ "completion": " Here is a simple explanation of black ", -+ "stop_reason": "length", -+ "stop": "holes" ++ "tokenCount": 42, ++ "outputText": "Hi! I'm Titan, an AI assistant. How can I help you today?", ++ "completionReason": "stop" + } -+ """ -+ //"AWSStepFunctions" | "DescribeStateMachine" | "POST" | "/" | AWSStepFunctionsClientBuilder.standard() -+ //| { c -> c.describeStateMachine(new DescribeStateMachineRequest().withStateMachineArn("stateMachineArn")) } -+ //| ["aws.stepfunctions.state_machine.arn": "stateMachineArn"] -+ //| "" -+ //"AWSStepFunctions" | "DescribeActivity" | "POST" | "/" | AWSStepFunctionsClientBuilder.standard() -+ //| { c -> c.describeActivity(new DescribeActivityRequest().withActivityArn("activityArn")) } -+ //| ["aws.stepfunctions.activity.arn": "activityArn"] -+ //| "" ++ ] ++ } ++ ''' ++ "BedrockRuntime" | "InvokeModel" | "POST" | "/" | ++ AmazonBedrockRuntimeClientBuilder.standard() | ++ { c -> ++ c.invokeModel( ++ new InvokeModelRequest() ++ .withModelId("anthropic.claude-3-5-sonnet-20241022-v2:0") ++ .withBody(StandardCharsets.UTF_8.encode(''' ++ { ++ "anthropic_version": "bedrock-2023-05-31", ++ "messages": [{ ++ "role": "user", ++ "content": "Hello, world" ++ }], ++ "max_tokens": 100, ++ "temperature": 0.7, ++ "top_p": 0.9 ++ } ++ ''')) ++ ) ++ } | ++ [ ++ "gen_ai.request.model": "anthropic.claude-3-5-sonnet-20241022-v2:0", ++ "gen_ai.system": "aws_bedrock", ++ "gen_ai.request.max_tokens": "100", ++ "gen_ai.request.temperature": "0.7", ++ "gen_ai.request.top_p": "0.9", ++ "gen_ai.response.finish_reasons": "[end_turn]", ++ "gen_ai.usage.input_tokens": "2095", ++ "gen_ai.usage.output_tokens": "503" ++ ] | ++ ''' ++ { ++ "stop_reason": "end_turn", ++ "usage": { ++ "input_tokens": 2095, ++ "output_tokens": 503 ++ } ++ } ++ ''' ++ "BedrockRuntime" | "InvokeModel" | "POST" | "/" | ++ AmazonBedrockRuntimeClientBuilder.standard() | ++ { c -> ++ c.invokeModel( ++ new InvokeModelRequest() ++ .withModelId("meta.llama3-70b-instruct-v1:0") ++ .withBody(StandardCharsets.UTF_8.encode(''' ++ { ++ "prompt": "<|begin_of_text|><|start_header_id|>user<|end_header_id|>\\\\nDescribe the purpose of a 'hello world' program in one line. <|eot_id|>\\\\n<|start_header_id|>assistant<|end_header_id|>\\\\n", ++ "max_gen_len": 128, ++ "temperature": 0.1, ++ "top_p": 0.9 ++ } ++ ''')) ++ ) ++ } | ++ [ ++ "gen_ai.request.model": "meta.llama3-70b-instruct-v1:0", ++ "gen_ai.system": "aws_bedrock", ++ "gen_ai.request.max_tokens": "128", ++ "gen_ai.request.temperature": "0.1", ++ "gen_ai.request.top_p": "0.9", ++ "gen_ai.response.finish_reasons": "[stop]", ++ "gen_ai.usage.input_tokens": "2095", ++ "gen_ai.usage.output_tokens": "503" ++ ] | ++ ''' ++ { ++ "prompt_token_count": 2095, ++ "generation_token_count": 503, ++ "stop_reason": "stop" ++ } ++ ''' ++ "BedrockRuntime" | "InvokeModel" | "POST" | "/" | ++ AmazonBedrockRuntimeClientBuilder.standard() | ++ { c -> ++ c.invokeModel( ++ new InvokeModelRequest() ++ .withModelId("cohere.command-r-v1:0") ++ .withBody(StandardCharsets.UTF_8.encode(''' ++ { ++ "message": "Convince me to write a LISP interpreter in one line.", ++ "temperature": 0.8, ++ "max_tokens": 4096, ++ "p": 0.45 ++ } ++ ''')) ++ ) ++ } | ++ [ ++ "gen_ai.request.model": "cohere.command-r-v1:0", ++ "gen_ai.system": "aws_bedrock", ++ "gen_ai.request.max_tokens": "4096", ++ "gen_ai.request.temperature": "0.8", ++ "gen_ai.request.top_p": "0.45", ++ "gen_ai.response.finish_reasons": "[COMPLETE]", ++ "gen_ai.usage.input_tokens": "9", ++ "gen_ai.usage.output_tokens": "2" ++ ] | ++ ''' ++ { ++ "text": "test-output", ++ "finish_reason": "COMPLETE" ++ } ++ ''' ++ "BedrockRuntime" | "InvokeModel" | "POST" | "/" | ++ AmazonBedrockRuntimeClientBuilder.standard() | ++ { c -> ++ c.invokeModel( ++ new InvokeModelRequest() ++ .withModelId("mistral.mistral-large-2402-v1:0") ++ .withBody(StandardCharsets.UTF_8.encode(''' ++ { ++ "prompt": "[INST] Describe the difference between a compiler and interpreter in one line. [/INST]\\\\n", ++ "max_tokens": 4096, ++ "temperature": 0.75, ++ "top_p": 0.25 ++ } ++ ''')) ++ ) ++ } | ++ [ ++ "gen_ai.request.model": "mistral.mistral-large-2402-v1:0", ++ "gen_ai.system": "aws_bedrock", ++ "gen_ai.request.max_tokens": "4096", ++ "gen_ai.request.temperature": "0.75", ++ "gen_ai.request.top_p": "0.25", ++ "gen_ai.response.finish_reasons": "[stop]", ++ "gen_ai.usage.input_tokens": "16", ++ "gen_ai.usage.output_tokens": "2" ++ ] | ++ ''' ++ { ++ "outputs": [{ ++ "text": "test-output", ++ "stop_reason": "stop" ++ }] ++ } ++ ''' ++ "AWSStepFunctions" | "DescribeStateMachine" | "POST" | "/" | AWSStepFunctionsClientBuilder.standard() ++ | { c -> c.describeStateMachine(new DescribeStateMachineRequest().withStateMachineArn("stateMachineArn")) } ++ | ["aws.stepfunctions.state_machine.arn": "stateMachineArn"] ++ | "" ++ "AWSStepFunctions" | "DescribeActivity" | "POST" | "/" | AWSStepFunctionsClientBuilder.standard() ++ | { c -> c.describeActivity(new DescribeActivityRequest().withActivityArn("activityArn")) } ++ | ["aws.stepfunctions.activity.arn": "activityArn"] ++ | "" + "SNS" | "Publish" | "POST" | "/" | AmazonSNSClientBuilder.standard() + | { c -> c.publish(new PublishRequest().withMessage("message").withTopicArn("topicArn")) } + | ["aws.sns.topic.arn": "topicArn"] @@ -2631,10 +1419,10 @@ index 081d542e76..4f71a06a57 100644 latestDepTestLibrary("software.amazon.awssdk:sqs:2.21.17") diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsExperimentalAttributes.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsExperimentalAttributes.java new file mode 100644 -index 0000000000..e1cb180d75 +index 0000000000..9e9f9cf59f --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsExperimentalAttributes.java -@@ -0,0 +1,47 @@ +@@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 @@ -2663,6 +1451,23 @@ index 0000000000..e1cb180d75 + static final AttributeKey GEN_AI_MODEL = stringKey("gen_ai.request.model"); + static final AttributeKey GEN_AI_SYSTEM = stringKey("gen_ai.system"); + ++ static final AttributeKey GEN_AI_REQUEST_MAX_TOKENS = ++ stringKey("gen_ai.request.max_tokens"); ++ ++ static final AttributeKey GEN_AI_REQUEST_TEMPERATURE = ++ stringKey("gen_ai.request.temperature"); ++ ++ static final AttributeKey GEN_AI_REQUEST_TOP_P = stringKey("gen_ai.request.top_p"); ++ ++ static final AttributeKey GEN_AI_RESPONSE_FINISH_REASONS = ++ stringKey("gen_ai.response.finish_reasons"); ++ ++ static final AttributeKey GEN_AI_USAGE_INPUT_TOKENS = ++ stringKey("gen_ai.usage.input_tokens"); ++ ++ static final AttributeKey GEN_AI_USAGE_OUTPUT_TOKENS = ++ stringKey("gen_ai.usage.output_tokens"); ++ + static final AttributeKey AWS_STATE_MACHINE_ARN = + stringKey("aws.stepfunctions.state_machine.arn"); + @@ -2680,6 +1485,15 @@ index 0000000000..e1cb180d75 + static final AttributeKey AWS_LAMBDA_RESOURCE_ID = + stringKey("aws.lambda.resource_mapping.id"); + ++ static boolean isGenAiAttribute(String attributeKey) { ++ return attributeKey.equals(GEN_AI_REQUEST_MAX_TOKENS.getKey()) ++ || attributeKey.equals(GEN_AI_REQUEST_TEMPERATURE.getKey()) ++ || attributeKey.equals(GEN_AI_REQUEST_TOP_P.getKey()) ++ || attributeKey.equals(GEN_AI_RESPONSE_FINISH_REASONS.getKey()) ++ || attributeKey.equals(GEN_AI_USAGE_INPUT_TOKENS.getKey()) ++ || attributeKey.equals(GEN_AI_USAGE_OUTPUT_TOKENS.getKey()); ++ } ++ + private AwsExperimentalAttributes() {} +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequest.java @@ -2758,10 +1572,10 @@ index 54253d0f7b..5326400f7e 100644 BatchGetItem( DYNAMODB, diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequestType.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequestType.java -index 9062f2aa17..9511cd6f05 100644 +index 9062f2aa17..4cd468e095 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequestType.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkRequestType.java -@@ -5,17 +5,62 @@ +@@ -5,17 +5,76 @@ package io.opentelemetry.instrumentation.awssdk.v2_2; @@ -2783,6 +1597,12 @@ index 9062f2aa17..9511cd6f05 100644 +import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsExperimentalAttributes.AWS_STREAM_NAME; +import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsExperimentalAttributes.AWS_TABLE_NAME; +import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsExperimentalAttributes.GEN_AI_MODEL; ++import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsExperimentalAttributes.GEN_AI_REQUEST_MAX_TOKENS; ++import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsExperimentalAttributes.GEN_AI_REQUEST_TEMPERATURE; ++import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsExperimentalAttributes.GEN_AI_REQUEST_TOP_P; ++import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsExperimentalAttributes.GEN_AI_RESPONSE_FINISH_REASONS; ++import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsExperimentalAttributes.GEN_AI_USAGE_INPUT_TOKENS; ++import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsExperimentalAttributes.GEN_AI_USAGE_OUTPUT_TOKENS; import static io.opentelemetry.instrumentation.awssdk.v2_2.FieldMapping.request; +import static io.opentelemetry.instrumentation.awssdk.v2_2.FieldMapping.response; @@ -2815,7 +1635,15 @@ index 9062f2aa17..9511cd6f05 100644 + BEDROCKKNOWLEDGEBASEOPERATION( + request(AWS_KNOWLEDGE_BASE_ID.getKey(), "knowledgeBaseId"), + response(AWS_KNOWLEDGE_BASE_ID.getKey(), "knowledgeBaseId")), -+ BEDROCKRUNTIME(request(GEN_AI_MODEL.getKey(), "modelId")), ++ BEDROCKRUNTIME( ++ request(GEN_AI_MODEL.getKey(), "modelId"), ++ request(GEN_AI_REQUEST_MAX_TOKENS.getKey(), "body"), ++ request(GEN_AI_REQUEST_TEMPERATURE.getKey(), "body"), ++ request(GEN_AI_REQUEST_TOP_P.getKey(), "body"), ++ request(GEN_AI_USAGE_INPUT_TOKENS.getKey(), "body"), ++ response(GEN_AI_RESPONSE_FINISH_REASONS.getKey(), "body"), ++ response(GEN_AI_USAGE_INPUT_TOKENS.getKey(), "body"), ++ response(GEN_AI_USAGE_OUTPUT_TOKENS.getKey(), "body")), + STEPFUNCTION( + request(AWS_STATE_MACHINE_ARN.getKey(), "stateMachineArn"), + request(AWS_STEP_FUNCTIONS_ACTIVITY_ARN.getKey(), "activityArn")), @@ -2828,6 +1656,263 @@ index 9062f2aa17..9511cd6f05 100644 // Wrapping in unmodifiableMap @SuppressWarnings("ImmutableEnumChecker") +diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapper.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapper.java +index 569d0eb5ae..8f2d463237 100644 +--- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapper.java ++++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FieldMapper.java +@@ -65,8 +65,13 @@ class FieldMapper { + for (int i = 1; i < path.size() && target != null; i++) { + target = next(target, path.get(i)); + } ++ String value; + if (target != null) { +- String value = serializer.serialize(target); ++ if (AwsExperimentalAttributes.isGenAiAttribute(fieldMapping.getAttribute())) { ++ value = serializer.serialize(fieldMapping.getAttribute(), target); ++ } else { ++ value = serializer.serialize(target); ++ } + if (!StringUtils.isEmpty(value)) { + span.setAttribute(fieldMapping.getAttribute(), value); + } +diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/Serializer.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/Serializer.java +index 979ecb08e8..fb846cae6a 100644 +--- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/Serializer.java ++++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/Serializer.java +@@ -5,13 +5,19 @@ + + package io.opentelemetry.instrumentation.awssdk.v2_2; + ++import com.fasterxml.jackson.core.JsonProcessingException; ++import com.fasterxml.jackson.databind.JsonNode; ++import com.fasterxml.jackson.databind.ObjectMapper; + import java.io.IOException; + import java.io.InputStream; + import java.util.Collection; + import java.util.Map; ++import java.util.Objects; + import java.util.Optional; + import java.util.stream.Collectors; ++import java.util.stream.Stream; + import javax.annotation.Nullable; ++import software.amazon.awssdk.core.SdkBytes; + import software.amazon.awssdk.core.SdkPojo; + import software.amazon.awssdk.http.ContentStreamProvider; + import software.amazon.awssdk.http.SdkHttpFullRequest; +@@ -21,6 +27,8 @@ import software.amazon.awssdk.utils.StringUtils; + + class Serializer { + ++ private static final ObjectMapper objectMapper = new ObjectMapper(); ++ + @Nullable + String serialize(Object target) { + +@@ -41,6 +49,41 @@ class Serializer { + return target.toString(); + } + ++ @Nullable ++ String serialize(String attributeName, Object target) { ++ try { ++ JsonNode jsonBody; ++ if (target instanceof SdkBytes) { ++ String jsonString = ((SdkBytes) target).asUtf8String(); ++ jsonBody = objectMapper.readTree(jsonString); ++ } else { ++ if (target != null) { ++ return target.toString(); ++ } ++ return null; ++ } ++ ++ switch (attributeName) { ++ case "gen_ai.request.max_tokens": ++ return getMaxTokens(jsonBody); ++ case "gen_ai.request.temperature": ++ return getTemperature(jsonBody); ++ case "gen_ai.request.top_p": ++ return getTopP(jsonBody); ++ case "gen_ai.response.finish_reasons": ++ return getFinishReasons(jsonBody); ++ case "gen_ai.usage.input_tokens": ++ return getInputTokens(jsonBody); ++ case "gen_ai.usage.output_tokens": ++ return getOutputTokens(jsonBody); ++ default: ++ return null; ++ } ++ } catch (JsonProcessingException e) { ++ return null; ++ } ++ } ++ + @Nullable + private static String serialize(SdkPojo sdkPojo) { + ProtocolMarshaller marshaller = +@@ -65,4 +108,162 @@ class Serializer { + String serialized = collection.stream().map(this::serialize).collect(Collectors.joining(",")); + return (StringUtils.isEmpty(serialized) ? null : "[" + serialized + "]"); + } ++ ++ @Nullable ++ private static String findFirstMatchingPath(JsonNode jsonBody, String... paths) { ++ if (jsonBody == null) { ++ return null; ++ } ++ ++ return Stream.of(paths) ++ .map( ++ path -> { ++ JsonNode node = jsonBody.at(path); ++ if (node != null && !node.isMissingNode()) { ++ return node.asText(); ++ } ++ return null; ++ }) ++ .filter(Objects::nonNull) ++ .findFirst() ++ .orElse(null); ++ } ++ ++ @Nullable ++ private static String approximateTokenCount(JsonNode jsonBody, String... textPaths) { ++ if (jsonBody == null) { ++ return null; ++ } ++ ++ return Stream.of(textPaths) ++ .map( ++ path -> { ++ JsonNode node = jsonBody.at(path); ++ if (node != null && !node.isMissingNode()) { ++ int tokenEstimate = (int) Math.ceil(node.asText().length() / 6.0); ++ return Integer.toString(tokenEstimate); ++ } ++ return null; ++ }) ++ .filter(Objects::nonNull) ++ .findFirst() ++ .orElse(null); ++ } ++ ++ // Model -> Path Mapping: ++ // Amazon Titan -> "/textGenerationConfig/maxTokenCount" ++ // Anthropic Claude -> "/max_tokens" ++ // Cohere Command -> "/max_tokens" ++ // Cohere Command R -> "/max_tokens" ++ // AI21 Jamba -> "/max_tokens" ++ // Meta Llama -> "/max_gen_len" ++ // Mistral AI -> "/max_tokens" ++ @Nullable ++ private static String getMaxTokens(JsonNode jsonBody) { ++ return findFirstMatchingPath( ++ jsonBody, "/textGenerationConfig/maxTokenCount", "/max_tokens", "/max_gen_len"); ++ } ++ ++ // Model -> Path Mapping: ++ // Amazon Titan -> "/textGenerationConfig/temperature" ++ // Anthropic Claude -> "/temperature" ++ // Cohere Command -> "/temperature" ++ // Cohere Command R -> "/temperature" ++ // AI21 Jamba -> "/temperature" ++ // Meta Llama -> "/temperature" ++ // Mistral AI -> "/temperature" ++ @Nullable ++ private static String getTemperature(JsonNode jsonBody) { ++ return findFirstMatchingPath(jsonBody, "/textGenerationConfig/temperature", "/temperature"); ++ } ++ ++ // Model -> Path Mapping: ++ // Amazon Titan -> "/textGenerationConfig/topP" ++ // Anthropic Claude -> "/top_p" ++ // Cohere Command -> "/p" ++ // Cohere Command R -> "/p" ++ // AI21 Jamba -> "/top_p" ++ // Meta Llama -> "/top_p" ++ // Mistral AI -> "/top_p" ++ @Nullable ++ private static String getTopP(JsonNode jsonBody) { ++ return findFirstMatchingPath(jsonBody, "/textGenerationConfig/topP", "/top_p", "/p"); ++ } ++ ++ // Model -> Path Mapping: ++ // Amazon Titan -> "/results/0/completionReason" ++ // Anthropic Claude -> "/stop_reason" ++ // Cohere Command -> "/generations/0/finish_reason" ++ // Cohere Command R -> "/finish_reason" ++ // AI21 Jamba -> "/choices/0/finish_reason" ++ // Meta Llama -> "/stop_reason" ++ // Mistral AI -> "/outputs/0/stop_reason" ++ @Nullable ++ private static String getFinishReasons(JsonNode jsonBody) { ++ String finishReason = ++ findFirstMatchingPath( ++ jsonBody, ++ "/results/0/completionReason", ++ "/stop_reason", ++ "/generations/0/finish_reason", ++ "/choices/0/finish_reason", ++ "/outputs/0/stop_reason", ++ "/finish_reason"); ++ ++ return finishReason != null ? "[" + finishReason + "]" : null; ++ } ++ ++ // Model -> Path Mapping: ++ // Amazon Titan -> "/inputTextTokenCount" ++ // Anthropic Claude -> "/usage/input_tokens" ++ // Cohere Command -> "/prompt" ++ // Cohere Command R -> "/message" ++ // AI21 Jamba -> "/usage/prompt_tokens" ++ // Meta Llama -> "/prompt_token_count" ++ // Mistral AI -> "/prompt" ++ @Nullable ++ private static String getInputTokens(JsonNode jsonBody) { ++ // Try direct tokens counts first ++ String directCount = ++ findFirstMatchingPath( ++ jsonBody, ++ "/inputTextTokenCount", ++ "/usage/input_tokens", ++ "/usage/prompt_tokens", ++ "/prompt_token_count"); ++ ++ if (directCount != null) { ++ return directCount; ++ } ++ ++ // Fall back to token approximation ++ return approximateTokenCount(jsonBody, "/prompt", "/message"); ++ } ++ ++ // Model -> Path Mapping: ++ // Amazon Titan -> "/results/0/tokenCount" ++ // Anthropic Claude -> "/usage/output_tokens" ++ // Cohere Command -> "/generations/0/text" ++ // Cohere Command R -> "/text" ++ // AI21 Jamba -> "/usage/completion_tokens" ++ // Meta Llama -> "/generation_token_count" ++ // Mistral AI -> "/outputs/0/text" ++ @Nullable ++ private static String getOutputTokens(JsonNode jsonBody) { ++ // Try direct token counts first ++ String directCount = ++ findFirstMatchingPath( ++ jsonBody, ++ "/results/0/tokenCount", ++ "/usage/output_tokens", ++ "/usage/completion_tokens", ++ "/generation_token_count"); ++ ++ if (directCount != null) { ++ return directCount; ++ } ++ ++ // Fall back to token approximation ++ return approximateTokenCount(jsonBody, "/outputs/0/text", "/text"); ++ } + } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java index f717b1efc4..352b02093e 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java @@ -2972,204 +2057,6 @@ index 53390c8d85..692cd005eb 100644 } def "send #operation async request with builder #builder.class.getName() mocked response"() { -diff --git a/instrumentation/vaadin-14.2/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/AbstractVaadinTest.java b/instrumentation/vaadin-14.2/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/AbstractVaadinTest.java -index 161a574119..08070aa332 100644 ---- a/instrumentation/vaadin-14.2/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/AbstractVaadinTest.java -+++ b/instrumentation/vaadin-14.2/testing/src/main/java/io/opentelemetry/javaagent/instrumentation/vaadin/AbstractVaadinTest.java -@@ -5,17 +5,17 @@ - - package io.opentelemetry.javaagent.instrumentation.vaadin; - --import static org.assertj.core.api.Assertions.assertThat; --import static org.awaitility.Awaitility.await; -+// import static org.assertj.core.api.Assertions.assertThat; -+// import static org.awaitility.Awaitility.await; - - import com.vaadin.flow.server.Version; - import com.vaadin.flow.spring.annotation.EnableVaadin; --import io.opentelemetry.api.trace.SpanKind; -+// import io.opentelemetry.api.trace.SpanKind; - import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; - import io.opentelemetry.instrumentation.testing.junit.http.AbstractHttpServerUsingTest; - import io.opentelemetry.instrumentation.testing.junit.http.HttpServerInstrumentationExtension; --import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; --import io.opentelemetry.sdk.trace.data.SpanData; -+// import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; -+// import io.opentelemetry.sdk.trace.data.SpanData; - import java.io.File; - import java.io.IOException; - import java.io.InputStream; -@@ -23,17 +23,17 @@ import java.net.URI; - import java.net.URISyntaxException; - import java.nio.file.Files; - import java.nio.file.Path; --import java.time.Duration; -+// import java.time.Duration; - import java.util.HashMap; --import java.util.List; -+// import java.util.List; - import java.util.Map; - import org.junit.jupiter.api.AfterAll; - import org.junit.jupiter.api.BeforeAll; --import org.junit.jupiter.api.Test; -+// import org.junit.jupiter.api.Test; - import org.junit.jupiter.api.extension.RegisterExtension; --import org.openqa.selenium.By; -+// import org.openqa.selenium.By; - import org.openqa.selenium.chrome.ChromeOptions; --import org.openqa.selenium.remote.RemoteWebDriver; -+// import org.openqa.selenium.remote.RemoteWebDriver; - import org.slf4j.Logger; - import org.slf4j.LoggerFactory; - import org.springframework.boot.SpringApplication; -@@ -126,77 +126,77 @@ public abstract class AbstractVaadinTest - return "/xyz"; - } - -- private void waitForStart(RemoteWebDriver driver) { -- // In development mode ui javascript is compiled when application starts -- // this involves downloading and installing npm and a bunch of packages -- // and running webpack. Wait until all of this is done before starting test. -- driver.manage().timeouts().implicitlyWait(Duration.ofMinutes(3)); -- driver.get(address.resolve("main").toString()); -- // wait for page to load -- driver.findElement(By.id("main.label")); -- // clear traces so test would start from clean state -- testing.clearData(); -- -- driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30)); -- } -- -- private RemoteWebDriver getWebDriver() { -- return new RemoteWebDriver(browser.getSeleniumAddress(), new ChromeOptions(), false); -- } -+ // private void waitForStart(RemoteWebDriver driver) { -+ // // In development mode ui javascript is compiled when application starts -+ // // this involves downloading and installing npm and a bunch of packages -+ // // and running webpack. Wait until all of this is done before starting test. -+ // driver.manage().timeouts().implicitlyWait(Duration.ofMinutes(3)); -+ // driver.get(address.resolve("main").toString()); -+ // // wait for page to load -+ // driver.findElement(By.id("main.label")); -+ // // clear traces so test would start from clean state -+ // testing.clearData(); -+ // -+ // driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30)); -+ // } -+ -+ // private RemoteWebDriver getWebDriver() { -+ // return new RemoteWebDriver(browser.getSeleniumAddress(), new ChromeOptions(), false); -+ // } - - abstract void assertFirstRequest(); - -- private void assertButtonClick() { -- await() -- .untilAsserted( -- () -> { -- List> traces = testing.waitForTraces(1); -- assertThat(traces.get(0)) -- .satisfies( -- spans -> { -- OpenTelemetryAssertions.assertThat(spans.get(0)) -- .hasName("POST " + getContextPath() + "/main") -- .hasNoParent() -- .hasKind(SpanKind.SERVER); -- OpenTelemetryAssertions.assertThat(spans.get(1)) -- .hasName("SpringVaadinServletService.handleRequest") -- .hasParent(spans.get(0)) -- .hasKind(SpanKind.INTERNAL); -- // we don't assert all the handler spans as these vary between -- // vaadin versions -- OpenTelemetryAssertions.assertThat(spans.get(spans.size() - 2)) -- .hasName("UidlRequestHandler.handleRequest") -- .hasParent(spans.get(1)) -- .hasKind(SpanKind.INTERNAL); -- OpenTelemetryAssertions.assertThat(spans.get(spans.size() - 1)) -- .hasName("EventRpcHandler.handle/click") -- .hasParent(spans.get(spans.size() - 2)) -- .hasKind(SpanKind.INTERNAL); -- }); -- }); -- } -- -- @Test -- public void navigateFromMainToOtherView() { -- RemoteWebDriver driver = getWebDriver(); -- waitForStart(driver); -- -- // fetch the test page -- driver.get(address.resolve("main").toString()); -- -- // wait for page to load -- assertThat(driver.findElement(By.id("main.label")).getText()).isEqualTo("Main view"); -- assertFirstRequest(); -- -- testing.clearData(); -- -- // click a button to trigger calling java code in MainView -- driver.findElement(By.id("main.button")).click(); -- -- // wait for page to load -- assertThat(driver.findElement(By.id("other.label")).getText()).isEqualTo("Other view"); -- assertButtonClick(); -- -- driver.close(); -- } -+ // private void assertButtonClick() { -+ // await() -+ // .untilAsserted( -+ // () -> { -+ // List> traces = testing.waitForTraces(1); -+ // assertThat(traces.get(0)) -+ // .satisfies( -+ // spans -> { -+ // OpenTelemetryAssertions.assertThat(spans.get(0)) -+ // .hasName("POST " + getContextPath() + "/main") -+ // .hasNoParent() -+ // .hasKind(SpanKind.SERVER); -+ // OpenTelemetryAssertions.assertThat(spans.get(1)) -+ // .hasName("SpringVaadinServletService.handleRequest") -+ // .hasParent(spans.get(0)) -+ // .hasKind(SpanKind.INTERNAL); -+ // // we don't assert all the handler spans as these vary between -+ // // vaadin versions -+ // OpenTelemetryAssertions.assertThat(spans.get(spans.size() - 2)) -+ // .hasName("UidlRequestHandler.handleRequest") -+ // .hasParent(spans.get(1)) -+ // .hasKind(SpanKind.INTERNAL); -+ // OpenTelemetryAssertions.assertThat(spans.get(spans.size() - 1)) -+ // .hasName("EventRpcHandler.handle/click") -+ // .hasParent(spans.get(spans.size() - 2)) -+ // .hasKind(SpanKind.INTERNAL); -+ // }); -+ // }); -+ // } -+ -+ // @Test -+ // public void navigateFromMainToOtherView() { -+ // RemoteWebDriver driver = getWebDriver(); -+ // waitForStart(driver); -+ // -+ // // fetch the test page -+ // driver.get(address.resolve("main").toString()); -+ // -+ // // wait for page to load -+ // assertThat(driver.findElement(By.id("main.label")).getText()).isEqualTo("Main view"); -+ // assertFirstRequest(); -+ // -+ // testing.clearData(); -+ // -+ // // click a button to trigger calling java code in MainView -+ // driver.findElement(By.id("main.button")).click(); -+ // -+ // // wait for page to load -+ // assertThat(driver.findElement(By.id("other.label")).getText()).isEqualTo("Other view"); -+ // assertButtonClick(); -+ // -+ // driver.close(); -+ // } - } diff --git a/version.gradle.kts b/version.gradle.kts index fdf57bdbea..c38a2e00f3 100644 --- a/version.gradle.kts diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java index 99a1732834..0f8652fb70 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java @@ -1798,8 +1798,8 @@ protected void doTestBedrockAgentDataSourceId() { 0.0); } - protected void doTestBedrockRuntimeModelId() { - var response = appClient.get("/bedrockruntime/invokeModel").aggregate().join(); + protected void doTestBedrockRuntimeAi21Jamba() { + var response = appClient.get("/bedrockruntime/invokeModel/ai21Jamba").aggregate().join(); var traces = mockCollectorClient.getTraces(); var metrics = mockCollectorClient.getMetrics( @@ -1809,9 +1809,9 @@ protected void doTestBedrockRuntimeModelId() { AppSignalsConstants.LATENCY_METRIC)); var localService = getApplicationOtelServiceName(); - var localOperation = "GET /bedrockruntime/invokeModel"; + var localOperation = "GET /bedrockruntime/invokeModel/ai21Jamba"; String type = "AWS::Bedrock::Model"; - String identifier = "anthropic.claude-v2"; + String identifier = "ai21.jamba-1-5-mini-v1:0"; assertSpanClientAttributes( traces, bedrockRuntimeSpanName("InvokeModel"), @@ -1828,7 +1828,371 @@ protected void doTestBedrockRuntimeModelId() { 200, List.of( assertAttribute( - SemanticConventionsConstants.GEN_AI_REQUEST_MODEL, "anthropic.claude-v2"))); + SemanticConventionsConstants.GEN_AI_REQUEST_MODEL, "ai21.jamba-1-5-mini-v1:0"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_TEMPERATURE, "0.7"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_TOP_P, "0.8"), + assertAttribute(SemanticConventionsConstants.GEN_AI_RESPONSE_FINISH_REASONS, "[stop]"), + assertAttribute(SemanticConventionsConstants.GEN_AI_USAGE_INPUT_TOKENS, "5"), + assertAttribute(SemanticConventionsConstants.GEN_AI_USAGE_OUTPUT_TOKENS, "42"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 0.0); + } + + protected void doTestBedrockRuntimeAmazonTitan() { + var response = appClient.get("/bedrockruntime/invokeModel/amazonTitan").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /bedrockruntime/invokeModel/amazonTitan"; + String type = "AWS::Bedrock::Model"; + String identifier = "amazon.titan-text-premier-v1:0"; + assertSpanClientAttributes( + traces, + bedrockRuntimeSpanName("InvokeModel"), + getBedrockRuntimeRpcServiceName(), + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + "bedrock.test", + 8080, + "http://bedrock.test:8080", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.GEN_AI_REQUEST_MODEL, + "amazon.titan-text-premier-v1:0"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_MAX_TOKENS, "100"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_TEMPERATURE, "0.7"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_TOP_P, "0.9"), + assertAttribute( + SemanticConventionsConstants.GEN_AI_RESPONSE_FINISH_REASONS, "[FINISHED]"), + assertAttribute(SemanticConventionsConstants.GEN_AI_USAGE_INPUT_TOKENS, "10"), + assertAttribute(SemanticConventionsConstants.GEN_AI_USAGE_OUTPUT_TOKENS, "15"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 0.0); + } + + protected void doTestBedrockRuntimeAnthropicClaude() { + var response = appClient.get("/bedrockruntime/invokeModel/anthropicClaude").aggregate().join(); + + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /bedrockruntime/invokeModel/anthropicClaude"; + String type = "AWS::Bedrock::Model"; + String identifier = "anthropic.claude-3-haiku-20240307-v1:0"; + + assertSpanClientAttributes( + traces, + bedrockRuntimeSpanName("InvokeModel"), + getBedrockRuntimeRpcServiceName(), + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + "bedrock.test", + 8080, + "http://bedrock.test:8080", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.GEN_AI_REQUEST_MODEL, + "anthropic.claude-3-haiku-20240307-v1:0"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_MAX_TOKENS, "512"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_TEMPERATURE, "0.6"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_TOP_P, "0.53"), + assertAttribute( + SemanticConventionsConstants.GEN_AI_RESPONSE_FINISH_REASONS, "[end_turn]"), + assertAttribute(SemanticConventionsConstants.GEN_AI_USAGE_INPUT_TOKENS, "2095"), + assertAttribute(SemanticConventionsConstants.GEN_AI_USAGE_OUTPUT_TOKENS, "503"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 0.0); + } + + protected void doTestBedrockRuntimeCohereCommandR() { + var response = appClient.get("/bedrockruntime/invokeModel/cohereCommandR").aggregate().join(); + + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /bedrockruntime/invokeModel/cohereCommandR"; + String type = "AWS::Bedrock::Model"; + String identifier = "cohere.command-r-v1:0"; + + assertSpanClientAttributes( + traces, + bedrockRuntimeSpanName("InvokeModel"), + getBedrockRuntimeRpcServiceName(), + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + "bedrock.test", + 8080, + "http://bedrock.test:8080", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.GEN_AI_REQUEST_MODEL, "cohere.command-r-v1:0"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_MAX_TOKENS, "4096"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_TEMPERATURE, "0.8"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_TOP_P, "0.45"), + assertAttribute( + SemanticConventionsConstants.GEN_AI_RESPONSE_FINISH_REASONS, "[COMPLETE]"), + assertAttribute(SemanticConventionsConstants.GEN_AI_USAGE_INPUT_TOKENS, "9"), + assertAttribute(SemanticConventionsConstants.GEN_AI_USAGE_OUTPUT_TOKENS, "16"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 0.0); + } + + protected void doTestBedrockRuntimeMetaLlama() { + var response = appClient.get("/bedrockruntime/invokeModel/metaLlama").aggregate().join(); + + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /bedrockruntime/invokeModel/metaLlama"; + String type = "AWS::Bedrock::Model"; + String identifier = "meta.llama3-70b-instruct-v1:0"; + + assertSpanClientAttributes( + traces, + bedrockRuntimeSpanName("InvokeModel"), + getBedrockRuntimeRpcServiceName(), + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + "bedrock.test", + 8080, + "http://bedrock.test:8080", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.GEN_AI_REQUEST_MODEL, "meta.llama3-70b-instruct-v1:0"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_MAX_TOKENS, "128"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_TEMPERATURE, "0.1"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_TOP_P, "0.9"), + assertAttribute(SemanticConventionsConstants.GEN_AI_RESPONSE_FINISH_REASONS, "[stop]"), + assertAttribute(SemanticConventionsConstants.GEN_AI_USAGE_INPUT_TOKENS, "2095"), + assertAttribute(SemanticConventionsConstants.GEN_AI_USAGE_OUTPUT_TOKENS, "503"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + 0.0); + } + + protected void doTestBedrockRuntimeMistral() { + var response = appClient.get("/bedrockruntime/invokeModel/mistralAi").aggregate().join(); + + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /bedrockruntime/invokeModel/mistralAi"; + String type = "AWS::Bedrock::Model"; + String identifier = "mistral.mistral-large-2402-v1:0"; + + assertSpanClientAttributes( + traces, + bedrockRuntimeSpanName("InvokeModel"), + getBedrockRuntimeRpcServiceName(), + localService, + localOperation, + getBedrockRuntimeServiceName(), + "InvokeModel", + type, + identifier, + "bedrock.test", + 8080, + "http://bedrock.test:8080", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.GEN_AI_REQUEST_MODEL, + "mistral.mistral-large-2402-v1:0"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_MAX_TOKENS, "4096"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_TEMPERATURE, "0.75"), + assertAttribute(SemanticConventionsConstants.GEN_AI_REQUEST_TOP_P, "0.25"), + assertAttribute(SemanticConventionsConstants.GEN_AI_RESPONSE_FINISH_REASONS, "[stop]"), + assertAttribute(SemanticConventionsConstants.GEN_AI_USAGE_INPUT_TOKENS, "15"), + assertAttribute(SemanticConventionsConstants.GEN_AI_USAGE_OUTPUT_TOKENS, "24"))); assertMetricClientAttributes( metrics, AppSignalsConstants.LATENCY_METRIC, diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java index 3cf5a8982e..fa5a586c5d 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java @@ -217,8 +217,33 @@ void testBedrockAgentDataSourceId() { } @Test - void testBedrockRuntimeModelId() { - doTestBedrockRuntimeModelId(); + void testBedrockRuntimeAmazonTitan() { + doTestBedrockRuntimeAmazonTitan(); + } + + @Test + void testBedrockRuntimeAi21Jamba() { + doTestBedrockRuntimeAi21Jamba(); + } + + @Test + void testBedrockRuntimeAnthropicClaude() { + doTestBedrockRuntimeAnthropicClaude(); + } + + @Test + void testBedrockRuntimeCohereCommandR() { + doTestBedrockRuntimeCohereCommandR(); + } + + @Test + void testBedrockRuntimeMetaLlama() { + doTestBedrockRuntimeMetaLlama(); + } + + @Test + void testBedrockRuntimeMistral() { + doTestBedrockRuntimeMistral(); } @Test diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java index a76736daf3..46c6b7e425 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java @@ -220,8 +220,33 @@ void testBedrockAgentDataSourceId() { } @Test - void testBedrockRuntimeModelId() { - doTestBedrockRuntimeModelId(); + void testBedrockRuntimeAmazonTitan() { + doTestBedrockRuntimeAmazonTitan(); + } + + @Test + void testBedrockRuntimeAi21Jamba() { + doTestBedrockRuntimeAi21Jamba(); + } + + @Test + void testBedrockRuntimeAnthropicClaude() { + doTestBedrockRuntimeAnthropicClaude(); + } + + @Test + void testBedrockRuntimeCohereCommandR() { + doTestBedrockRuntimeCohereCommandR(); + } + + @Test + void testBedrockRuntimeMetaLlama() { + doTestBedrockRuntimeMetaLlama(); + } + + @Test + void testBedrockRuntimeMistral() { + doTestBedrockRuntimeMistral(); } @Test @@ -236,8 +261,8 @@ void testBedrockAgentRuntimeAgentId() { // TODO: Enable testBedrockAgentRuntimeKnowledgeBaseId test after KnowledgeBaseId is supported in // OTEL BedrockAgentRuntime instrumentation - // @Test - // void testBedrockAgentRuntimeKnowledgeBaseId() { - // doTestBedrockAgentRuntimeKnowledgeBaseId(); - // } + @Test + void testBedrockAgentRuntimeKnowledgeBaseId() { + doTestBedrockAgentRuntimeKnowledgeBaseId(); + } } diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java index 3fe1644dd5..f0cac6da46 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java @@ -63,6 +63,12 @@ public class SemanticConventionsConstants { public static final String AWS_AGENT_ID = "aws.bedrock.agent.id"; public static final String AWS_GUARDRAIL_ID = "aws.bedrock.guardrail.id"; public static final String GEN_AI_REQUEST_MODEL = "gen_ai.request.model"; + public static final String GEN_AI_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens"; + public static final String GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature"; + public static final String GEN_AI_REQUEST_TOP_P = "gen_ai.request.top_p"; + public static final String GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons"; + public static final String GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens"; + public static final String GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens"; // kafka public static final String MESSAGING_CLIENT_ID = "messaging.client_id"; diff --git a/appsignals-tests/images/aws-sdk/aws-sdk-base/src/main/java/com/amazon/sampleapp/Utils.java b/appsignals-tests/images/aws-sdk/aws-sdk-base/src/main/java/com/amazon/sampleapp/Utils.java index f8f5313b26..c9264ed1c9 100644 --- a/appsignals-tests/images/aws-sdk/aws-sdk-base/src/main/java/com/amazon/sampleapp/Utils.java +++ b/appsignals-tests/images/aws-sdk/aws-sdk-base/src/main/java/com/amazon/sampleapp/Utils.java @@ -188,13 +188,69 @@ public static void setupInvokeModelRoute(int status) { post( "/model/:modelId/invoke", (req, res) -> { - ObjectNode jsonResponse = objectMapper.createObjectNode(); - jsonResponse.put("completion", "A simple completion token."); - jsonResponse.put("stop_reason", "stop_sequence"); - jsonResponse.put("stop", "stop_token"); - + String modelId = req.params(":modelId"); + ObjectMapper mapper = new ObjectMapper(); + ObjectNode jsonResponse = mapper.createObjectNode(); res.status(status); res.type("application/json"); + + if (modelId.contains("amazon.titan")) { + jsonResponse.put("inputTextTokenCount", 10); + + ArrayNode results = mapper.createArrayNode(); + ObjectNode result = mapper.createObjectNode(); + result.put("tokenCount", 15); + result.put("completionReason", "FINISHED"); + results.add(result); + + jsonResponse.set("results", results); + } else if (modelId.contains("ai21.jamba")) { + ArrayNode choices = mapper.createArrayNode(); + ObjectNode choice = mapper.createObjectNode(); + choice.put("finish_reason", "stop"); + + ObjectNode message = mapper.createObjectNode(); + message.put("content", "I am the AI21 Jamba language model"); + message.put("role", "assistant"); + choice.set("message", message); + + choices.add(choice); + jsonResponse.set("choices", choices); + + ObjectNode usage = mapper.createObjectNode(); + usage.put("prompt_tokens", 5); + usage.put("completion_tokens", 42); + jsonResponse.set("usage", usage); + } else if (modelId.contains("anthropic.claude")) { + jsonResponse.put("stop_reason", "end_turn"); + + ObjectNode usage = mapper.createObjectNode(); + usage.put("input_tokens", 2095); + usage.put("output_tokens", 503); + jsonResponse.set("usage", usage); + } else if (modelId.contains("cohere.command")) { + jsonResponse.put( + "text", + "LISP's elegant simplicity and powerful macro system make it perfect for building interpreters!"); + jsonResponse.put("finish_reason", "COMPLETE"); + } else if (modelId.contains("meta.llama")) { + jsonResponse.put("prompt_token_count", 2095); + jsonResponse.put("generation_token_count", 503); + jsonResponse.put("stop_reason", "stop"); + } else if (modelId.contains("mistral")) { + ArrayNode outputs = mapper.createArrayNode(); + ObjectNode output = mapper.createObjectNode(); + + output.put( + "text", + "A compiler translates the entire source code to machine code before execution, while an interpreter executes the code line by line in real-time."); + output.put("stop_reason", "stop"); + + outputs.add(output); + + jsonResponse.set("outputs", outputs); + } + return jsonResponse; }); } diff --git a/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java b/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java index 0a7498bf2a..ad5f7a73b4 100644 --- a/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java +++ b/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java @@ -56,12 +56,16 @@ import com.amazonaws.services.sqs.model.CreateQueueRequest; import com.amazonaws.services.sqs.model.ReceiveMessageRequest; import com.amazonaws.services.sqs.model.SendMessageRequest; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.io.IOException; import java.net.http.HttpClient; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -578,6 +582,175 @@ private static void setupBedrock() { bedrockRuntimeClient.invokeModel(invokeModelRequest); return ""; }); + get( + "/bedrockruntime/invokeModel/ai21Jamba", + (req, res) -> { + setMainStatus(200); + String modelId = "ai21.jamba-1-5-mini-v1:0"; + + ObjectMapper mapper = new ObjectMapper(); + Map request = new HashMap<>(); + + List> messages = new ArrayList<>(); + Map message = new HashMap<>(); + message.put("role", "user"); + message.put("content", "Which LLM are you?"); + messages.add(message); + + request.put("messages", messages); + request.put("max_tokens", 1000); + request.put("top_p", 0.8); + request.put("temperature", 0.7); + + InvokeModelRequest invokeModelRequest = + new InvokeModelRequest() + .withModelId(modelId) + .withBody(StandardCharsets.UTF_8.encode(mapper.writeValueAsString(request))); + + var response = bedrockRuntimeClient.invokeModel(invokeModelRequest); + var responseBody = new String(response.getBody().array(), StandardCharsets.UTF_8); + + return ""; + }); + get( + "/bedrockruntime/invokeModel/amazonTitan", + (req, res) -> { + setMainStatus(200); + String modelId = "amazon.titan-text-premier-v1:0"; + + ObjectMapper mapper = new ObjectMapper(); + Map request = new HashMap<>(); + request.put("inputText", "Hello, world!"); + + Map config = new HashMap<>(); + config.put("temperature", 0.7); + config.put("topP", 0.9); + config.put("maxTokenCount", 100); + + request.put("textGenerationConfig", config); + + InvokeModelRequest invokeModelRequest = + new InvokeModelRequest() + .withModelId(modelId) + .withBody(StandardCharsets.UTF_8.encode(mapper.writeValueAsString(request))); + + var response = bedrockRuntimeClient.invokeModel(invokeModelRequest); + var responseBody = new String(response.getBody().array(), StandardCharsets.UTF_8); + + return ""; + }); + get( + "/bedrockruntime/invokeModel/anthropicClaude", + (req, res) -> { + setMainStatus(200); + String modelId = "anthropic.claude-3-haiku-20240307-v1:0"; + + ObjectMapper mapper = new ObjectMapper(); + Map request = new HashMap<>(); + + List> messages = new ArrayList<>(); + Map message = new HashMap<>(); + message.put("role", "user"); + message.put("content", "Describe a cache in one line"); + messages.add(message); + + request.put("messages", messages); + request.put("anthropic_version", "bedrock-2023-05-31"); + request.put("max_tokens", 512); + request.put("top_p", 0.53); + request.put("temperature", 0.6); + + InvokeModelRequest invokeModelRequest = + new InvokeModelRequest() + .withModelId(modelId) + .withBody(StandardCharsets.UTF_8.encode(mapper.writeValueAsString(request))); + + var response = bedrockRuntimeClient.invokeModel(invokeModelRequest); + var responseBody = new String(response.getBody().array(), StandardCharsets.UTF_8); + + return ""; + }); + get( + "/bedrockruntime/invokeModel/cohereCommandR", + (req, res) -> { + setMainStatus(200); + String modelId = "cohere.command-r-v1:0"; + + ObjectMapper mapper = new ObjectMapper(); + Map request = new HashMap<>(); + + request.put("message", "Convince me to write a LISP interpreter in one line"); + request.put("temperature", 0.8); + request.put("max_tokens", 4096); + request.put("p", 0.45); + + InvokeModelRequest invokeModelRequest = + new InvokeModelRequest() + .withModelId(modelId) + .withBody(StandardCharsets.UTF_8.encode(mapper.writeValueAsString(request))); + + var response = bedrockRuntimeClient.invokeModel(invokeModelRequest); + var responseBody = new String(response.getBody().array(), StandardCharsets.UTF_8); + + return ""; + }); + get( + "/bedrockruntime/invokeModel/metaLlama", + (req, res) -> { + setMainStatus(200); + String modelId = "meta.llama3-70b-instruct-v1:0"; + + ObjectMapper mapper = new ObjectMapper(); + Map request = new HashMap<>(); + + String prompt = "Describe the purpose of a 'hello world' program in one line"; + String instruction = + String.format( + "<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n%s<|eot_id|>\n<|start_header_id|>assistant<|end_header_id|>\n", + prompt); + + request.put("prompt", instruction); + request.put("max_gen_len", 128); + request.put("temperature", 0.1); + request.put("top_p", 0.9); + + InvokeModelRequest invokeModelRequest = + new InvokeModelRequest() + .withModelId(modelId) + .withBody(StandardCharsets.UTF_8.encode(mapper.writeValueAsString(request))); + + var response = bedrockRuntimeClient.invokeModel(invokeModelRequest); + var responseBody = new String(response.getBody().array(), StandardCharsets.UTF_8); + + return ""; + }); + get( + "/bedrockruntime/invokeModel/mistralAi", + (req, res) -> { + setMainStatus(200); + String modelId = "mistral.mistral-large-2402-v1:0"; + + ObjectMapper mapper = new ObjectMapper(); + Map request = new HashMap<>(); + + String prompt = "Describe the difference between a compiler and interpreter in one line."; + String instruction = String.format("[INST] %s [/INST]\n", prompt); + + request.put("prompt", instruction); + request.put("max_tokens", 4096); + request.put("temperature", 0.75); + request.put("top_p", 0.25); + + InvokeModelRequest invokeModelRequest = + new InvokeModelRequest() + .withModelId(modelId) + .withBody(StandardCharsets.UTF_8.encode(mapper.writeValueAsString(request))); + + var response = bedrockRuntimeClient.invokeModel(invokeModelRequest); + var responseBody = new String(response.getBody().array(), StandardCharsets.UTF_8); + + return ""; + }); get( "/bedrockagent/get-data-source", diff --git a/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java b/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java index bc43402e9d..2982135fd4 100644 --- a/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java +++ b/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java @@ -28,6 +28,9 @@ import java.net.http.HttpClient; import java.nio.charset.Charset; import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -601,6 +604,169 @@ private static void setupBedrock() { return ""; }); get( + "/bedrockruntime/invokeModel/ai21Jamba", + (req, res) -> { + setMainStatus(200); + + ObjectMapper mapper = new ObjectMapper(); + Map request = new HashMap<>(); + + List> messages = new ArrayList<>(); + Map message = new HashMap<>(); + message.put("role", "user"); + message.put("content", "Which LLM are you?"); + messages.add(message); + + request.put("messages", messages); + request.put("max_tokens", 1000); + request.put("top_p", 0.8); + request.put("temperature", 0.7); + + InvokeModelRequest invokeModelRequest = + InvokeModelRequest.builder() + .modelId("ai21.jamba-1-5-mini-v1:0") + .body(SdkBytes.fromUtf8String(mapper.writeValueAsString(request))) + .build(); + + bedrockRuntimeClient.invokeModel(invokeModelRequest); + + return ""; + }); + get( + "/bedrockruntime/invokeModel/amazonTitan", + (req, res) -> { + setMainStatus(200); + + ObjectMapper mapper = new ObjectMapper(); + Map request = new HashMap<>(); + request.put("inputText", "Hello, world!"); + + Map config = new HashMap<>(); + config.put("temperature", 0.7); + config.put("topP", 0.9); + config.put("maxTokenCount", 100); + + request.put("textGenerationConfig", config); + + InvokeModelRequest invokeModelRequest = + InvokeModelRequest.builder() + .modelId("amazon.titan-text-premier-v1:0") + .body(SdkBytes.fromUtf8String(mapper.writeValueAsString(request))) + .build(); + + bedrockRuntimeClient.invokeModel(invokeModelRequest); + + return ""; + }); + get( + "/bedrockruntime/invokeModel/anthropicClaude", + (req, res) -> { + setMainStatus(200); + + ObjectMapper mapper = new ObjectMapper(); + Map request = new HashMap<>(); + + List> messages = new ArrayList<>(); + Map message = new HashMap<>(); + message.put("role", "user"); + message.put("content", "Describe a cache in one line"); + messages.add(message); + + request.put("messages", messages); + request.put("anthropic_version", "bedrock-2023-05-31"); + request.put("max_tokens", 512); + request.put("top_p", 0.53); + request.put("temperature", 0.6); + + InvokeModelRequest invokeModelRequest = + InvokeModelRequest.builder() + .modelId("anthropic.claude-3-haiku-20240307-v1:0") + .body(SdkBytes.fromUtf8String(mapper.writeValueAsString(request))) + .build(); + + bedrockRuntimeClient.invokeModel(invokeModelRequest); + + return ""; + }); + get( + "/bedrockruntime/invokeModel/cohereCommandR", + (req, res) -> { + setMainStatus(200); + + ObjectMapper mapper = new ObjectMapper(); + Map request = new HashMap<>(); + + request.put("message", "Convince me to write a LISP interpreter in one line"); + request.put("temperature", 0.8); + request.put("max_tokens", 4096); + request.put("p", 0.45); + + InvokeModelRequest invokeModelRequest = + InvokeModelRequest.builder() + .modelId("cohere.command-r-v1:0") + .body(SdkBytes.fromUtf8String(mapper.writeValueAsString(request))) + .build(); + + bedrockRuntimeClient.invokeModel(invokeModelRequest); + + return ""; + }); + get( + "/bedrockruntime/invokeModel/metaLlama", + (req, res) -> { + setMainStatus(200); + + ObjectMapper mapper = new ObjectMapper(); + Map request = new HashMap<>(); + + String prompt = "Describe the purpose of a 'hello world' program in one line"; + String instruction = + String.format( + "<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n%s<|eot_id|>\n<|start_header_id|>assistant<|end_header_id|>\n", + prompt); + + request.put("prompt", instruction); + request.put("max_gen_len", 128); + request.put("temperature", 0.1); + request.put("top_p", 0.9); + + InvokeModelRequest invokeModelRequest = + InvokeModelRequest.builder() + .modelId("meta.llama3-70b-instruct-v1:0") + .body(SdkBytes.fromUtf8String(mapper.writeValueAsString(request))) + .build(); + + bedrockRuntimeClient.invokeModel(invokeModelRequest); + + return ""; + }); + get( + "/bedrockruntime/invokeModel/mistralAi", + (req, res) -> { + setMainStatus(200); + + ObjectMapper mapper = new ObjectMapper(); + Map request = new HashMap<>(); + + String prompt = "Describe the difference between a compiler and interpreter in one line."; + String instruction = String.format("[INST] %s [/INST]\n", prompt); + + request.put("prompt", instruction); + request.put("max_tokens", 4096); + request.put("temperature", 0.75); + request.put("top_p", 0.25); + + InvokeModelRequest invokeModelRequest = + InvokeModelRequest.builder() + .modelId("mistral.mistral-large-2402-v1:0") + .body(SdkBytes.fromUtf8String(mapper.writeValueAsString(request))) + .build(); + + bedrockRuntimeClient.invokeModel(invokeModelRequest); + + return ""; + }); + get( "/bedrockagent/get-data-source", (req, res) -> { setMainStatus(200); From 015c6db998baa9a86d26fdad697ef27e5bb926f4 Mon Sep 17 00:00:00 2001 From: Prashant Srivastava <50466688+srprash@users.noreply.github.com> Date: Fri, 22 Nov 2024 09:38:29 -0800 Subject: [PATCH 08/11] Utility for building a lambda layer artifact (#953) --- lambda-layer/.gitignore | 5 + lambda-layer/build-layer.sh | 48 ++ lambda-layer/build.gradle.kts | 46 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 61574 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + lambda-layer/gradlew | 245 +++++++ lambda-layer/gradlew.bat | 93 +++ lambda-layer/otel-handler | 24 + .../aws-otel-java-instrumentation.patch | 13 + .../opentelemetry-java-instrumentation.patch | 645 ++++++++++++++++++ lambda-layer/settings.gradle.kts | 16 + 11 files changed, 1141 insertions(+) create mode 100644 lambda-layer/.gitignore create mode 100755 lambda-layer/build-layer.sh create mode 100644 lambda-layer/build.gradle.kts create mode 100644 lambda-layer/gradle/wrapper/gradle-wrapper.jar create mode 100644 lambda-layer/gradle/wrapper/gradle-wrapper.properties create mode 100755 lambda-layer/gradlew create mode 100644 lambda-layer/gradlew.bat create mode 100644 lambda-layer/otel-handler create mode 100644 lambda-layer/patches/aws-otel-java-instrumentation.patch create mode 100644 lambda-layer/patches/opentelemetry-java-instrumentation.patch create mode 100644 lambda-layer/settings.gradle.kts diff --git a/lambda-layer/.gitignore b/lambda-layer/.gitignore new file mode 100644 index 0000000000..1b6985c009 --- /dev/null +++ b/lambda-layer/.gitignore @@ -0,0 +1,5 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/lambda-layer/build-layer.sh b/lambda-layer/build-layer.sh new file mode 100755 index 0000000000..b99fce80c5 --- /dev/null +++ b/lambda-layer/build-layer.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +SOURCEDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + + +## Get OTel version +file="$SOURCEDIR/../.github/patches/versions" +version=$(awk -F'=v' '/OTEL_JAVA_INSTRUMENTATION_VERSION/ {print $2}' "$file") +echo "Found OTEL Version: ${version}" +# Exit if the version is empty or null +if [[ -z "$version" ]]; then + echo "Error: Version could not be found in ${file}." + exit 1 +fi + + +## Clone and Patch the OpenTelemetry Java Instrumentation Repository +git clone https://github.com/open-telemetry/opentelemetry-java-instrumentation.git +pushd opentelemetry-java-instrumentation +git checkout v${version} -b tag-v${version} + +# This patch is for Lambda related context propagation +patch -p1 < "$SOURCEDIR"/patches/opentelemetry-java-instrumentation.patch + +# There is another patch in the .github/patches directory for other changes. We should apply them too for consistency. +patch -p1 < "$SOURCEDIR"/../.github/patches/opentelemetry-java-instrumentation.patch + +git add -A +git commit -m "Create patch version" +./gradlew publishToMavenLocal +popd +rm -rf opentelemetry-java-instrumentation + + +## Build the ADOT Java from current source +pushd "$SOURCEDIR"/.. +patch -p1 < "${SOURCEDIR}"/patches/aws-otel-java-instrumentation.patch +CI=false ./gradlew publishToMavenLocal -Prelease.version=${version}-adot-lambda1 +popd + + +## Build ADOT Lambda Java SDK Layer Code +./gradlew build + + +## Copy ADOT Java Agent downloaded using Gradle task and bundle it with the Lambda handler script +cp "$SOURCEDIR"/build/javaagent/aws-opentelemetry-agent*.jar ./opentelemetry-javaagent.jar +zip -qr opentelemetry-javaagent-layer.zip opentelemetry-javaagent.jar otel-handler diff --git a/lambda-layer/build.gradle.kts b/lambda-layer/build.gradle.kts new file mode 100644 index 0000000000..92da026df2 --- /dev/null +++ b/lambda-layer/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + java + + id("com.diffplug.spotless") +} + +base.archivesBaseName = "aws-otel-lambda-java-extensions" +group = "software.amazon.opentelemetry.lambda" + +repositories { + mavenCentral() + mavenLocal() +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +spotless { + java { + googleJavaFormat("1.15.0") + } +} + +val javaagentDependency by configurations.creating { + extendsFrom() +} + +dependencies { + compileOnly(platform("io.opentelemetry:opentelemetry-bom:1.32.1")) + compileOnly(platform("io.opentelemetry:opentelemetry-bom-alpha:1.32.1-alpha")) + // Already included in wrapper so compileOnly + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-aws") + javaagentDependency("software.amazon.opentelemetry:aws-opentelemetry-agent:1.32.1-adot-lambda1") +} + +tasks.register("download") { + from(javaagentDependency) + into("$buildDir/javaagent") +} + +tasks.named("build") { + dependsOn("download") +} diff --git a/lambda-layer/gradle/wrapper/gradle-wrapper.jar b/lambda-layer/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..943f0cbfa754578e88a3dae77fce6e3dea56edbf GIT binary patch literal 61574 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 ScNgau(U?ep-K_E5zy1%ZQTdPn literal 0 HcmV?d00001 diff --git a/lambda-layer/gradle/wrapper/gradle-wrapper.properties b/lambda-layer/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..37aef8d3f0 --- /dev/null +++ b/lambda-layer/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/lambda-layer/gradlew b/lambda-layer/gradlew new file mode 100755 index 0000000000..5bba57aa19 --- /dev/null +++ b/lambda-layer/gradlew @@ -0,0 +1,245 @@ + +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/lambda-layer/gradlew.bat b/lambda-layer/gradlew.bat new file mode 100644 index 0000000000..084f02df4a --- /dev/null +++ b/lambda-layer/gradlew.bat @@ -0,0 +1,93 @@ + +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lambda-layer/otel-handler b/lambda-layer/otel-handler new file mode 100644 index 0000000000..9104e2a7b2 --- /dev/null +++ b/lambda-layer/otel-handler @@ -0,0 +1,24 @@ +#!/bin/bash + +export OTEL_INSTRUMENTATION_AWS_SDK_EXPERIMENTAL_SPAN_ATTRIBUTES=true + +export OTEL_PROPAGATORS="${OTEL_PROPAGATORS:-xray,tracecontext,b3,b3multi}" + +# Temporarily set OTEL_SERVICE_NAME variable to work around but in javaagent not handling +# OTEL_RESOURCE_ATTRIBUTES as set in otel-handler-upstream. It doesn't hurt to apply this +# to wrapper as well. +# TODO(anuraaga): Move to opentelemetry-lambda +export OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME:-${AWS_LAMBDA_FUNCTION_NAME}} + +export JAVA_TOOL_OPTIONS="-javaagent:/opt/opentelemetry-javaagent.jar ${JAVA_TOOL_OPTIONS}" + +if [[ $OTEL_RESOURCE_ATTRIBUTES != *"service.name="* ]]; then + export OTEL_RESOURCE_ATTRIBUTES="service.name=${AWS_LAMBDA_FUNCTION_NAME},${OTEL_RESOURCE_ATTRIBUTES}" +fi + +export OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT=10000 + +# Disable the Application Signals runtime metrics since we are on Lambda +export OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED=false + +exec "$@" diff --git a/lambda-layer/patches/aws-otel-java-instrumentation.patch b/lambda-layer/patches/aws-otel-java-instrumentation.patch new file mode 100644 index 0000000000..0a65783bfb --- /dev/null +++ b/lambda-layer/patches/aws-otel-java-instrumentation.patch @@ -0,0 +1,13 @@ +diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts +index 9493189..6090207 100644 +--- a/dependencyManagement/build.gradle.kts ++++ b/dependencyManagement/build.gradle.kts +@@ -27,7 +27,7 @@ data class DependencySet(val group: String, val version: String, val modules: Li + val TEST_SNAPSHOTS = rootProject.findProperty("testUpstreamSnapshots") == "true" + + // This is the version of the upstream instrumentation BOM +-val otelVersion = "1.32.1-adot2" ++val otelVersion = "1.32.1-adot-lambda1" + val otelSnapshotVersion = "1.33.0" + val otelAlphaVersion = if (!TEST_SNAPSHOTS) "$otelVersion-alpha" else "$otelSnapshotVersion-alpha-SNAPSHOT" + val otelJavaAgentVersion = if (!TEST_SNAPSHOTS) otelVersion else "$otelSnapshotVersion-SNAPSHOT" \ No newline at end of file diff --git a/lambda-layer/patches/opentelemetry-java-instrumentation.patch b/lambda-layer/patches/opentelemetry-java-instrumentation.patch new file mode 100644 index 0000000000..4ccb33e9a2 --- /dev/null +++ b/lambda-layer/patches/opentelemetry-java-instrumentation.patch @@ -0,0 +1,645 @@ +diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/ApiGatewayProxyRequest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/ApiGatewayProxyRequest.java +index a96fa5e3f9..df5bcec438 100644 +--- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/ApiGatewayProxyRequest.java ++++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/ApiGatewayProxyRequest.java +@@ -30,7 +30,14 @@ public abstract class ApiGatewayProxyRequest { + private static boolean noHttpPropagationNeeded() { + Collection fields = + GlobalOpenTelemetry.getPropagators().getTextMapPropagator().fields(); +- return fields.isEmpty(); ++ return fields.isEmpty() || xrayPropagationFieldsOnly(fields); ++ } ++ ++ private static boolean xrayPropagationFieldsOnly(Collection fields) { ++ // ugly but faster than typical convert-to-set-and-check-contains-only ++ return (fields.size() == 1) ++ && ParentContextExtractor.AWS_TRACE_HEADER_PROPAGATOR_KEY.equalsIgnoreCase( ++ fields.iterator().next()); + } + + public static ApiGatewayProxyRequest forStream(InputStream source) { +diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenter.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenter.java +index 4136e7bed9..dbbcb1c99d 100644 +--- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenter.java ++++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenter.java +@@ -11,7 +11,6 @@ import io.opentelemetry.context.propagation.TextMapGetter; + import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + import io.opentelemetry.instrumentation.api.internal.ContextPropagationDebug; + import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest; +-import java.util.Locale; + import java.util.Map; + import javax.annotation.Nullable; + +@@ -47,25 +46,15 @@ public class AwsLambdaFunctionInstrumenter { + } + + public Context extract(AwsLambdaRequest input) { ++ return ParentContextExtractor.extract(input.getHeaders(), this); ++ } ++ ++ public Context extract(Map headers, TextMapGetter> getter) { + ContextPropagationDebug.debugContextLeakIfEnabled(); + + return openTelemetry + .getPropagators() + .getTextMapPropagator() +- .extract(Context.root(), input.getHeaders(), MapGetter.INSTANCE); +- } +- +- private enum MapGetter implements TextMapGetter> { +- INSTANCE; +- +- @Override +- public Iterable keys(Map map) { +- return map.keySet(); +- } +- +- @Override +- public String get(Map map, String s) { +- return map.get(s.toLowerCase(Locale.ROOT)); +- } ++ .extract(Context.root(), headers, getter); + } + } +diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java +index aeb828b8e7..277c358ca8 100644 +--- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java ++++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsLambdaFunctionInstrumenterFactory.java +@@ -23,7 +23,6 @@ public final class AwsLambdaFunctionInstrumenterFactory { + openTelemetry, + "io.opentelemetry.aws-lambda-core-1.0", + AwsLambdaFunctionInstrumenterFactory::spanName) +- .addSpanLinksExtractor(new AwsXrayEnvSpanLinksExtractor()) + .addAttributesExtractor(new AwsLambdaFunctionAttributesExtractor()) + .buildInstrumenter(SpanKindExtractor.alwaysServer())); + } +diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractor.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractor.java +deleted file mode 100644 +index c88cf20c91..0000000000 +--- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractor.java ++++ /dev/null +@@ -1,84 +0,0 @@ +-/* +- * Copyright The OpenTelemetry Authors +- * SPDX-License-Identifier: Apache-2.0 +- */ +- +-package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal; +- +-import io.opentelemetry.api.common.AttributeKey; +-import io.opentelemetry.api.common.Attributes; +-import io.opentelemetry.api.trace.Span; +-import io.opentelemetry.api.trace.SpanContext; +-import io.opentelemetry.context.Context; +-import io.opentelemetry.context.propagation.TextMapGetter; +-import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; +-import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksBuilder; +-import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksExtractor; +-import io.opentelemetry.instrumentation.awslambdacore.v1_0.AwsLambdaRequest; +-import java.util.Collections; +-import java.util.Locale; +-import java.util.Map; +- +-/** +- * This class is internal and is hence not for public use. Its APIs are unstable and can change at +- * any time. +- */ +-final class AwsXrayEnvSpanLinksExtractor implements SpanLinksExtractor { +- +- private static final String AWS_TRACE_HEADER_ENV_KEY = "_X_AMZN_TRACE_ID"; +- private static final String AWS_TRACE_HEADER_PROP = "com.amazonaws.xray.traceHeader"; +- // lower-case map getter used for extraction +- private static final String AWS_TRACE_HEADER_PROPAGATOR_KEY = "x-amzn-trace-id"; +- +- private static final Attributes LINK_ATTRIBUTES = +- Attributes.of(AttributeKey.stringKey("source"), "x-ray-env"); +- +- @Override +- public void extract( +- SpanLinksBuilder spanLinks, +- io.opentelemetry.context.Context parentContext, +- AwsLambdaRequest awsLambdaRequest) { +- extract(spanLinks); +- } +- +- public static void extract(SpanLinksBuilder spanLinks) { +- Map contextMap = getTraceHeaderMap(); +- if (contextMap.isEmpty()) { +- return; +- } +- Context xrayContext = +- AwsXrayPropagator.getInstance().extract(Context.root(), contextMap, MapGetter.INSTANCE); +- SpanContext envVarSpanCtx = Span.fromContext(xrayContext).getSpanContext(); +- if (envVarSpanCtx.isValid()) { +- spanLinks.addLink(envVarSpanCtx, LINK_ATTRIBUTES); +- } +- } +- +- private static Map getTraceHeaderMap() { +- String traceHeader = System.getProperty(AWS_TRACE_HEADER_PROP); +- if (isEmptyOrNull(traceHeader)) { +- traceHeader = System.getenv(AWS_TRACE_HEADER_ENV_KEY); +- } +- return isEmptyOrNull(traceHeader) +- ? Collections.emptyMap() +- : Collections.singletonMap(AWS_TRACE_HEADER_PROPAGATOR_KEY, traceHeader); +- } +- +- private static boolean isEmptyOrNull(String value) { +- return value == null || value.isEmpty(); +- } +- +- private enum MapGetter implements TextMapGetter> { +- INSTANCE; +- +- @Override +- public Iterable keys(Map map) { +- return map.keySet(); +- } +- +- @Override +- public String get(Map map, String s) { +- return map.get(s.toLowerCase(Locale.ROOT)); +- } +- } +-} +diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/ParentContextExtractor.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/ParentContextExtractor.java +new file mode 100644 +index 0000000000..72d4f9253b +--- /dev/null ++++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/ParentContextExtractor.java +@@ -0,0 +1,91 @@ ++/* ++ * Copyright The OpenTelemetry Authors ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal; ++ ++import static io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.MapUtils.lowercaseMap; ++ ++import io.opentelemetry.api.trace.Span; ++import io.opentelemetry.api.trace.SpanContext; ++import io.opentelemetry.context.Context; ++import io.opentelemetry.context.propagation.TextMapGetter; ++import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; ++import java.util.Collections; ++import java.util.Locale; ++import java.util.Map; ++ ++/** ++ * This class is internal and is hence not for public use. Its APIs are unstable and can change at ++ * any time. ++ */ ++public final class ParentContextExtractor { ++ ++ private static final String AWS_TRACE_HEADER_ENV_KEY = "_X_AMZN_TRACE_ID"; ++ private static final String AWS_TRACE_HEADER_PROP = "com.amazonaws.xray.traceHeader"; ++ ++ static Context extract(Map headers, AwsLambdaFunctionInstrumenter instrumenter) { ++ Context parentContext = null; ++ String parentTraceHeader = getTraceHeader(); ++ if (parentTraceHeader != null) { ++ parentContext = fromXrayHeader(parentTraceHeader); ++ } ++ if (!isValidAndSampled(parentContext)) { ++ // try http ++ parentContext = fromHttpHeaders(headers, instrumenter); ++ } ++ return parentContext; ++ } ++ ++ private static String getTraceHeader() { ++ // Lambda propagates trace header by system property instead of environment variable from java17 ++ String traceHeader = System.getProperty(AWS_TRACE_HEADER_PROP); ++ if (traceHeader == null || traceHeader.isEmpty()) { ++ return System.getenv(AWS_TRACE_HEADER_ENV_KEY); ++ } ++ return traceHeader; ++ } ++ ++ private static boolean isValidAndSampled(Context context) { ++ if (context == null) { ++ return false; ++ } ++ Span parentSpan = Span.fromContext(context); ++ SpanContext parentSpanContext = parentSpan.getSpanContext(); ++ return (parentSpanContext.isValid() && parentSpanContext.isSampled()); ++ } ++ ++ private static Context fromHttpHeaders( ++ Map headers, AwsLambdaFunctionInstrumenter instrumenter) { ++ return instrumenter.extract(lowercaseMap(headers), MapGetter.INSTANCE); ++ } ++ ++ // lower-case map getter used for extraction ++ static final String AWS_TRACE_HEADER_PROPAGATOR_KEY = "x-amzn-trace-id"; ++ ++ public static Context fromXrayHeader(String parentHeader) { ++ return AwsXrayPropagator.getInstance() ++ .extract( ++ // see BaseTracer#extract() on why we're using root() here ++ Context.root(), ++ Collections.singletonMap(AWS_TRACE_HEADER_PROPAGATOR_KEY, parentHeader), ++ MapGetter.INSTANCE); ++ } ++ ++ private enum MapGetter implements TextMapGetter> { ++ INSTANCE; ++ ++ @Override ++ public Iterable keys(Map map) { ++ return map.keySet(); ++ } ++ ++ @Override ++ public String get(Map map, String s) { ++ return map.get(s.toLowerCase(Locale.ROOT)); ++ } ++ } ++ ++ private ParentContextExtractor() {} ++} +diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractorTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractorTest.java +deleted file mode 100644 +index 509bfbd05e..0000000000 +--- a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/AwsXrayEnvSpanLinksExtractorTest.java ++++ /dev/null +@@ -1,128 +0,0 @@ +-/* +- * Copyright The OpenTelemetry Authors +- * SPDX-License-Identifier: Apache-2.0 +- */ +- +-package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal; +- +-import static org.assertj.core.api.Assertions.assertThat; +-import static org.mockito.ArgumentMatchers.eq; +-import static org.mockito.Mockito.mock; +-import static org.mockito.Mockito.verify; +-import static org.mockito.Mockito.verifyNoInteractions; +- +-import io.opentelemetry.api.common.AttributeKey; +-import io.opentelemetry.api.common.Attributes; +-import io.opentelemetry.api.trace.SpanContext; +-import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksBuilder; +-import org.junit.jupiter.api.Test; +-import org.junit.jupiter.api.extension.ExtendWith; +-import org.mockito.ArgumentCaptor; +-import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; +-import uk.org.webcompere.systemstubs.jupiter.SystemStub; +-import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; +-import uk.org.webcompere.systemstubs.properties.SystemProperties; +- +-/** +- * This class is internal and is hence not for public use. Its APIs are unstable and can change at +- * any time. +- */ +-@ExtendWith(SystemStubsExtension.class) +-class AwsXrayEnvSpanLinksExtractorTest { +- private static final Attributes EXPECTED_LINK_ATTRIBUTES = +- Attributes.of(AttributeKey.stringKey("source"), "x-ray-env"); +- +- @SystemStub final EnvironmentVariables environmentVariables = new EnvironmentVariables(); +- @SystemStub final SystemProperties systemProperties = new SystemProperties(); +- +- @Test +- void shouldIgnoreIfEnvVarAndSystemPropertyEmpty() { +- // given +- SpanLinksBuilder spanLinksBuilder = mock(SpanLinksBuilder.class); +- environmentVariables.set("_X_AMZN_TRACE_ID", ""); +- systemProperties.set("com.amazonaws.xray.traceHeader", ""); +- // when +- AwsXrayEnvSpanLinksExtractor.extract(spanLinksBuilder); +- // then +- verifyNoInteractions(spanLinksBuilder); +- } +- +- @Test +- void shouldLinkAwsParentHeaderAndChooseSystemPropertyIfValidAndNotSampled() { +- // given +- SpanLinksBuilder spanLinksBuilder = mock(SpanLinksBuilder.class); +- environmentVariables.set( +- "_X_AMZN_TRACE_ID", +- "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=0"); +- systemProperties.set( +- "com.amazonaws.xray.traceHeader", +- "Root=1-8a3c60f7-d188f8fa79d48a391a778fa7;Parent=0000000000000789;Sampled=0"); +- // when +- AwsXrayEnvSpanLinksExtractor.extract(spanLinksBuilder); +- // then +- ArgumentCaptor captor = ArgumentCaptor.forClass(SpanContext.class); +- verify(spanLinksBuilder).addLink(captor.capture(), eq(EXPECTED_LINK_ATTRIBUTES)); +- SpanContext spanContext = captor.getValue(); +- assertThat(spanContext.isValid()).isTrue(); +- assertThat(spanContext.isSampled()).isFalse(); +- assertThat(spanContext.getSpanId()).isEqualTo("0000000000000789"); +- assertThat(spanContext.getTraceId()).isEqualTo("8a3c60f7d188f8fa79d48a391a778fa7"); +- } +- +- @Test +- void shouldLinkAwsParentHeaderIfValidAndNotSampled() { +- // given +- SpanLinksBuilder spanLinksBuilder = mock(SpanLinksBuilder.class); +- environmentVariables.set( +- "_X_AMZN_TRACE_ID", +- "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=0"); +- // when +- AwsXrayEnvSpanLinksExtractor.extract(spanLinksBuilder); +- // then +- ArgumentCaptor captor = ArgumentCaptor.forClass(SpanContext.class); +- verify(spanLinksBuilder).addLink(captor.capture(), eq(EXPECTED_LINK_ATTRIBUTES)); +- SpanContext spanContext = captor.getValue(); +- assertThat(spanContext.isValid()).isTrue(); +- assertThat(spanContext.isSampled()).isFalse(); +- assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456"); +- assertThat(spanContext.getTraceId()).isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6"); +- } +- +- @Test +- void shouldLinkAwsParentHeaderIfValidAndNotSampledSystemProperty() { +- // given +- SpanLinksBuilder spanLinksBuilder = mock(SpanLinksBuilder.class); +- systemProperties.set( +- "com.amazonaws.xray.traceHeader", +- "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=0"); +- // when +- AwsXrayEnvSpanLinksExtractor.extract(spanLinksBuilder); +- // then +- ArgumentCaptor captor = ArgumentCaptor.forClass(SpanContext.class); +- verify(spanLinksBuilder).addLink(captor.capture(), eq(EXPECTED_LINK_ATTRIBUTES)); +- SpanContext spanContext = captor.getValue(); +- assertThat(spanContext.isValid()).isTrue(); +- assertThat(spanContext.isSampled()).isFalse(); +- assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456"); +- assertThat(spanContext.getTraceId()).isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6"); +- } +- +- @Test +- void shouldLinkAwsParentHeaderIfValidAndSampledSystemProperty() { +- // given +- SpanLinksBuilder spanLinksBuilder = mock(SpanLinksBuilder.class); +- systemProperties.set( +- "com.amazonaws.xray.traceHeader", +- "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=1"); +- // when +- AwsXrayEnvSpanLinksExtractor.extract(spanLinksBuilder); +- // then +- ArgumentCaptor captor = ArgumentCaptor.forClass(SpanContext.class); +- verify(spanLinksBuilder).addLink(captor.capture(), eq(EXPECTED_LINK_ATTRIBUTES)); +- SpanContext spanContext = captor.getValue(); +- assertThat(spanContext.isValid()).isTrue(); +- assertThat(spanContext.isSampled()).isTrue(); +- assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456"); +- assertThat(spanContext.getTraceId()).isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6"); +- } +-} +diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/ParentContextExtractorTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/ParentContextExtractorTest.java +new file mode 100644 +index 0000000000..1fa0b6e536 +--- /dev/null ++++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/library/src/test/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/internal/ParentContextExtractorTest.java +@@ -0,0 +1,135 @@ ++/* ++ * Copyright The OpenTelemetry Authors ++ * SPDX-License-Identifier: Apache-2.0 ++ */ ++ ++package io.opentelemetry.instrumentation.awslambdacore.v1_0.internal; ++ ++import static org.assertj.core.api.Assertions.assertThat; ++ ++import com.google.common.collect.ImmutableMap; ++import io.opentelemetry.api.OpenTelemetry; ++import io.opentelemetry.api.trace.Span; ++import io.opentelemetry.api.trace.SpanContext; ++import io.opentelemetry.context.Context; ++import io.opentelemetry.context.propagation.ContextPropagators; ++import io.opentelemetry.extension.trace.propagation.B3Propagator; ++import java.util.Map; ++import org.junit.jupiter.api.Test; ++import org.junit.jupiter.api.extension.ExtendWith; ++import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; ++import uk.org.webcompere.systemstubs.jupiter.SystemStub; ++import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; ++import uk.org.webcompere.systemstubs.properties.SystemProperties; ++ ++/** ++ * This class is internal and is hence not for public use. Its APIs are unstable and can change at ++ * any time. ++ */ ++@ExtendWith(SystemStubsExtension.class) ++class ParentContextExtractorTest { ++ ++ @SystemStub final EnvironmentVariables environmentVariables = new EnvironmentVariables(); ++ ++ private static final OpenTelemetry OTEL = ++ OpenTelemetry.propagating(ContextPropagators.create(B3Propagator.injectingSingleHeader())); ++ ++ private static final AwsLambdaFunctionInstrumenter INSTRUMENTER = ++ AwsLambdaFunctionInstrumenterFactory.createInstrumenter(OTEL); ++ ++ @Test ++ void shouldUseHttpIfAwsParentNotSampled() { ++ // given ++ Map headers = ++ ImmutableMap.of( ++ "X-b3-traceId", ++ "4fd0b6131f19f39af59518d127b0cafe", ++ "x-b3-spanid", ++ "0000000000000123", ++ "X-B3-Sampled", ++ "true"); ++ environmentVariables.set( ++ "_X_AMZN_TRACE_ID", ++ "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=0"); ++ ++ // when ++ Context context = ParentContextExtractor.extract(headers, INSTRUMENTER); ++ // then ++ Span span = Span.fromContext(context); ++ SpanContext spanContext = span.getSpanContext(); ++ assertThat(spanContext.isValid()).isTrue(); ++ assertThat(spanContext.isValid()).isTrue(); ++ assertThat(spanContext.getSpanId()).isEqualTo("0000000000000123"); ++ assertThat(spanContext.getTraceId()).isEqualTo("4fd0b6131f19f39af59518d127b0cafe"); ++ } ++ ++ @Test ++ void shouldPreferAwsParentHeaderIfValidAndSampled() { ++ // given ++ Map headers = ++ ImmutableMap.of( ++ "X-b3-traceId", ++ "4fd0b6131f19f39af59518d127b0cafe", ++ "x-b3-spanid", ++ "0000000000000456", ++ "X-B3-Sampled", ++ "true"); ++ environmentVariables.set( ++ "_X_AMZN_TRACE_ID", ++ "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=1"); ++ ++ // when ++ Context context = ParentContextExtractor.extract(headers, INSTRUMENTER); ++ // then ++ Span span = Span.fromContext(context); ++ SpanContext spanContext = span.getSpanContext(); ++ assertThat(spanContext.isValid()).isTrue(); ++ assertThat(spanContext.isValid()).isTrue(); ++ assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456"); ++ assertThat(spanContext.getTraceId()).isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6"); ++ } ++ ++ @Test ++ void shouldExtractCaseInsensitiveHeaders() { ++ // given ++ Map headers = ++ ImmutableMap.of( ++ "X-b3-traceId", ++ "4fd0b6131f19f39af59518d127b0cafe", ++ "x-b3-spanid", ++ "0000000000000456", ++ "X-B3-Sampled", ++ "true"); ++ ++ // when ++ Context context = ParentContextExtractor.extract(headers, INSTRUMENTER); ++ // then ++ Span span = Span.fromContext(context); ++ SpanContext spanContext = span.getSpanContext(); ++ assertThat(spanContext.isValid()).isTrue(); ++ assertThat(spanContext.isValid()).isTrue(); ++ assertThat(spanContext.getSpanId()).isEqualTo("0000000000000456"); ++ assertThat(spanContext.getTraceId()).isEqualTo("4fd0b6131f19f39af59518d127b0cafe"); ++ } ++ ++ @Test ++ void shouldPreferSystemPropertyOverEnvVariable() { ++ // given ++ systemProperties.set( ++ "com.amazonaws.xray.traceHeader", ++ "Root=1-8a3c60f7-d188f8fa79d48a391a778fa7;Parent=0000000000000789;Sampled=0"); ++ environmentVariables.set( ++ "_X_AMZN_TRACE_ID", ++ "Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=0000000000000456;Sampled=1"); ++ ++ // when ++ Context context = ParentContextExtractor.extract(headers, INSTRUMENTER); ++ // then ++ Span span = Span.fromContext(context); ++ SpanContext spanContext = span.getSpanContext(); ++ assertThat(spanContext.isValid()).isTrue(); ++ assertThat(spanContext.isValid()).isTrue(); ++ assertThat(spanContext.getSpanId()).isEqualTo("0000000000000789"); ++ assertThat(spanContext.getTraceId()).isEqualTo("d188f8fa79d48a391a778fa7"); ++ } ++} +diff --git a/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java b/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java +index e088efa906..544da9b1bb 100644 +--- a/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java ++++ b/instrumentation/aws-lambda/aws-lambda-core-1.0/testing/src/main/java/io/opentelemetry/instrumentation/awslambdacore/v1_0/AbstractAwsLambdaTest.java +@@ -12,8 +12,6 @@ import static org.mockito.Mockito.when; + + import com.amazonaws.services.lambda.runtime.Context; + import com.amazonaws.services.lambda.runtime.RequestHandler; +-import io.opentelemetry.api.common.AttributeKey; +-import io.opentelemetry.api.common.Attributes; + import io.opentelemetry.api.trace.SpanKind; + import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; + import io.opentelemetry.sdk.trace.data.StatusData; +@@ -102,22 +100,8 @@ public abstract class AbstractAwsLambdaTest { + span -> + span.hasName("my_function") + .hasKind(SpanKind.SERVER) +- .hasLinksSatisfying( +- links -> +- assertThat(links) +- .singleElement() +- .satisfies( +- link -> { +- assertThat(link.getSpanContext().getTraceId()) +- .isEqualTo("8a3c60f7d188f8fa79d48a391a778fa6"); +- assertThat(link.getSpanContext().getSpanId()) +- .isEqualTo("0000000000000456"); +- assertThat(link.getAttributes()) +- .isEqualTo( +- Attributes.of( +- AttributeKey.stringKey("source"), +- "x-ray-env")); +- })) ++ .hasTraceId("8a3c60f7d188f8fa79d48a391a778fa6") ++ .hasParentSpanId("0000000000000456") + .hasAttributesSatisfyingExactly( + equalTo(SemanticAttributes.FAAS_INVOCATION_ID, "1-22-333")))); + } +diff --git a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SqsMessageSpanLinksExtractor.java b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SqsMessageSpanLinksExtractor.java +index 305e3e62a0..844ee31899 100644 +--- a/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SqsMessageSpanLinksExtractor.java ++++ b/instrumentation/aws-lambda/aws-lambda-events-2.2/library/src/main/java/io/opentelemetry/instrumentation/awslambdaevents/v2_2/internal/SqsMessageSpanLinksExtractor.java +@@ -9,48 +9,22 @@ import com.amazonaws.services.lambda.runtime.events.SQSEvent.SQSMessage; + import io.opentelemetry.api.trace.Span; + import io.opentelemetry.api.trace.SpanContext; + import io.opentelemetry.context.Context; +-import io.opentelemetry.context.propagation.TextMapGetter; +-import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; + import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksBuilder; + import io.opentelemetry.instrumentation.api.instrumenter.SpanLinksExtractor; +-import java.util.Collections; +-import java.util.Locale; +-import java.util.Map; ++import io.opentelemetry.instrumentation.awslambdacore.v1_0.internal.ParentContextExtractor; + + class SqsMessageSpanLinksExtractor implements SpanLinksExtractor { + private static final String AWS_TRACE_HEADER_SQS_ATTRIBUTE_KEY = "AWSTraceHeader"; + +- // lower-case map getter used for extraction +- static final String AWS_TRACE_HEADER_PROPAGATOR_KEY = "x-amzn-trace-id"; +- + @Override + public void extract(SpanLinksBuilder spanLinks, Context parentContext, SQSMessage message) { + String parentHeader = message.getAttributes().get(AWS_TRACE_HEADER_SQS_ATTRIBUTE_KEY); + if (parentHeader != null) { +- Context xrayContext = +- AwsXrayPropagator.getInstance() +- .extract( +- Context.root(), // We don't want the ambient context. +- Collections.singletonMap(AWS_TRACE_HEADER_PROPAGATOR_KEY, parentHeader), +- MapGetter.INSTANCE); +- SpanContext messageSpanCtx = Span.fromContext(xrayContext).getSpanContext(); +- if (messageSpanCtx.isValid()) { +- spanLinks.addLink(messageSpanCtx); ++ SpanContext parentCtx = ++ Span.fromContext(ParentContextExtractor.fromXrayHeader(parentHeader)).getSpanContext(); ++ if (parentCtx.isValid()) { ++ spanLinks.addLink(parentCtx); + } + } + } +- +- private enum MapGetter implements TextMapGetter> { +- INSTANCE; +- +- @Override +- public Iterable keys(Map map) { +- return map.keySet(); +- } +- +- @Override +- public String get(Map map, String s) { +- return map.get(s.toLowerCase(Locale.ROOT)); +- } +- } + } +diff --git a/version.gradle.kts b/version.gradle.kts +index cc1414c0bf..db8a59b046 100644 +--- a/version.gradle.kts ++++ b/version.gradle.kts +@@ -1,5 +1,5 @@ +-val stableVersion = "1.32.1" +-val alphaVersion = "1.32.1-alpha" ++val stableVersion = "1.32.1-adot-lambda1" ++val alphaVersion = "1.32.1-adot-lambda1-alpha" + + allprojects { + if (findProperty("otel.stable") != "true") { \ No newline at end of file diff --git a/lambda-layer/settings.gradle.kts b/lambda-layer/settings.gradle.kts new file mode 100644 index 0000000000..b37d925e37 --- /dev/null +++ b/lambda-layer/settings.gradle.kts @@ -0,0 +1,16 @@ +pluginManagement { + plugins { + id("com.diffplug.spotless") version "6.13.0" + id("com.github.ben-manes.versions") version "0.50.0" + id("com.github.johnrengelman.shadow") version "8.1.1" + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + mavenLocal() + } +} + +rootProject.name = "aws-otel-lambda-java" From aa840189ba3cc7bd7b4ef7fff57d2d3e6344a124 Mon Sep 17 00:00:00 2001 From: Prashant Srivastava <50466688+srprash@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:40:53 -0800 Subject: [PATCH 09/11] Renaming to otel-instrument and default configs (#956) --- lambda-layer/build-layer.sh | 2 +- lambda-layer/otel-handler | 24 ----------------------- lambda-layer/otel-instrument | 38 ++++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 25 deletions(-) delete mode 100644 lambda-layer/otel-handler create mode 100644 lambda-layer/otel-instrument diff --git a/lambda-layer/build-layer.sh b/lambda-layer/build-layer.sh index b99fce80c5..478ef68ba0 100755 --- a/lambda-layer/build-layer.sh +++ b/lambda-layer/build-layer.sh @@ -45,4 +45,4 @@ popd ## Copy ADOT Java Agent downloaded using Gradle task and bundle it with the Lambda handler script cp "$SOURCEDIR"/build/javaagent/aws-opentelemetry-agent*.jar ./opentelemetry-javaagent.jar -zip -qr opentelemetry-javaagent-layer.zip opentelemetry-javaagent.jar otel-handler +zip -qr opentelemetry-javaagent-layer.zip opentelemetry-javaagent.jar otel-instrument diff --git a/lambda-layer/otel-handler b/lambda-layer/otel-handler deleted file mode 100644 index 9104e2a7b2..0000000000 --- a/lambda-layer/otel-handler +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -export OTEL_INSTRUMENTATION_AWS_SDK_EXPERIMENTAL_SPAN_ATTRIBUTES=true - -export OTEL_PROPAGATORS="${OTEL_PROPAGATORS:-xray,tracecontext,b3,b3multi}" - -# Temporarily set OTEL_SERVICE_NAME variable to work around but in javaagent not handling -# OTEL_RESOURCE_ATTRIBUTES as set in otel-handler-upstream. It doesn't hurt to apply this -# to wrapper as well. -# TODO(anuraaga): Move to opentelemetry-lambda -export OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME:-${AWS_LAMBDA_FUNCTION_NAME}} - -export JAVA_TOOL_OPTIONS="-javaagent:/opt/opentelemetry-javaagent.jar ${JAVA_TOOL_OPTIONS}" - -if [[ $OTEL_RESOURCE_ATTRIBUTES != *"service.name="* ]]; then - export OTEL_RESOURCE_ATTRIBUTES="service.name=${AWS_LAMBDA_FUNCTION_NAME},${OTEL_RESOURCE_ATTRIBUTES}" -fi - -export OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT=10000 - -# Disable the Application Signals runtime metrics since we are on Lambda -export OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED=false - -exec "$@" diff --git a/lambda-layer/otel-instrument b/lambda-layer/otel-instrument new file mode 100644 index 0000000000..450eb925a5 --- /dev/null +++ b/lambda-layer/otel-instrument @@ -0,0 +1,38 @@ +#!/bin/bash + +export OTEL_INSTRUMENTATION_AWS_SDK_EXPERIMENTAL_SPAN_ATTRIBUTES=true + +export OTEL_PROPAGATORS="${OTEL_PROPAGATORS:-xray,tracecontext,b3,b3multi}" + +export OTEL_SERVICE_NAME=${OTEL_SERVICE_NAME:-${AWS_LAMBDA_FUNCTION_NAME}} + +export JAVA_TOOL_OPTIONS="-javaagent:/opt/opentelemetry-javaagent.jar ${JAVA_TOOL_OPTIONS}" + +if [[ $OTEL_RESOURCE_ATTRIBUTES != *"service.name="* ]]; then + export OTEL_RESOURCE_ATTRIBUTES="service.name=${AWS_LAMBDA_FUNCTION_NAME},${OTEL_RESOURCE_ATTRIBUTES}" +fi + +export OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT=10000 + +# Disable the Application Signals runtime metrics since we are on Lambda +export OTEL_AWS_APPLICATION_SIGNALS_RUNTIME_ENABLED=false + +# Use OTLP traces exporter if not specified +export OTEL_TRACES_EXPORTER=${OTEL_TRACES_EXPORTER:-"otlp"} + +# Disable metrics and logs export by default if not specified +export OTEL_METRICS_EXPORTER=${OTEL_METRICS_EXPORTER:-"none"} +export OTEL_LOGS_EXPORTER=${OTEL_LOGS_EXPORTER:-"none"} + +# Enable Application Signals by default if not specified +export OTEL_AWS_APPLICATION_SIGNALS_ENABLED=${OTEL_AWS_APPLICATION_SIGNALS_ENABLED:-"true"} + +# Append Lambda Resource Attributes to OTel Resource Attribute List +LAMBDA_RESOURCE_ATTRIBUTES="cloud.region=$AWS_REGION,cloud.provider=aws,faas.name=$AWS_LAMBDA_FUNCTION_NAME,faas.version=$AWS_LAMBDA_FUNCTION_VERSION,faas.instance=$AWS_LAMBDA_LOG_STREAM_NAME,aws.log.group.names=$AWS_LAMBDA_LOG_GROUP_NAME"; +if [ -z "${OTEL_RESOURCE_ATTRIBUTES}" ]; then + export OTEL_RESOURCE_ATTRIBUTES=$LAMBDA_RESOURCE_ATTRIBUTES; +else + export OTEL_RESOURCE_ATTRIBUTES="$LAMBDA_RESOURCE_ATTRIBUTES,$OTEL_RESOURCE_ATTRIBUTES"; +fi + +exec "$@" From c00d3af817b692e0d8fc5a5ec54ae0645d7088a7 Mon Sep 17 00:00:00 2001 From: Michael He <53622546+yiyuan-he@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:25:43 -0800 Subject: [PATCH 10/11] Add Contract Tests for SecretsManager, StepFunctions, and SNS (#958) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Description of changes:* Adding contract tests for new AWS resources. Rebasing the commits in this [PR](https://github.com/aws-observability/aws-otel-java-instrumentation/pull/936/commits/8295fb06341a29e8da4f52bf8b9130f39c2c9bc8) since there were some merge conflicts with Gen AI contract tests. *Test plan:* Screenshot 2024-11-25 at 9 16 39 PM By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --- .../test/awssdk/base/AwsSdkBaseTest.java | 868 +++++++++++++++++- .../test/awssdk/v1/AwsSdkV1Test.java | 80 ++ .../test/awssdk/v2/AwsSdkV2Test.java | 82 +- .../test/utils/AppSignalsConstants.java | 2 + .../utils/SemanticConventionsConstants.java | 5 + .../aws-sdk/aws-sdk-v1/build.gradle.kts | 4 + .../main/java/com/amazon/sampleapp/App.java | 387 ++++++++ .../aws-sdk/aws-sdk-v2/build.gradle.kts | 4 + .../main/java/com/amazon/sampleapp/App.java | 385 ++++++++ 9 files changed, 1810 insertions(+), 7 deletions(-) diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java index 0f8652fb70..278aa4011d 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java @@ -42,7 +42,11 @@ public abstract class AwsSdkBaseTest extends ContractTestBase { LocalStackContainer.Service.S3, LocalStackContainer.Service.DYNAMODB, LocalStackContainer.Service.SQS, - LocalStackContainer.Service.KINESIS) + LocalStackContainer.Service.KINESIS, + LocalStackContainer.Service.SECRETSMANAGER, + LocalStackContainer.Service.IAM, + LocalStackContainer.Service.STEPFUNCTIONS, + LocalStackContainer.Service.SNS) .withEnv("DEFAULT_REGION", "us-west-2") .withNetwork(network) .withEnv("LOCALSTACK_HOST", "127.0.0.1") @@ -102,6 +106,12 @@ protected String getApplicationWaitPattern() { protected abstract String getBedrockAgentRuntimeSpanNamePrefix(); + protected abstract String getSecretsManagerSpanNamePrefix(); + + protected abstract String getStepFunctionsSpanNamePrefix(); + + protected abstract String getSnsSpanNamePrefix(); + protected abstract String getS3RpcServiceName(); protected abstract String getDynamoDbRpcServiceName(); @@ -118,6 +128,12 @@ protected String getApplicationWaitPattern() { protected abstract String getBedrockAgentRuntimeRpcServiceName(); + protected abstract String getSecretsManagerRpcServiceName(); + + protected abstract String getSnsRpcServiceName(); + + protected abstract String getStepFunctionsRpcServiceName(); + private String getS3ServiceName() { return "AWS::S3"; } @@ -150,6 +166,18 @@ private String getBedrockRuntimeServiceName() { return "AWS::BedrockRuntime"; } + private String getSecretsManagerServiceName() { + return "AWS::SecretsManager"; + } + + private String getStepFunctionsServiceName() { + return "AWS::StepFunctions"; + } + + protected String getSnsServiceName() { + return "AWS::SNS"; + } + private String s3SpanName(String operation) { return String.format("%s.%s", getS3SpanNamePrefix(), operation); } @@ -182,10 +210,31 @@ private String bedrockAgentRuntimeSpanName(String operation) { return String.format("%s.%s", getBedrockAgentRuntimeSpanNamePrefix(), operation); } + private String secretsManagerSpanName(String operation) { + return String.format("%s.%s", getSecretsManagerSpanNamePrefix(), operation); + } + + private String stepFunctionsSpanName(String operation) { + return String.format("%s.%s", getStepFunctionsSpanNamePrefix(), operation); + } + + private String snsSpanName(String operation) { + return String.format("%s.%s", getSnsSpanNamePrefix(), operation); + } + protected ThrowingConsumer assertAttribute(String key, String value) { return (attribute) -> { - assertThat(attribute.getKey()).isEqualTo(key); - assertThat(attribute.getValue().getStringValue()).isEqualTo(value); + var actualKey = attribute.getKey(); + var actualValue = attribute.getValue().getStringValue(); + + assertThat(actualKey).isEqualTo(key); + + // We only want to Regex Pattern Match on the Secret Id and Secret Arn + if (actualValue.contains("secret-id")) { + assertThat(actualValue).matches(value); + } else { + assertThat(actualValue).isEqualTo(value); + } }; } @@ -258,6 +307,7 @@ private void assertSpanClientAttributes( String method, String type, String identifier, + String cloudformationIdentifier, String peerName, int peerPort, String url, @@ -276,6 +326,7 @@ private void assertSpanClientAttributes( method, type, identifier, + cloudformationIdentifier, peerName, peerPort, url, @@ -293,6 +344,7 @@ private void assertSpanProducerAttributes( String method, String type, String identifier, + String cloudformationIdentifier, String peerName, int peerPort, String url, @@ -310,6 +362,7 @@ private void assertSpanProducerAttributes( method, type, identifier, + cloudformationIdentifier, peerName, peerPort, url, @@ -358,6 +411,7 @@ private void assertSpanAttributes( String method, String type, String identifier, + String cloudformationIdentifier, String peerName, int peerPort, String url, @@ -371,6 +425,7 @@ private void assertSpanAttributes( var spanAttributes = span.getAttributesList(); assertThat(span.getKind()).isEqualTo(spanKind); assertThat(span.getName()).isEqualTo(spanName); + assertSemanticConventionsAttributes( spanAttributes, rpcService, method, peerName, peerPort, url, statusCode); assertAwsAttributes( @@ -381,6 +436,7 @@ private void assertSpanAttributes( method, type, identifier, + cloudformationIdentifier, awsSpanKind); for (var assertion : extraAssertions) { assertThat(spanAttributes).satisfiesOnlyOnce(assertion); @@ -396,6 +452,7 @@ private void assertAwsAttributes( String operation, String type, String identifier, + String clouformationIdentifier, String spanKind) { var assertions = @@ -406,11 +463,14 @@ private void assertAwsAttributes( .satisfiesOnlyOnce(assertAttribute(AppSignalsConstants.AWS_REMOTE_OPERATION, operation)) .satisfiesOnlyOnce(assertAttribute(AppSignalsConstants.AWS_REMOTE_SERVICE, service)) .satisfiesOnlyOnce(assertAttribute(AppSignalsConstants.AWS_SPAN_KIND, spanKind)); - if (type != null && identifier != null) { + if (type != null && identifier != null && clouformationIdentifier != null) { assertions.satisfiesOnlyOnce( assertAttribute(AppSignalsConstants.AWS_REMOTE_RESOURCE_TYPE, type)); assertions.satisfiesOnlyOnce( assertAttribute(AppSignalsConstants.AWS_REMOTE_RESOURCE_IDENTIFIER, identifier)); + assertions.satisfiesOnlyOnce( + assertAttribute( + AppSignalsConstants.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, clouformationIdentifier)); } } @@ -435,6 +495,7 @@ protected void assertMetricClientAttributes( String method, String type, String identifier, + String cloudformationIdentifier, Double expectedSum) { assertMetricAttributes( resourceScopeMetrics, @@ -446,6 +507,7 @@ protected void assertMetricClientAttributes( method, type, identifier, + cloudformationIdentifier, expectedSum); } @@ -458,6 +520,7 @@ protected void assertMetricProducerAttributes( String method, String type, String identifier, + String cloudformationIdentifier, Double expectedSum) { assertMetricAttributes( resourceScopeMetrics, @@ -469,6 +532,7 @@ protected void assertMetricProducerAttributes( method, type, identifier, + cloudformationIdentifier, expectedSum); } @@ -481,6 +545,7 @@ protected void assertMetricConsumerAttributes( String method, String type, String identifier, + String cloudformationIdentifier, Double expectedSum) { assertMetricAttributes( resourceScopeMetrics, @@ -492,6 +557,7 @@ protected void assertMetricConsumerAttributes( method, type, identifier, + cloudformationIdentifier, expectedSum); } @@ -505,6 +571,7 @@ protected void assertMetricAttributes( String method, String type, String identifier, + String cloudformationIdentifier, Double expectedSum) { assertThat(resourceScopeMetrics) .anySatisfy( @@ -524,6 +591,7 @@ protected void assertMetricAttributes( method, type, identifier, + cloudformationIdentifier, spanKind); if (expectedSum != null) { double actualSum = dataPoint.getSum(); @@ -554,6 +622,7 @@ protected void doTestS3CreateBucket() throws Exception { var localOperation = "GET /s3/createbucket/:bucketname"; var type = "AWS::S3::Bucket"; var identifier = "create-bucket"; + var cloudformationIdentifier = "create-bucket"; assertSpanClientAttributes( traces, @@ -565,6 +634,7 @@ protected void doTestS3CreateBucket() throws Exception { "CreateBucket", type, identifier, + cloudformationIdentifier, "create-bucket.s3.localstack", 4566, "http://create-bucket.s3.localstack:4566", @@ -579,6 +649,7 @@ protected void doTestS3CreateBucket() throws Exception { "CreateBucket", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -589,6 +660,7 @@ protected void doTestS3CreateBucket() throws Exception { "CreateBucket", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -599,6 +671,7 @@ protected void doTestS3CreateBucket() throws Exception { "CreateBucket", type, identifier, + cloudformationIdentifier, 0.0); } @@ -617,6 +690,7 @@ protected void doTestS3CreateObject() throws Exception { var localOperation = "GET /s3/createobject/:bucketname/:objectname"; var type = "AWS::S3::Bucket"; var identifier = "put-object"; + var cloudformationIdentifier = "put-object"; assertSpanClientAttributes( traces, @@ -628,6 +702,7 @@ protected void doTestS3CreateObject() throws Exception { "PutObject", type, identifier, + cloudformationIdentifier, "put-object.s3.localstack", 4566, "http://put-object.s3.localstack:4566", @@ -642,6 +717,7 @@ protected void doTestS3CreateObject() throws Exception { "PutObject", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -652,6 +728,7 @@ protected void doTestS3CreateObject() throws Exception { "PutObject", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -662,6 +739,7 @@ protected void doTestS3CreateObject() throws Exception { "PutObject", type, identifier, + cloudformationIdentifier, 0.0); } @@ -679,6 +757,7 @@ protected void doTestS3GetObject() throws Exception { var localOperation = "GET /s3/getobject/:bucketName/:objectname"; var type = "AWS::S3::Bucket"; var identifier = "get-object"; + var cloudformationIdentifier = "get-object"; assertSpanClientAttributes( traces, @@ -690,6 +769,7 @@ protected void doTestS3GetObject() throws Exception { "GetObject", type, identifier, + cloudformationIdentifier, "get-object.s3.localstack", 4566, "http://get-object.s3.localstack:4566", @@ -704,6 +784,7 @@ protected void doTestS3GetObject() throws Exception { "GetObject", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -714,6 +795,7 @@ protected void doTestS3GetObject() throws Exception { "GetObject", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -724,6 +806,7 @@ protected void doTestS3GetObject() throws Exception { "GetObject", type, identifier, + cloudformationIdentifier, 0.0); } @@ -741,6 +824,7 @@ protected void doTestS3Error() { var localOperation = "GET /s3/error"; var type = "AWS::S3::Bucket"; var identifier = "error-bucket"; + var cloudformationIdentifier = "error-bucket"; assertSpanClientAttributes( traces, @@ -752,6 +836,7 @@ protected void doTestS3Error() { "GetObject", type, identifier, + cloudformationIdentifier, "error-bucket.s3.test", 8080, "http://error-bucket.s3.test:8080", @@ -766,6 +851,7 @@ protected void doTestS3Error() { "GetObject", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -776,6 +862,7 @@ protected void doTestS3Error() { "GetObject", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -786,6 +873,7 @@ protected void doTestS3Error() { "GetObject", type, identifier, + cloudformationIdentifier, 1.0); } @@ -803,6 +891,7 @@ protected void doTestS3Fault() { var localOperation = "GET /s3/fault"; var type = "AWS::S3::Bucket"; var identifier = "fault-bucket"; + var cloudformationIdentifier = "fault-bucket"; assertSpanClientAttributes( traces, @@ -814,6 +903,7 @@ protected void doTestS3Fault() { "GetObject", type, identifier, + cloudformationIdentifier, "fault-bucket.s3.test", 8080, "http://fault-bucket.s3.test:8080", @@ -828,6 +918,7 @@ protected void doTestS3Fault() { "GetObject", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -838,6 +929,7 @@ protected void doTestS3Fault() { "GetObject", type, identifier, + cloudformationIdentifier, 1.0); assertMetricClientAttributes( metrics, @@ -848,6 +940,7 @@ protected void doTestS3Fault() { "GetObject", type, identifier, + cloudformationIdentifier, 0.0); } @@ -873,6 +966,7 @@ protected void doTestDynamoDbCreateTable() { var localOperation = "GET /ddb/createtable/:tablename"; var type = "AWS::DynamoDB::Table"; var identifier = "some-table"; + var cloudformationIdentifier = "some-table"; assertSpanClientAttributes( traces, @@ -884,6 +978,7 @@ protected void doTestDynamoDbCreateTable() { "CreateTable", type, identifier, + cloudformationIdentifier, "localstack", 4566, "http://localstack:4566", @@ -898,6 +993,7 @@ protected void doTestDynamoDbCreateTable() { "CreateTable", type, identifier, + cloudformationIdentifier, 20000.0); assertMetricClientAttributes( metrics, @@ -908,6 +1004,7 @@ protected void doTestDynamoDbCreateTable() { "CreateTable", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -918,6 +1015,7 @@ protected void doTestDynamoDbCreateTable() { "CreateTable", type, identifier, + cloudformationIdentifier, 0.0); } @@ -935,6 +1033,7 @@ protected void doTestDynamoDbPutItem() { var localOperation = "GET /ddb/putitem/:tablename/:partitionkey"; var type = "AWS::DynamoDB::Table"; var identifier = "putitem-table"; + var cloudformationIdentifier = "putitem-table"; assertSpanClientAttributes( traces, @@ -946,6 +1045,7 @@ protected void doTestDynamoDbPutItem() { "PutItem", type, identifier, + cloudformationIdentifier, "localstack", 4566, "http://localstack:4566", @@ -960,6 +1060,7 @@ protected void doTestDynamoDbPutItem() { "PutItem", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -970,6 +1071,7 @@ protected void doTestDynamoDbPutItem() { "PutItem", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -980,6 +1082,7 @@ protected void doTestDynamoDbPutItem() { "PutItem", type, identifier, + cloudformationIdentifier, 0.0); } @@ -997,6 +1100,7 @@ protected void doTestDynamoDbError() throws Exception { var localOperation = "GET /ddb/error"; var type = "AWS::DynamoDB::Table"; var identifier = "nonexistanttable"; + var cloudformationIdentifier = "nonexistanttable"; assertSpanClientAttributes( traces, @@ -1008,6 +1112,7 @@ protected void doTestDynamoDbError() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, "error.test", 8080, "http://error.test:8080", @@ -1022,6 +1127,7 @@ protected void doTestDynamoDbError() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1032,6 +1138,7 @@ protected void doTestDynamoDbError() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1042,6 +1149,7 @@ protected void doTestDynamoDbError() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, 1.0); } @@ -1065,6 +1173,7 @@ protected void doTestDynamoDbFault() throws Exception { var localOperation = "GET /ddb/fault"; var type = "AWS::DynamoDB::Table"; var identifier = "nonexistanttable"; + var cloudformationIdentifier = "nonexistanttable"; assertSpanClientAttributes( traces, @@ -1076,6 +1185,7 @@ protected void doTestDynamoDbFault() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, "fault.test", 8080, "http://fault.test:8080", @@ -1090,6 +1200,7 @@ protected void doTestDynamoDbFault() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, 20000.0); assertMetricClientAttributes( metrics, @@ -1100,6 +1211,7 @@ protected void doTestDynamoDbFault() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, 1.0); assertMetricClientAttributes( metrics, @@ -1110,6 +1222,7 @@ protected void doTestDynamoDbFault() throws Exception { "PutItem", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1127,6 +1240,7 @@ protected void doTestSQSCreateQueue() throws Exception { var localOperation = "GET /sqs/createqueue/:queuename"; var type = "AWS::SQS::Queue"; var identifier = "some-queue"; + var cloudformationIdentifier = "some-queue"; assertSpanClientAttributes( traces, @@ -1138,6 +1252,7 @@ protected void doTestSQSCreateQueue() throws Exception { "CreateQueue", type, identifier, + cloudformationIdentifier, "localstack", 4566, "http://localstack:4566", @@ -1152,6 +1267,7 @@ protected void doTestSQSCreateQueue() throws Exception { "CreateQueue", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1162,6 +1278,7 @@ protected void doTestSQSCreateQueue() throws Exception { "CreateQueue", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1172,6 +1289,7 @@ protected void doTestSQSCreateQueue() throws Exception { "CreateQueue", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1190,6 +1308,7 @@ protected void doTestSQSSendMessage() throws Exception { // SendMessage does not capture aws.queue.name String type = null; String identifier = null; + String cloudformationIdentifier = null; assertSpanProducerAttributes( traces, @@ -1201,6 +1320,7 @@ protected void doTestSQSSendMessage() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, "localstack", 4566, "http://localstack:4566", @@ -1216,6 +1336,7 @@ protected void doTestSQSSendMessage() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricProducerAttributes( metrics, @@ -1226,6 +1347,7 @@ protected void doTestSQSSendMessage() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 0.0); assertMetricProducerAttributes( metrics, @@ -1236,6 +1358,7 @@ protected void doTestSQSSendMessage() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1258,6 +1381,7 @@ protected void doTestSQSReceiveMessage() throws Exception { // ReceiveMessage does not capture aws.queue.name String type = null; String identifier = null; + String cloudformationIdentifier = null; // Consumer traces for SQS behave like a Server span (they create the local aws service // attributes), but have RPC attributes like a client span. assertSpanConsumerAttributes( @@ -1282,6 +1406,7 @@ protected void doTestSQSReceiveMessage() throws Exception { "ReceiveMessage", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricConsumerAttributes( metrics, @@ -1292,6 +1417,7 @@ protected void doTestSQSReceiveMessage() throws Exception { "ReceiveMessage", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1310,6 +1436,7 @@ protected void doTestSQSError() throws Exception { // SendMessage does not capture aws.queue.name String type = null; String identifier = null; + String cloudformationIdentifier = null; assertSpanProducerAttributes( traces, @@ -1321,6 +1448,7 @@ protected void doTestSQSError() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, "error.test", 8080, "http://error.test:8080", @@ -1336,6 +1464,7 @@ protected void doTestSQSError() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricProducerAttributes( metrics, @@ -1346,6 +1475,7 @@ protected void doTestSQSError() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 0.0); assertMetricProducerAttributes( metrics, @@ -1356,6 +1486,7 @@ protected void doTestSQSError() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 1.0); } @@ -1374,6 +1505,7 @@ protected void doTestSQSFault() throws Exception { // SendMessage does not capture aws.queue.name String type = null; String identifier = null; + String cloudformationIdentifier = null; assertSpanProducerAttributes( traces, @@ -1385,6 +1517,7 @@ protected void doTestSQSFault() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, "fault.test", 8080, "http://fault.test:8080", @@ -1400,6 +1533,7 @@ protected void doTestSQSFault() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricProducerAttributes( metrics, @@ -1410,6 +1544,7 @@ protected void doTestSQSFault() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 1.0); assertMetricProducerAttributes( metrics, @@ -1420,6 +1555,7 @@ protected void doTestSQSFault() throws Exception { "SendMessage", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1437,6 +1573,7 @@ protected void doTestKinesisPutRecord() throws Exception { var localOperation = "GET /kinesis/putrecord/:streamname"; var type = "AWS::Kinesis::Stream"; var identifier = "my-stream"; + var cloudformationIdentifier = "my-stream"; assertSpanClientAttributes( traces, @@ -1448,6 +1585,7 @@ protected void doTestKinesisPutRecord() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, "localstack", 4566, "http://localstack:4566", @@ -1462,6 +1600,7 @@ protected void doTestKinesisPutRecord() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1472,6 +1611,7 @@ protected void doTestKinesisPutRecord() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1482,6 +1622,7 @@ protected void doTestKinesisPutRecord() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1499,6 +1640,7 @@ protected void doTestKinesisError() throws Exception { var localOperation = "GET /kinesis/error"; var type = "AWS::Kinesis::Stream"; var identifier = "nonexistantstream"; + var cloudformationIdentifier = "nonexistantstream"; assertSpanClientAttributes( traces, @@ -1510,6 +1652,7 @@ protected void doTestKinesisError() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, "error.test", 8080, "http://error.test:8080", @@ -1525,6 +1668,7 @@ protected void doTestKinesisError() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1535,6 +1679,7 @@ protected void doTestKinesisError() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1545,6 +1690,7 @@ protected void doTestKinesisError() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 1.0); } @@ -1562,6 +1708,7 @@ protected void doTestKinesisFault() throws Exception { var localOperation = "GET /kinesis/fault"; var type = "AWS::Kinesis::Stream"; var identifier = "faultstream"; + var cloudformationIdentifier = "faultstream"; assertSpanClientAttributes( traces, @@ -1573,6 +1720,7 @@ protected void doTestKinesisFault() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, "fault.test", 8080, "http://fault.test:8080", @@ -1587,6 +1735,7 @@ protected void doTestKinesisFault() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1597,6 +1746,7 @@ protected void doTestKinesisFault() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 1.0); assertMetricClientAttributes( metrics, @@ -1607,6 +1757,7 @@ protected void doTestKinesisFault() throws Exception { "PutRecord", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1625,6 +1776,7 @@ protected void doTestBedrockAgentKnowledgeBaseId() { var localOperation = "GET /bedrockagent/getknowledgeBase/:knowledgeBaseId"; String type = "AWS::Bedrock::KnowledgeBase"; String identifier = "knowledge-base-id"; + String cloudformationIdentifier = "knowledge-base-id"; assertSpanClientAttributes( traces, bedrockAgentSpanName("GetKnowledgeBase"), @@ -1635,6 +1787,7 @@ protected void doTestBedrockAgentKnowledgeBaseId() { "GetKnowledgeBase", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1651,6 +1804,7 @@ protected void doTestBedrockAgentKnowledgeBaseId() { "GetKnowledgeBase", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1661,6 +1815,7 @@ protected void doTestBedrockAgentKnowledgeBaseId() { "GetKnowledgeBase", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1671,6 +1826,7 @@ protected void doTestBedrockAgentKnowledgeBaseId() { "GetKnowledgeBase", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1688,6 +1844,7 @@ protected void doTestBedrockAgentAgentId() { var localOperation = "GET /bedrockagent/getagent/:agentId"; String type = "AWS::Bedrock::Agent"; String identifier = "test-agent-id"; + String cloudformationIdentifier = "test-agent-id"; assertSpanClientAttributes( traces, bedrockAgentSpanName("GetAgent"), @@ -1698,6 +1855,7 @@ protected void doTestBedrockAgentAgentId() { "GetAgent", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1712,6 +1870,7 @@ protected void doTestBedrockAgentAgentId() { "GetAgent", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1722,6 +1881,7 @@ protected void doTestBedrockAgentAgentId() { "GetAgent", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1732,6 +1892,7 @@ protected void doTestBedrockAgentAgentId() { "GetAgent", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1749,6 +1910,7 @@ protected void doTestBedrockAgentDataSourceId() { var localOperation = "GET /bedrockagent/get-data-source"; String type = "AWS::Bedrock::DataSource"; String identifier = "nonExistDatasourceId"; + String cloudformationIdentifier = "nonExistDatasourceId"; assertSpanClientAttributes( traces, bedrockAgentSpanName("GetDataSource"), @@ -1759,6 +1921,7 @@ protected void doTestBedrockAgentDataSourceId() { "GetDataSource", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1775,6 +1938,7 @@ protected void doTestBedrockAgentDataSourceId() { "GetDataSource", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1785,6 +1949,7 @@ protected void doTestBedrockAgentDataSourceId() { "GetDataSource", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1795,6 +1960,7 @@ protected void doTestBedrockAgentDataSourceId() { "GetDataSource", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1812,6 +1978,7 @@ protected void doTestBedrockRuntimeAi21Jamba() { var localOperation = "GET /bedrockruntime/invokeModel/ai21Jamba"; String type = "AWS::Bedrock::Model"; String identifier = "ai21.jamba-1-5-mini-v1:0"; + String cloudformationIdentifier = "ai21.jamba-1-5-mini-v1:0"; assertSpanClientAttributes( traces, bedrockRuntimeSpanName("InvokeModel"), @@ -1822,6 +1989,7 @@ protected void doTestBedrockRuntimeAi21Jamba() { "InvokeModel", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1843,6 +2011,7 @@ protected void doTestBedrockRuntimeAi21Jamba() { "InvokeModel", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1853,6 +2022,7 @@ protected void doTestBedrockRuntimeAi21Jamba() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1863,6 +2033,7 @@ protected void doTestBedrockRuntimeAi21Jamba() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1880,6 +2051,7 @@ protected void doTestBedrockRuntimeAmazonTitan() { var localOperation = "GET /bedrockruntime/invokeModel/amazonTitan"; String type = "AWS::Bedrock::Model"; String identifier = "amazon.titan-text-premier-v1:0"; + String cloudformationIdentifier = "amazon.titan-text-premier-v1:0"; assertSpanClientAttributes( traces, bedrockRuntimeSpanName("InvokeModel"), @@ -1890,6 +2062,7 @@ protected void doTestBedrockRuntimeAmazonTitan() { "InvokeModel", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1914,6 +2087,7 @@ protected void doTestBedrockRuntimeAmazonTitan() { "InvokeModel", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1924,6 +2098,7 @@ protected void doTestBedrockRuntimeAmazonTitan() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -1934,6 +2109,7 @@ protected void doTestBedrockRuntimeAmazonTitan() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); } @@ -1952,6 +2128,7 @@ protected void doTestBedrockRuntimeAnthropicClaude() { var localOperation = "GET /bedrockruntime/invokeModel/anthropicClaude"; String type = "AWS::Bedrock::Model"; String identifier = "anthropic.claude-3-haiku-20240307-v1:0"; + String cloudformationIdentifier = "anthropic.claude-3-haiku-20240307-v1:0"; assertSpanClientAttributes( traces, @@ -1963,6 +2140,7 @@ protected void doTestBedrockRuntimeAnthropicClaude() { "InvokeModel", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1987,6 +2165,7 @@ protected void doTestBedrockRuntimeAnthropicClaude() { "InvokeModel", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -1997,6 +2176,7 @@ protected void doTestBedrockRuntimeAnthropicClaude() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2007,6 +2187,7 @@ protected void doTestBedrockRuntimeAnthropicClaude() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); } @@ -2025,6 +2206,7 @@ protected void doTestBedrockRuntimeCohereCommandR() { var localOperation = "GET /bedrockruntime/invokeModel/cohereCommandR"; String type = "AWS::Bedrock::Model"; String identifier = "cohere.command-r-v1:0"; + String cloudformationIdentifier = "cohere.command-r-v1:0"; assertSpanClientAttributes( traces, @@ -2036,6 +2218,7 @@ protected void doTestBedrockRuntimeCohereCommandR() { "InvokeModel", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2059,6 +2242,7 @@ protected void doTestBedrockRuntimeCohereCommandR() { "InvokeModel", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -2069,6 +2253,7 @@ protected void doTestBedrockRuntimeCohereCommandR() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2079,6 +2264,7 @@ protected void doTestBedrockRuntimeCohereCommandR() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); } @@ -2097,6 +2283,7 @@ protected void doTestBedrockRuntimeMetaLlama() { var localOperation = "GET /bedrockruntime/invokeModel/metaLlama"; String type = "AWS::Bedrock::Model"; String identifier = "meta.llama3-70b-instruct-v1:0"; + String cloudformationIdentifier = "meta.llama3-70b-instruct-v1:0"; assertSpanClientAttributes( traces, @@ -2108,6 +2295,7 @@ protected void doTestBedrockRuntimeMetaLlama() { "InvokeModel", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2130,6 +2318,7 @@ protected void doTestBedrockRuntimeMetaLlama() { "InvokeModel", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -2140,6 +2329,7 @@ protected void doTestBedrockRuntimeMetaLlama() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2150,6 +2340,7 @@ protected void doTestBedrockRuntimeMetaLlama() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); } @@ -2168,6 +2359,7 @@ protected void doTestBedrockRuntimeMistral() { var localOperation = "GET /bedrockruntime/invokeModel/mistralAi"; String type = "AWS::Bedrock::Model"; String identifier = "mistral.mistral-large-2402-v1:0"; + String cloudformationIdentifier = "mistral.mistral-large-2402-v1:0"; assertSpanClientAttributes( traces, @@ -2179,6 +2371,7 @@ protected void doTestBedrockRuntimeMistral() { "InvokeModel", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2202,6 +2395,7 @@ protected void doTestBedrockRuntimeMistral() { "InvokeModel", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -2212,6 +2406,7 @@ protected void doTestBedrockRuntimeMistral() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2222,6 +2417,7 @@ protected void doTestBedrockRuntimeMistral() { "InvokeModel", type, identifier, + cloudformationIdentifier, 0.0); } @@ -2239,6 +2435,8 @@ protected void doTestBedrockGuardrailId() { var localOperation = "GET /bedrock/getguardrail"; String type = "AWS::Bedrock::Guardrail"; String identifier = "test-bedrock-guardrail"; + String cloudformationIdentifier = + "arn:aws:bedrock:us-east-1:000000000000:guardrail/test-bedrock-guardrail"; assertSpanClientAttributes( traces, bedrockSpanName("GetGuardrail"), @@ -2249,13 +2447,17 @@ protected void doTestBedrockGuardrailId() { "GetGuardrail", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", 200, List.of( assertAttribute( - SemanticConventionsConstants.AWS_GUARDRAIL_ID, "test-bedrock-guardrail"))); + SemanticConventionsConstants.AWS_GUARDRAIL_ID, "test-bedrock-guardrail"), + assertAttribute( + SemanticConventionsConstants.AWS_GUARDRAIL_ARN, + "arn:aws:bedrock:us-east-1:000000000000:guardrail/test-bedrock-guardrail"))); assertMetricClientAttributes( metrics, AppSignalsConstants.LATENCY_METRIC, @@ -2265,6 +2467,7 @@ protected void doTestBedrockGuardrailId() { "GetGuardrail", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -2275,6 +2478,7 @@ protected void doTestBedrockGuardrailId() { "GetGuardrail", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2285,6 +2489,7 @@ protected void doTestBedrockGuardrailId() { "GetGuardrail", type, identifier, + cloudformationIdentifier, 0.0); } @@ -2302,6 +2507,7 @@ protected void doTestBedrockAgentRuntimeAgentId() { var localOperation = "GET /bedrockagentruntime/getmemory/:agentId"; String type = "AWS::Bedrock::Agent"; String identifier = "test-agent-id"; + String cloudformationIdentifier = "test-agent-id"; assertSpanClientAttributes( traces, bedrockAgentRuntimeSpanName("GetAgentMemory"), @@ -2312,6 +2518,7 @@ protected void doTestBedrockAgentRuntimeAgentId() { "GetAgentMemory", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2326,6 +2533,7 @@ protected void doTestBedrockAgentRuntimeAgentId() { "GetAgentMemory", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -2336,6 +2544,7 @@ protected void doTestBedrockAgentRuntimeAgentId() { "GetAgentMemory", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2346,6 +2555,7 @@ protected void doTestBedrockAgentRuntimeAgentId() { "GetAgentMemory", type, identifier, + cloudformationIdentifier, 0.0); } @@ -2364,6 +2574,7 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { var localOperation = "GET /bedrockagentruntime/retrieve/:knowledgeBaseId"; String type = "AWS::Bedrock::KnowledgeBase"; String identifier = "test-knowledge-base-id"; + String cloudformationIdentifier = "test-knowledge-base-id"; assertSpanClientAttributes( traces, bedrockAgentRuntimeSpanName("Retrieve"), @@ -2374,6 +2585,7 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { "Retrieve", type, identifier, + cloudformationIdentifier, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2390,6 +2602,7 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { "Retrieve", type, identifier, + cloudformationIdentifier, 5000.0); assertMetricClientAttributes( metrics, @@ -2400,6 +2613,7 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { "Retrieve", type, identifier, + cloudformationIdentifier, 0.0); assertMetricClientAttributes( metrics, @@ -2410,6 +2624,650 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { "Retrieve", type, identifier, + cloudformationIdentifier, + 0.0); + } + + protected void doTestSecretsManagerDescribeSecret() throws Exception { + appClient.get("/secretsmanager/describesecret/test-secret-id").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /secretsmanager/describesecret/:secretId"; + var type = "AWS::SecretsManager::Secret"; + var identifier = "test-secret-id-[A-Za-z0-9]{6}"; + var cloudformationIdentifier = + "arn:aws:secretsmanager:us-west-2:000000000000:secret:test-secret-id-[A-Za-z0-9]{6}"; + assertSpanClientAttributes( + traces, + secretsManagerSpanName("DescribeSecret"), + getSecretsManagerRpcServiceName(), + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + type, + identifier, + cloudformationIdentifier, + "localstack", + 4566, + "http://localstack:4566", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.AWS_SECRET_ARN, + "arn:aws:secretsmanager:us-west-2:000000000000:secret:test-secret-id-[A-Za-z0-9]{6}"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + type, + identifier, + cloudformationIdentifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + type, + identifier, + cloudformationIdentifier, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + type, + identifier, + cloudformationIdentifier, + 0.0); + } + + protected void doTestSecretsManagerError() throws Exception { + appClient.get("/secretsmanager/error").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /secretsmanager/error"; + assertSpanClientAttributes( + traces, + secretsManagerSpanName("DescribeSecret"), + getSecretsManagerRpcServiceName(), + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + "error.test", + 8080, + "http://error.test:8080", + 400, + List.of()); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + 1.0); + } + + protected void doTestSecretsManagerFault() throws Exception { + appClient.get("/secretsmanager/fault").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /secretsmanager/fault"; + assertSpanClientAttributes( + traces, + secretsManagerSpanName("DescribeSecret"), + getSecretsManagerRpcServiceName(), + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + "fault.test", + 8080, + "http://fault.test:8080", + 500, + List.of()); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + 1.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getSecretsManagerServiceName(), + "DescribeSecret", + null, + null, + null, + 0.0); + } + + protected void doTestStepFunctionsDescribeStateMachine() throws Exception { + appClient.get("/sfn/describestatemachine/test-state-machine").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sfn/describestatemachine/:name"; + var type = "AWS::StepFunctions::StateMachine"; + var identifier = "test-state-machine"; + var cloudformationIdentifier = + "arn:aws:states:us-west-2:000000000000:stateMachine:test-state-machine"; + + assertSpanClientAttributes( + traces, + stepFunctionsSpanName("DescribeStateMachine"), + getStepFunctionsRpcServiceName(), + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeStateMachine", + type, + identifier, + cloudformationIdentifier, + "localstack", + 4566, + "http://localstack:4566", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.AWS_STATE_MACHINE_ARN, + "arn:aws:states:us-west-2:000000000000:stateMachine:test-state-machine"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeStateMachine", + type, + identifier, + cloudformationIdentifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeStateMachine", + type, + identifier, + cloudformationIdentifier, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeStateMachine", + type, + identifier, + cloudformationIdentifier, + 0.0); + } + + protected void doTestStepFunctionsDescribeActivity() throws Exception { + appClient.get("/sfn/describeactivity/test-activity").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sfn/describeactivity/:name"; + var type = "AWS::StepFunctions::Activity"; + var identifier = "test-activity"; + var cloudformationIdentifier = "arn:aws:states:us-west-2:000000000000:activity:test-activity"; + + assertSpanClientAttributes( + traces, + stepFunctionsSpanName("DescribeActivity"), + getStepFunctionsRpcServiceName(), + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + "localstack", + 4566, + "http://localstack:4566", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.AWS_ACTIVITY_ARN, + "arn:aws:states:us-west-2:000000000000:activity:test-activity"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 0.0); + } + + protected void doTestStepFunctionsError() throws Exception { + appClient.get("/sfn/error").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sfn/error"; + var type = "AWS::StepFunctions::Activity"; + var identifier = "nonexistent-activity"; + var cloudformationIdentifier = + "arn:aws:states:us-west-2:000000000000:activity:nonexistent-activity"; + + assertSpanClientAttributes( + traces, + stepFunctionsSpanName("DescribeActivity"), + getStepFunctionsRpcServiceName(), + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + "error.test", + 8080, + "http://error.test:8080", + 400, + List.of( + assertAttribute( + SemanticConventionsConstants.AWS_ACTIVITY_ARN, + "arn:aws:states:us-west-2:000000000000:activity:nonexistent-activity"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 1.0); + } + + protected void doTestStepFunctionsFault() throws Exception { + appClient.get("/sfn/fault").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sfn/fault"; + var type = "AWS::StepFunctions::Activity"; + var identifier = "fault-activity"; + var cloudformationIdentifier = "arn:aws:states:us-west-2:000000000000:activity:fault-activity"; + + assertSpanClientAttributes( + traces, + stepFunctionsSpanName("DescribeActivity"), + getStepFunctionsRpcServiceName(), + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + "fault.test", + 8080, + "http://fault.test:8080", + 500, + List.of( + assertAttribute( + SemanticConventionsConstants.AWS_ACTIVITY_ARN, + "arn:aws:states:us-west-2:000000000000:activity:fault-activity"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 1.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getStepFunctionsServiceName(), + "DescribeActivity", + type, + identifier, + cloudformationIdentifier, + 0.0); + } + + protected void doTestSnsGetTopicAttributes() throws Exception { + appClient.get("/sns/gettopicattributes/test-topic").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sns/gettopicattributes/:topicId"; + var type = "AWS::SNS::Topic"; + var identifier = "test-topic"; + var cloudformationIdentifier = "arn:aws:sns:us-west-2:000000000000:test-topic"; + + assertSpanClientAttributes( + traces, + snsSpanName("GetTopicAttributes"), + getSnsRpcServiceName(), + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + type, + identifier, + cloudformationIdentifier, + "localstack", + 4566, + "http://localstack:4566", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.AWS_TOPIC_ARN, + "arn:aws:sns:us-west-2:000000000000:test-topic"))); + } + + protected void doTestSnsError() throws Exception { + appClient.get("/sns/error").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sns/error"; + assertSpanClientAttributes( + traces, + snsSpanName("GetTopicAttributes"), + getSnsRpcServiceName(), + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + "error.test", + 8080, + "http://error.test:8080", + 400, + List.of()); + + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + 5000.0); + + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + 0.0); + + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + 1.0); + } + + protected void doTestSnsFault() throws Exception { + appClient.get("/sns/fault").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /sns/fault"; + assertSpanClientAttributes( + traces, + snsSpanName("GetTopicAttributes"), + getSnsRpcServiceName(), + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + "fault.test", + 8080, + "http://fault.test:8080", + 500, + List.of()); + + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + 5000.0); + + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, + 1.0); + + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getSnsServiceName(), + "GetTopicAttributes", + null, + null, + null, 0.0); } } diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java index fa5a586c5d..5a53b83a1e 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java @@ -76,6 +76,21 @@ protected String getBedrockAgentRuntimeSpanNamePrefix() { return "AWSBedrockAgentRuntime"; } + @Override + protected String getSecretsManagerSpanNamePrefix() { + return "AWSSecretsManager"; + } + + @Override + protected String getStepFunctionsSpanNamePrefix() { + return "AWSStepFunctions"; + } + + @Override + protected String getSnsSpanNamePrefix() { + return "SNS"; + } + protected String getS3RpcServiceName() { return "Amazon S3"; } @@ -90,6 +105,21 @@ protected String getSqsRpcServiceName() { return "AmazonSQS"; } + @Override + protected String getSecretsManagerRpcServiceName() { + return "AWSSecretsManager"; + } + + @Override + protected String getStepFunctionsRpcServiceName() { + return "AWSStepFunctions"; + } + + @Override + protected String getSnsRpcServiceName() { + return "AmazonSNS"; + } + protected String getKinesisRpcServiceName() { return "AmazonKinesis"; } @@ -260,4 +290,54 @@ void testBedrockAgentRuntimeAgentId() { void testBedrockAgentRuntimeKnowledgeBaseId() { doTestBedrockAgentRuntimeKnowledgeBaseId(); } + + @Test + void testSecretsManagerDescribeSecret() throws Exception { + doTestSecretsManagerDescribeSecret(); + } + + @Test + void testSecretsManagerError() throws Exception { + doTestSecretsManagerError(); + } + + @Test + void testSecretsManagerFault() throws Exception { + doTestSecretsManagerFault(); + } + + @Test + void testStepFunctionsDescribeStateMachine() throws Exception { + doTestStepFunctionsDescribeStateMachine(); + } + + @Test + void testStepFunctionsDescribeActivity() throws Exception { + doTestStepFunctionsDescribeActivity(); + } + + @Test + void testStepFunctionsError() throws Exception { + doTestStepFunctionsError(); + } + + @Test + void testStepFunctionsFault() throws Exception { + doTestStepFunctionsFault(); + } + + @Test + void testSnsGetTopicAttributes() throws Exception { + doTestSnsGetTopicAttributes(); + } + + @Test + void testSnsError() throws Exception { + doTestStepFunctionsError(); + } + + @Test + void testSnsFault() throws Exception { + doTestStepFunctionsFault(); + } } diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java index 46c6b7e425..c1259ca6cc 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java @@ -75,6 +75,21 @@ protected String getBedrockAgentRuntimeSpanNamePrefix() { return "BedrockAgentRuntime"; } + @Override + protected String getSecretsManagerSpanNamePrefix() { + return "SecretsManager"; + } + + @Override + protected String getStepFunctionsSpanNamePrefix() { + return "Sfn"; + } + + @Override + protected String getSnsSpanNamePrefix() { + return "Sns"; + } + @Override protected String getS3RpcServiceName() { return "S3"; @@ -114,6 +129,21 @@ protected String getBedrockAgentRuntimeRpcServiceName() { return "BedrockAgentRuntime"; } + @Override + protected String getSecretsManagerRpcServiceName() { + return "SecretsManager"; + } + + @Override + protected String getStepFunctionsRpcServiceName() { + return "Sfn"; + } + + @Override + protected String getSnsRpcServiceName() { + return "Sns"; + } + @Test void testS3CreateBucket() throws Exception { doTestS3CreateBucket(); @@ -259,10 +289,58 @@ void testBedrockAgentRuntimeAgentId() { doTestBedrockAgentRuntimeAgentId(); } - // TODO: Enable testBedrockAgentRuntimeKnowledgeBaseId test after KnowledgeBaseId is supported in - // OTEL BedrockAgentRuntime instrumentation @Test void testBedrockAgentRuntimeKnowledgeBaseId() { doTestBedrockAgentRuntimeKnowledgeBaseId(); } + + @Test + void testSecretsManagerDescribeSecret() throws Exception { + doTestSecretsManagerDescribeSecret(); + } + + @Test + void testSecretsManagerError() throws Exception { + doTestSecretsManagerError(); + } + + @Test + void testSecretsManagerFault() throws Exception { + doTestSecretsManagerFault(); + } + + @Test + void testStepFunctionsDescribeStateMachine() throws Exception { + doTestStepFunctionsDescribeStateMachine(); + } + + @Test + void testStepFunctionsDescribeActivity() throws Exception { + doTestStepFunctionsDescribeActivity(); + } + + @Test + void testStepFunctionsError() throws Exception { + doTestStepFunctionsError(); + } + + @Test + void testStepFunctionsFault() throws Exception { + doTestStepFunctionsFault(); + } + + @Test + void testSnsGetTopicAttributes() throws Exception { + doTestSnsGetTopicAttributes(); + } + + @Test + void testSnsError() throws Exception { + doTestStepFunctionsError(); + } + + @Test + void testSnsFault() throws Exception { + doTestStepFunctionsFault(); + } } diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/AppSignalsConstants.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/AppSignalsConstants.java index 675b69032b..0ff11305c2 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/AppSignalsConstants.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/AppSignalsConstants.java @@ -31,6 +31,8 @@ public class AppSignalsConstants { public static final String AWS_REMOTE_OPERATION = "aws.remote.operation"; public static final String AWS_REMOTE_RESOURCE_TYPE = "aws.remote.resource.type"; public static final String AWS_REMOTE_RESOURCE_IDENTIFIER = "aws.remote.resource.identifier"; + public static final String AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER = + "aws.remote.resource.cfn.primary.identifier"; public static final String AWS_SPAN_KIND = "aws.span.kind"; public static final String AWS_REMOTE_DB_USER = "aws.remote.db.user"; diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java index f0cac6da46..51077ea6a1 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java @@ -62,6 +62,7 @@ public class SemanticConventionsConstants { public static final String AWS_DATA_SOURCE_ID = "aws.bedrock.data_source.id"; public static final String AWS_AGENT_ID = "aws.bedrock.agent.id"; public static final String AWS_GUARDRAIL_ID = "aws.bedrock.guardrail.id"; + public static final String AWS_GUARDRAIL_ARN = "aws.bedrock.guardrail.arn"; public static final String GEN_AI_REQUEST_MODEL = "gen_ai.request.model"; public static final String GEN_AI_REQUEST_MAX_TOKENS = "gen_ai.request.max_tokens"; public static final String GEN_AI_REQUEST_TEMPERATURE = "gen_ai.request.temperature"; @@ -69,6 +70,10 @@ public class SemanticConventionsConstants { public static final String GEN_AI_RESPONSE_FINISH_REASONS = "gen_ai.response.finish_reasons"; public static final String GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens"; public static final String GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens"; + public static final String AWS_SECRET_ARN = "aws.secretsmanager.secret.arn"; + public static final String AWS_STATE_MACHINE_ARN = "aws.stepfunctions.state_machine.arn"; + public static final String AWS_ACTIVITY_ARN = "aws.stepfunctions.activity.arn"; + public static final String AWS_TOPIC_ARN = "aws.sns.topic.arn"; // kafka public static final String MESSAGING_CLIENT_ID = "messaging.client_id"; diff --git a/appsignals-tests/images/aws-sdk/aws-sdk-v1/build.gradle.kts b/appsignals-tests/images/aws-sdk/aws-sdk-v1/build.gradle.kts index 6ee3f0cae1..77fad5427c 100644 --- a/appsignals-tests/images/aws-sdk/aws-sdk-v1/build.gradle.kts +++ b/appsignals-tests/images/aws-sdk/aws-sdk-v1/build.gradle.kts @@ -33,6 +33,10 @@ dependencies { implementation("com.amazonaws:aws-java-sdk-dynamodb") implementation("com.amazonaws:aws-java-sdk-sqs") implementation("com.amazonaws:aws-java-sdk-kinesis") + implementation("com.amazonaws:aws-java-sdk-secretsmanager") + implementation("com.amazonaws:aws-java-sdk-iam") + implementation("com.amazonaws:aws-java-sdk-stepfunctions") + implementation("com.amazonaws:aws-java-sdk-sns") implementation("com.amazonaws:aws-java-sdk-bedrock") implementation("com.amazonaws:aws-java-sdk-bedrockagent") implementation("com.amazonaws:aws-java-sdk-bedrockruntime") diff --git a/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java b/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java index ad5f7a73b4..6b39559b0d 100644 --- a/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java +++ b/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java @@ -44,6 +44,9 @@ import com.amazonaws.services.dynamodbv2.model.KeyType; import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; import com.amazonaws.services.dynamodbv2.model.PutItemRequest; +import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient; +import com.amazonaws.services.identitymanagement.model.CreateRoleRequest; +import com.amazonaws.services.identitymanagement.model.PutRolePolicyRequest; import com.amazonaws.services.kinesis.AmazonKinesisClient; import com.amazonaws.services.kinesis.model.CreateStreamRequest; import com.amazonaws.services.kinesis.model.PutRecordRequest; @@ -52,10 +55,30 @@ import com.amazonaws.services.s3.model.GetObjectRequest; import com.amazonaws.services.s3.model.PutObjectRequest; import com.amazonaws.services.s3.model.Region; +import com.amazonaws.services.secretsmanager.AWSSecretsManagerClient; +import com.amazonaws.services.secretsmanager.model.CreateSecretRequest; +import com.amazonaws.services.secretsmanager.model.DescribeSecretRequest; +import com.amazonaws.services.secretsmanager.model.ListSecretsRequest; +import com.amazonaws.services.secretsmanager.model.SecretListEntry; +import com.amazonaws.services.sns.AmazonSNSClient; +import com.amazonaws.services.sns.model.CreateTopicRequest; +import com.amazonaws.services.sns.model.GetTopicAttributesRequest; +import com.amazonaws.services.sns.model.ListTopicsRequest; +import com.amazonaws.services.sns.model.Topic; import com.amazonaws.services.sqs.AmazonSQSClient; import com.amazonaws.services.sqs.model.CreateQueueRequest; import com.amazonaws.services.sqs.model.ReceiveMessageRequest; import com.amazonaws.services.sqs.model.SendMessageRequest; +import com.amazonaws.services.stepfunctions.AWSStepFunctionsClient; +import com.amazonaws.services.stepfunctions.model.ActivityListItem; +import com.amazonaws.services.stepfunctions.model.CreateActivityRequest; +import com.amazonaws.services.stepfunctions.model.CreateStateMachineRequest; +import com.amazonaws.services.stepfunctions.model.DescribeActivityRequest; +import com.amazonaws.services.stepfunctions.model.DescribeStateMachineRequest; +import com.amazonaws.services.stepfunctions.model.ListActivitiesRequest; +import com.amazonaws.services.stepfunctions.model.ListStateMachinesRequest; +import com.amazonaws.services.stepfunctions.model.StateMachineListItem; +import com.amazonaws.services.stepfunctions.model.StateMachineType; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.io.IOException; @@ -125,6 +148,9 @@ public static void main(String[] args) throws IOException, InterruptedException setupS3(); setupSqs(); setupKinesis(); + setupSecretsManager(); + setupStepFunctions(); + setupSns(); setupBedrock(); // Add this log line so that we only start testing after all routes are configured. @@ -518,6 +544,367 @@ private static void setupS3() { }); } + private static void setupSecretsManager() { + var secretsManagerClient = + AWSSecretsManagerClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration(endpointConfiguration) + .build(); + var secretName = "test-secret-id"; + String existingSecretArn = null; + try { + var listRequest = new ListSecretsRequest(); + var listResponse = secretsManagerClient.listSecrets(listRequest); + existingSecretArn = + listResponse.getSecretList().stream() + .filter(secret -> secret.getName().contains(secretName)) + .findFirst() + .map(SecretListEntry::getARN) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing secrets", e); + } + + if (existingSecretArn != null) { + logger.debug("Secret already exists, skipping creation"); + } else { + logger.info("Secret not found, creating new one"); + var createSecretRequest = new CreateSecretRequest().withName(secretName); + var createSecretResponse = secretsManagerClient.createSecret(createSecretRequest); + existingSecretArn = createSecretResponse.getARN(); + } + + String finalExistingSecretArn = existingSecretArn; + get( + "/secretsmanager/describesecret/:secretId", + (req, res) -> { + var describeRequest = new DescribeSecretRequest().withSecretId(finalExistingSecretArn); + secretsManagerClient.describeSecret(describeRequest); + return ""; + }); + + get( + "/secretsmanager/error", + (req, res) -> { + setMainStatus(400); + var errorClient = + AWSSecretsManagerClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration( + new EndpointConfiguration( + "http://error.test:8080", Regions.US_WEST_2.getName())) + .build(); + + try { + var describeRequest = + new DescribeSecretRequest() + .withSecretId( + "arn:aws:secretsmanager:us-west-2:000000000000:secret:nonexistent-secret-id"); + errorClient.describeSecret(describeRequest); + } catch (Exception e) { + logger.debug("Error describing secret", e); + } + return ""; + }); + + get( + "/secretsmanager/fault", + (req, res) -> { + setMainStatus(500); + var faultClient = + AWSSecretsManagerClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration( + new EndpointConfiguration( + "http://fault.test:8080", Regions.US_WEST_2.getName())) + .build(); + + try { + var describeRequest = + new DescribeSecretRequest() + .withSecretId( + "arn:aws:secretsmanager:us-west-2:000000000000:secret:fault-secret-id"); + faultClient.describeSecret(describeRequest); + } catch (Exception e) { + logger.debug("Error describing secret", e); + } + return ""; + }); + } + + private static void setupStepFunctions() { + var stepFunctionsClient = + AWSStepFunctionsClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration(endpointConfiguration) + .build(); + var iamClient = + AmazonIdentityManagementClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration(endpointConfiguration) + .build(); + + var sfnName = "test-state-machine"; + String existingStateMachineArn = null; + try { + var listRequest = new ListStateMachinesRequest(); + var listResponse = stepFunctionsClient.listStateMachines(listRequest); + existingStateMachineArn = + listResponse.getStateMachines().stream() + .filter(machine -> machine.getName().equals(sfnName)) + .findFirst() + .map(StateMachineListItem::getStateMachineArn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing state machines", e); + } + + if (existingStateMachineArn != null) { + logger.debug("State machine already exists, skipping creation"); + } else { + logger.debug("State machine not found, creating new one"); + String trustPolicy = + "{" + + "\"Version\": \"2012-10-17\"," + + "\"Statement\": [" + + " {" + + " \"Effect\": \"Allow\"," + + " \"Principal\": {" + + " \"Service\": \"states.amazonaws.com\"" + + " }," + + " \"Action\": \"sts:AssumeRole\"" + + " }" + + "]}"; + var roleRequest = + new CreateRoleRequest() + .withRoleName(sfnName + "-role") + .withAssumeRolePolicyDocument(trustPolicy); + var roleArn = iamClient.createRole(roleRequest).getRole().getArn(); + String policyDocument = + "{" + + "\"Version\": \"2012-10-17\"," + + "\"Statement\": [" + + " {" + + " \"Effect\": \"Allow\"," + + " \"Action\": [" + + " \"lambda:InvokeFunction\"" + + " ]," + + " \"Resource\": [" + + " \"*\"" + + " ]" + + " }" + + "]}"; + var policyRequest = + new PutRolePolicyRequest() + .withRoleName(sfnName + "-role") + .withPolicyName(sfnName + "-policy") + .withPolicyDocument(policyDocument); + iamClient.putRolePolicy(policyRequest); + String stateMachineDefinition = + "{" + + " \"Comment\": \"A Hello World example of the Amazon States Language using a Pass state\"," + + " \"StartAt\": \"HelloWorld\"," + + " \"States\": {" + + " \"HelloWorld\": {" + + " \"Type\": \"Pass\"," + + " \"Result\": \"Hello World!\"," + + " \"End\": true" + + " }" + + " }" + + "}"; + var sfnRequest = + new CreateStateMachineRequest() + .withName(sfnName) + .withRoleArn(roleArn) + .withDefinition(stateMachineDefinition) + .withType(StateMachineType.STANDARD); + var createResponse = stepFunctionsClient.createStateMachine(sfnRequest); + existingStateMachineArn = createResponse.getStateMachineArn(); + } + + var activityName = "test-activity"; + String existingActivityArn = null; + try { + var listRequest = new ListActivitiesRequest(); + var listResponse = stepFunctionsClient.listActivities(listRequest); + existingActivityArn = + listResponse.getActivities().stream() + .filter(activity -> activity.getName().equals(activityName)) + .findFirst() + .map(ActivityListItem::getActivityArn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing activities", e); + } + + if (existingActivityArn != null) { + logger.debug("Activity already exists, skipping creation"); + } else { + logger.debug("Activity does not exist, creating new one"); + var createRequest = new CreateActivityRequest().withName(activityName); + var createResponse = stepFunctionsClient.createActivity(createRequest); + existingActivityArn = createResponse.getActivityArn(); + } + + String finalExistingStateMachineArn = existingStateMachineArn; + String finalExistingActivityArn = existingActivityArn; + + get( + "/sfn/describestatemachine/:name", + (req, res) -> { + var describeRequest = + new DescribeStateMachineRequest().withStateMachineArn(finalExistingStateMachineArn); + stepFunctionsClient.describeStateMachine(describeRequest); + return ""; + }); + + get( + "/sfn/describeactivity/:name", + (req, res) -> { + var describeRequest = + new DescribeActivityRequest().withActivityArn(finalExistingActivityArn); + stepFunctionsClient.describeActivity(describeRequest); + return ""; + }); + + get( + "/sfn/error", + (req, res) -> { + setMainStatus(400); + var errorClient = + AWSStepFunctionsClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration( + new EndpointConfiguration( + "http://error.test:8080", Regions.US_WEST_2.getName())) + .build(); + + try { + var describeRequest = + new DescribeActivityRequest() + .withActivityArn( + "arn:aws:states:us-west-2:000000000000:activity:nonexistent-activity"); + errorClient.describeActivity(describeRequest); + } catch (Exception e) { + logger.error("Error describing activity", e); + } + return ""; + }); + + get( + "/sfn/fault", + (req, res) -> { + setMainStatus(500); + var faultClient = + AWSStepFunctionsClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration( + new EndpointConfiguration( + "http://fault.test:8080", Regions.US_WEST_2.getName())) + .build(); + + try { + var describeRequest = + new DescribeActivityRequest() + .withActivityArn( + "arn:aws:states:us-west-2:000000000000:activity:fault-activity"); + faultClient.describeActivity(describeRequest); + } catch (Exception e) { + logger.error("Error describing activity", e); + } + return ""; + }); + } + + private static void setupSns() { + var snsClient = + AmazonSNSClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration(endpointConfiguration) + .build(); + + var topicName = "test-topic"; + String existingTopicArn = null; + + try { + var listTopicsRequest = new ListTopicsRequest(); + var listTopicsResult = snsClient.listTopics(listTopicsRequest); + existingTopicArn = + listTopicsResult.getTopics().stream() + .filter(topic -> topic.getTopicArn().contains(topicName)) + .findFirst() + .map(Topic::getTopicArn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing topics", e); + } + + if (existingTopicArn != null) { + logger.debug("Topic already exists, skipping creation"); + } else { + logger.debug("Topic does not exist, creating new one"); + var createTopicRequest = new CreateTopicRequest().withName(topicName); + var createTopicResult = snsClient.createTopic(createTopicRequest); + existingTopicArn = createTopicResult.getTopicArn(); + } + + String finalExistingTopicArn = existingTopicArn; + get( + "/sns/gettopicattributes/:topicId", + (req, res) -> { + var getTopicAttributesRequest = + new GetTopicAttributesRequest().withTopicArn(finalExistingTopicArn); + snsClient.getTopicAttributes(getTopicAttributesRequest); + return ""; + }); + + get( + "/sns/error", + (req, res) -> { + setMainStatus(400); + var errorClient = + AmazonSNSClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration( + new EndpointConfiguration( + "https://error.test:8080", Regions.US_WEST_2.getName())) + .build(); + + try { + var getTopicAttributesRequest = + new GetTopicAttributesRequest() + .withTopicArn("arn:aws:sns:us-west-2:000000000000:nonexistent-topic"); + errorClient.getTopicAttributes(getTopicAttributesRequest); + } catch (Exception e) { + logger.error("Error describing topic", e); + } + return ""; + }); + + get( + "/sns/fault", + (req, res) -> { + setMainStatus(500); + var faultClient = + AmazonSNSClient.builder() + .withCredentials(CREDENTIALS_PROVIDER) + .withEndpointConfiguration( + new EndpointConfiguration( + "http://fault.test:8080", Regions.US_WEST_2.getName())) + .build(); + + try { + var getTopicAttributesRequest = + new GetTopicAttributesRequest() + .withTopicArn("arn:aws:sns:us-west-2:000000000000:fault-topic"); + faultClient.getTopicAttributes(getTopicAttributesRequest); + } catch (Exception e) { + logger.error("Error describing topic", e); + } + return ""; + }); + } + private static void setupBedrock() { // Localstack does not support Bedrock related services. // We point all Bedrock related request endpoints to the local app, diff --git a/appsignals-tests/images/aws-sdk/aws-sdk-v2/build.gradle.kts b/appsignals-tests/images/aws-sdk/aws-sdk-v2/build.gradle.kts index e50f70772f..0ba17450d6 100644 --- a/appsignals-tests/images/aws-sdk/aws-sdk-v2/build.gradle.kts +++ b/appsignals-tests/images/aws-sdk/aws-sdk-v2/build.gradle.kts @@ -33,6 +33,10 @@ dependencies { implementation("software.amazon.awssdk:dynamodb") implementation("software.amazon.awssdk:sqs") implementation("software.amazon.awssdk:kinesis") + implementation("software.amazon.awssdk:secretsmanager") + implementation("software.amazon.awssdk:iam") + implementation("software.amazon.awssdk:sfn") + implementation("software.amazon.awssdk:sns") implementation("software.amazon.awssdk:bedrock") implementation("software.amazon.awssdk:bedrockagent") implementation("software.amazon.awssdk:bedrockruntime") diff --git a/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java b/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java index 2982135fd4..c96b762dfd 100644 --- a/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java +++ b/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java @@ -60,6 +60,9 @@ import software.amazon.awssdk.services.dynamodb.model.KeyType; import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.iam.IamClient; +import software.amazon.awssdk.services.iam.model.CreateRoleRequest; +import software.amazon.awssdk.services.iam.model.PutRolePolicyRequest; import software.amazon.awssdk.services.kinesis.KinesisClient; import software.amazon.awssdk.services.kinesis.model.CreateStreamRequest; import software.amazon.awssdk.services.kinesis.model.DescribeStreamRequest; @@ -68,6 +71,26 @@ import software.amazon.awssdk.services.s3.model.CreateBucketRequest; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest; +import software.amazon.awssdk.services.secretsmanager.model.DescribeSecretRequest; +import software.amazon.awssdk.services.secretsmanager.model.ListSecretsRequest; +import software.amazon.awssdk.services.secretsmanager.model.SecretListEntry; +import software.amazon.awssdk.services.sfn.SfnClient; +import software.amazon.awssdk.services.sfn.model.ActivityListItem; +import software.amazon.awssdk.services.sfn.model.CreateActivityRequest; +import software.amazon.awssdk.services.sfn.model.CreateStateMachineRequest; +import software.amazon.awssdk.services.sfn.model.DescribeActivityRequest; +import software.amazon.awssdk.services.sfn.model.DescribeStateMachineRequest; +import software.amazon.awssdk.services.sfn.model.ListActivitiesRequest; +import software.amazon.awssdk.services.sfn.model.ListStateMachinesRequest; +import software.amazon.awssdk.services.sfn.model.StateMachineListItem; +import software.amazon.awssdk.services.sfn.model.StateMachineType; +import software.amazon.awssdk.services.sns.SnsClient; +import software.amazon.awssdk.services.sns.model.CreateTopicRequest; +import software.amazon.awssdk.services.sns.model.GetTopicAttributesRequest; +import software.amazon.awssdk.services.sns.model.ListTopicsRequest; +import software.amazon.awssdk.services.sns.model.Topic; import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.sqs.model.CreateQueueRequest; import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest; @@ -121,7 +144,10 @@ public static void main(String[] args) throws IOException, InterruptedException setupS3(); setupSqs(); setupKinesis(); + setupSecretsManager(); + setupSfn(); setupBedrock(); + setupSns(); // Add this log line so that we only start testing after all routes are configured. awaitInitialization(); logger.info("All routes initialized"); @@ -532,6 +558,365 @@ private static void setupS3() { }); } + private static void setupSecretsManager() { + var secretsManagerClient = + SecretsManagerClient.builder() + .endpointOverride(endpoint) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + var secretName = "test-secret-id"; + String existingSecretArn = null; + try { + var listRequest = ListSecretsRequest.builder().build(); + var listResponse = secretsManagerClient.listSecrets(listRequest); + existingSecretArn = + listResponse.secretList().stream() + .filter(secret -> secret.name().contains(secretName)) + .findFirst() + .map(SecretListEntry::arn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing secrets", e); + } + + if (existingSecretArn != null) { + logger.debug("Secret already exists, skipping creation"); + } else { + logger.debug("Secret not found, creating a new one"); + var createSecretRequest = CreateSecretRequest.builder().name(secretName).build(); + var createSecretResponse = secretsManagerClient.createSecret(createSecretRequest); + existingSecretArn = createSecretResponse.arn(); + } + + String finalExistingSecretArn = existingSecretArn; + get( + "/secretsmanager/describesecret/:secretId", + (req, res) -> { + var describeRequest = + DescribeSecretRequest.builder().secretId(finalExistingSecretArn).build(); + secretsManagerClient.describeSecret(describeRequest); + return ""; + }); + + get( + "/secretsmanager/error", + (req, res) -> { + setMainStatus(400); + var errorClient = + SecretsManagerClient.builder() + .endpointOverride(URI.create("http://error.test:8080")) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + try { + var describeRequest = + DescribeSecretRequest.builder() + .secretId( + "arn:aws:secretsmanager:us-west-2:000000000000:secret:nonexistent-secret-id") + .build(); + errorClient.describeSecret(describeRequest); + } catch (Exception e) { + logger.error("Error describing secret", e); + } + return ""; + }); + + get( + "/secretsmanager/fault", + (req, res) -> { + setMainStatus(500); + var faultClient = + SecretsManagerClient.builder() + .endpointOverride(URI.create("http://fault.test:8080")) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + try { + var describeRequest = + DescribeSecretRequest.builder() + .secretId( + "arn:aws:secretsmanager:us-west-2:000000000000:secret:fault-secret-id") + .build(); + faultClient.describeSecret(describeRequest); + } catch (Exception e) { + logger.error("Error describing secret", e); + } + return ""; + }); + } + + private static void setupSfn() { + var sfnClient = + SfnClient.builder() + .endpointOverride(endpoint) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + var iamClient = + IamClient.builder() + .endpointOverride(endpoint) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + var sfnName = "test-state-machine"; + String existingStateMachineArn = null; + try { + var listRequest = ListStateMachinesRequest.builder().build(); + var listResponse = sfnClient.listStateMachines(listRequest); + existingStateMachineArn = + listResponse.stateMachines().stream() + .filter(machine -> machine.name().equals(sfnName)) + .findFirst() + .map(StateMachineListItem::stateMachineArn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing state machines", e); + } + + if (existingStateMachineArn != null) { + logger.debug("State machine already exists, skipping creation"); + } else { + logger.debug("State machine not found, creating a new one"); + String trustPolicy = + "{" + + "\"Version\": \"2012-10-17\"," + + "\"Statement\": [" + + " {" + + " \"Effect\": \"Allow\"," + + " \"Principal\": {" + + " \"Service\": \"states.amazonaws.com\"" + + " }," + + " \"Action\": \"sts:AssumeRole\"" + + " }" + + "]}"; + var roleRequest = + CreateRoleRequest.builder() + .roleName(sfnName + "-role") + .assumeRolePolicyDocument(trustPolicy) + .build(); + var roleArn = iamClient.createRole(roleRequest).role().arn(); + String policyDocument = + "{" + + "\"Version\": \"2012-10-17\"," + + "\"Statement\": [" + + " {" + + " \"Effect\": \"Allow\"," + + " \"Action\": [" + + " \"lambda:InvokeFunction\"" + + " ]," + + " \"Resource\": [" + + " \"*\"" + + " ]" + + " }" + + "]}"; + var policyRequest = + PutRolePolicyRequest.builder() + .roleName(sfnName + "-role") + .policyName(sfnName + "-policy") + .policyDocument(policyDocument) + .build(); + iamClient.putRolePolicy(policyRequest); + String stateMachineDefinition = + "{" + + " \"Comment\": \"A Hello World example of the Amazon States Language using a Pass state\"," + + " \"StartAt\": \"HelloWorld\"," + + " \"States\": {" + + " \"HelloWorld\": {" + + " \"Type\": \"Pass\"," + + " \"Result\": \"Hello World!\"," + + " \"End\": true" + + " }" + + " }" + + "}"; + var sfnRequest = + CreateStateMachineRequest.builder() + .name(sfnName) + .roleArn(roleArn) + .definition(stateMachineDefinition) + .type(StateMachineType.STANDARD) + .build(); + existingStateMachineArn = sfnClient.createStateMachine(sfnRequest).stateMachineArn(); + } + + var activityName = "test-activity"; + String existingActivityArn = null; + + try { + var listRequest = ListActivitiesRequest.builder().build(); + var listResponse = sfnClient.listActivities(listRequest); + existingActivityArn = + listResponse.activities().stream() + .filter(activity -> activity.name().equals(activityName)) + .findFirst() + .map(ActivityListItem::activityArn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing activities", e); + } + + if (existingActivityArn != null) { + logger.debug("Activities already exists, skipping creation"); + } else { + logger.debug("Activities not found, creating a new one"); + var createRequest = CreateActivityRequest.builder().name(activityName).build(); + existingActivityArn = sfnClient.createActivity(createRequest).activityArn(); + } + + String finalExistingStateMachineArn = existingStateMachineArn; + String finalExistingActivityArn = existingActivityArn; + + get( + "/sfn/describestatemachine/:name", + (req, res) -> { + var describeRequest = + DescribeStateMachineRequest.builder() + .stateMachineArn(finalExistingStateMachineArn) + .build(); + sfnClient.describeStateMachine(describeRequest); + return ""; + }); + + get( + "/sfn/describeactivity/:name", + (req, res) -> { + var describeRequest = + DescribeActivityRequest.builder().activityArn(finalExistingActivityArn).build(); + sfnClient.describeActivity(describeRequest); + return ""; + }); + + get( + "/sfn/error", + (req, res) -> { + setMainStatus(400); + var errorClient = + SfnClient.builder() + .endpointOverride(URI.create("http://error.test:8080")) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + try { + var describeRequest = + DescribeActivityRequest.builder() + .activityArn( + "arn:aws:states:us-west-2:000000000000:activity:nonexistent-activity") + .build(); + errorClient.describeActivity(describeRequest); + } catch (Exception e) { + logger.error("Error describing activity", e); + } + return ""; + }); + + get( + "/sfn/fault", + (req, res) -> { + setMainStatus(500); + var faultClient = + SfnClient.builder() + .endpointOverride(URI.create("http://fault.test:8080")) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + try { + var describeRequest = + DescribeActivityRequest.builder() + .activityArn("arn:aws:states:us-west-2:000000000000:activity:fault-activity") + .build(); + faultClient.describeActivity(describeRequest); + } catch (Exception e) { + logger.error("Error describing activity", e); + } + return ""; + }); + } + + private static void setupSns() { + var snsClient = + SnsClient.builder() + .endpointOverride(endpoint) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + var topicName = "test-topic"; + String existingTopicArn = null; + + try { + var listRequest = ListTopicsRequest.builder().build(); + var listResponse = snsClient.listTopics(listRequest); + existingTopicArn = + listResponse.topics().stream() + .filter(topic -> topic.topicArn().contains(topicName)) + .findFirst() + .map(Topic::topicArn) + .orElse(null); + } catch (Exception e) { + logger.error("Error listing topics", e); + } + + if (existingTopicArn != null) { + logger.debug("Topics already exists, skipping creation"); + } else { + logger.debug("Topics not found, creating a new one"); + var createTopicRequest = CreateTopicRequest.builder().name(topicName).build(); + var createTopicResponse = snsClient.createTopic(createTopicRequest); + existingTopicArn = createTopicResponse.topicArn(); + } + + String finalExistingTopicArn = existingTopicArn; + get( + "/sns/gettopicattributes/:topicId", + (req, res) -> { + var getTopicAttributesRequest = + GetTopicAttributesRequest.builder().topicArn(finalExistingTopicArn).build(); + snsClient.getTopicAttributes(getTopicAttributesRequest); + return ""; + }); + + get( + "/sns/error", + (req, res) -> { + setMainStatus(400); + var errorClient = + SnsClient.builder() + .endpointOverride(URI.create("http://error.test:8080")) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + try { + var getTopicAttributesRequest = + GetTopicAttributesRequest.builder() + .topicArn("arn:aws:sns:us-west-2:000000000000:nonexistent-topic") + .build(); + errorClient.getTopicAttributes(getTopicAttributesRequest); + } catch (Exception e) { + logger.error("Error describing topic", e); + } + return ""; + }); + + get( + "/sns/fault", + (req, res) -> { + setMainStatus(500); + var faultClient = + SnsClient.builder() + .endpointOverride(URI.create("http://fault.test:8080")) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + try { + var getTopicAttributesRequest = + GetTopicAttributesRequest.builder() + .topicArn("arn:aws:sns:us-west-2:000000000000:fault-topic") + .build(); + faultClient.getTopicAttributes(getTopicAttributesRequest); + } catch (Exception e) { + logger.error("Error describing topic", e); + } + return ""; + }); + } + private static void setupBedrock() { // Localstack does not support Bedrock related services. // We point all Bedrock related request endpoints to the local app, From eae57c577d8fe3c702c0f8d36db9493b19930fe2 Mon Sep 17 00:00:00 2001 From: Prashant Srivastava <50466688+srprash@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:52:30 -0800 Subject: [PATCH 11/11] APIGW + Lambda sample app (#961) --- sample-apps/apigateway-lambda/README.md | 58 +++++++++ .../apigateway-lambda/build.gradle.kts | 40 ++++++ .../com/amazon/sampleapp/LambdaHandler.java | 82 ++++++++++++ .../apigateway-lambda/terraform/main.tf | 119 ++++++++++++++++++ .../apigateway-lambda/terraform/variables.tf | 43 +++++++ settings.gradle.kts | 1 + 6 files changed, 343 insertions(+) create mode 100644 sample-apps/apigateway-lambda/README.md create mode 100644 sample-apps/apigateway-lambda/build.gradle.kts create mode 100644 sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java create mode 100644 sample-apps/apigateway-lambda/terraform/main.tf create mode 100644 sample-apps/apigateway-lambda/terraform/variables.tf diff --git a/sample-apps/apigateway-lambda/README.md b/sample-apps/apigateway-lambda/README.md new file mode 100644 index 0000000000..6ecc80986b --- /dev/null +++ b/sample-apps/apigateway-lambda/README.md @@ -0,0 +1,58 @@ +## API Gateway + Lambda Sample Application + +The directory contains the source code and the Infrastructure as Code (IaC) to create the sample app in your AWS account. + +### Prerequisite +Before you begin, ensure you have the following installed: +- Java 17 +- Gradle +- Terraform +- AWS CLI configured with appropriate credentials + +### Getting Started + +#### 1. Build the application +```bash +# Change to the project directory +cd sample-apps/apigateway-lambda + +# Build the application using Gradle +gradle clean build + +# Prepare the Lambda deployment package +gradle createLambdaZip +``` + +#### 2. Deploy the application +```bash +# Change to the terraform directory +cd terraform + +# Initialize Terraform +terraform init + +# (Optional) Review the deployment plan for better understanding of the components +terraform plan + +# Deploy +terraform apply +``` + +#### 3. Testing the applicating +After successful deployment, Terraform will output the API Gateway endpoint URL. You can test the application using: +```bash +curl +``` + +#### 4. Clean Up +To avoid incurring unnecessary charges, remember to destroy the resources when you are done: +```bash +terraform destroy +``` + +#### (Optional) Instrumenting with Application Signals Lambda Layer +You can choose to instrument the Lambda function with Application Signals Lambda Layer upon deployment by passing in the layer ARN to the `adot_layer_arn` variable. +You must have the layer already published to your account before executing the following command. +```bash +terraform apply -var "adot_layer_arn=" +``` \ No newline at end of file diff --git a/sample-apps/apigateway-lambda/build.gradle.kts b/sample-apps/apigateway-lambda/build.gradle.kts new file mode 100644 index 0000000000..66992540ab --- /dev/null +++ b/sample-apps/apigateway-lambda/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + java + application +} + +application { + mainClass.set("com.amazon.sampleapp.LambdaHandler") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + implementation("com.amazonaws:aws-lambda-java-core:1.2.2") + implementation("com.squareup.okhttp3:okhttp:4.11.0") + implementation("software.amazon.awssdk:s3:2.29.23") + implementation("org.json:json:20240303") + implementation("org.slf4j:jcl-over-slf4j:2.0.16") + testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") +} + +tasks.jar { + manifest { + attributes["Main-Class"] = "com.amazon.sampleapp.LambdaHandler" + } +} + +tasks.register("createLambdaZip") { + dependsOn("build") + from(tasks.compileJava.get()) + from(tasks.processResources.get()) + into("lib") { + from(configurations.runtimeClasspath.get()) + } + archiveFileName.set("lambda-function.zip") + destinationDirectory.set(layout.buildDirectory.dir("distributions")) +} diff --git a/sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java b/sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java new file mode 100644 index 0000000000..f3e11bc38d --- /dev/null +++ b/sample-apps/apigateway-lambda/src/main/java/com/amazon/sampleapp/LambdaHandler.java @@ -0,0 +1,82 @@ +package com.amazon.sampleapp; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import java.io.IOException; +import java.util.Map; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.json.JSONObject; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.HeadBucketRequest; +import software.amazon.awssdk.services.s3.model.S3Exception; + +public class LambdaHandler implements RequestHandler> { + + private final OkHttpClient client = new OkHttpClient(); + private final S3Client s3Client = S3Client.create(); + + @Override + public Map handleRequest(Object input, Context context) { + System.out.println("Executing LambdaHandler"); + + // https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime + // try and get the trace id from environment variable _X_AMZN_TRACE_ID. If it's not present + // there + // then try the system property. + String traceId = + System.getenv("_X_AMZN_TRACE_ID") != null + ? System.getenv("_X_AMZN_TRACE_ID") + : System.getProperty("com.amazonaws.xray.traceHeader"); + + System.out.println("Trace ID: " + traceId); + + JSONObject responseBody = new JSONObject(); + responseBody.put("traceId", traceId); + + // Make a remote call using OkHttp + System.out.println("Making a remote call using OkHttp"); + String url = "https://www.amazon.com"; + Request request = new Request.Builder().url(url).build(); + + try (Response response = client.newCall(request).execute()) { + responseBody.put("httpRequest", "Request successful"); + } catch (IOException e) { + context.getLogger().log("Error: " + e.getMessage()); + responseBody.put("httpRequest", "Request failed"); + } + System.out.println("Remote call done"); + + // Make a S3 HeadBucket call to check whether the bucket exists + System.out.println("Making a S3 HeadBucket call"); + String bucketName = "SomeDummyBucket"; + try { + HeadBucketRequest headBucketRequest = HeadBucketRequest.builder().bucket(bucketName).build(); + s3Client.headBucket(headBucketRequest); + responseBody.put("s3Request", "Bucket exists and is accessible: " + bucketName); + } catch (S3Exception e) { + if (e.statusCode() == 403) { + responseBody.put("s3Request", "Access denied to bucket: " + bucketName); + } else if (e.statusCode() == 404) { + responseBody.put("s3Request", "Bucket does not exist: " + bucketName); + } else { + System.err.println("Error checking bucket: " + e.awsErrorDetails().errorMessage()); + responseBody.put( + "s3Request", "Error checking bucket: " + e.awsErrorDetails().errorMessage()); + } + } + System.out.println("S3 HeadBucket call done"); + + // return a response in the ApiGateway proxy format + return Map.of( + "isBase64Encoded", + false, + "statusCode", + 200, + "body", + responseBody.toString(), + "headers", + Map.of("Content-Type", "application/json")); + } +} diff --git a/sample-apps/apigateway-lambda/terraform/main.tf b/sample-apps/apigateway-lambda/terraform/main.tf new file mode 100644 index 0000000000..6881f0e1ce --- /dev/null +++ b/sample-apps/apigateway-lambda/terraform/main.tf @@ -0,0 +1,119 @@ +### Lambda function +locals { + architecture = var.architecture == "x86_64" ? "amd64" : "arm64" +} + +resource "aws_iam_role" "lambda_role" { + name = "lambda_execution_role" + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Action = "sts:AssumeRole", + Effect = "Allow", + Principal = { Service = "lambda.amazonaws.com" } + }] + }) +} + +resource "aws_iam_policy" "s3_access" { + name = "S3ListBucketPolicy" + description = "Allow Lambda to check a given S3 bucket exists" + policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Effect = "Allow", + Action = ["s3:ListBucket"], + Resource = "*" + }] + }) +} + +resource "aws_iam_role_policy_attachment" "attach_execution_role_policy" { + role = aws_iam_role.lambda_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +} + +resource "aws_iam_role_policy_attachment" "attach_s3_policy" { + role = aws_iam_role.lambda_role.name + policy_arn = aws_iam_policy.s3_access.arn +} + +resource "aws_iam_role_policy_attachment" "attach_xray_policy" { + role = aws_iam_role.lambda_role.name + policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess" +} + +resource "aws_lambda_function" "sampleLambdaFunction" { + function_name = var.function_name + runtime = var.runtime + timeout = 300 + handler = "com.amazon.sampleapp.LambdaHandler::handleRequest" + role = aws_iam_role.lambda_role.arn + filename = "${path.module}/../build/distributions/lambda-function.zip" + source_code_hash = filebase64sha256("${path.module}/../build/distributions/lambda-function.zip") + architectures = [var.architecture] + memory_size = 512 + tracing_config { + mode = var.lambda_tracing_mode + } + layers = var.adot_layer_arn != null && var.adot_layer_arn != "" ? [var.adot_layer_arn] : [] + environment { + variables = var.adot_layer_arn != null && var.adot_layer_arn != "" ? { + AWS_LAMBDA_EXEC_WRAPPER = "/opt/otel-instrument" + } : {} + } +} + +### API Gateway proxy to Lambda function +resource "aws_api_gateway_rest_api" "apigw_lambda_api" { + name = var.api_gateway_name +} + +resource "aws_api_gateway_resource" "apigw_lambda_resource" { + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id + parent_id = aws_api_gateway_rest_api.apigw_lambda_api.root_resource_id + path_part = "lambda" +} + +resource "aws_api_gateway_method" "apigw_lambda_method" { + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id + resource_id = aws_api_gateway_resource.apigw_lambda_resource.id + http_method = "ANY" + authorization = "NONE" +} + +resource "aws_api_gateway_integration" "apigw_lambda_integration" { + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id + resource_id = aws_api_gateway_resource.apigw_lambda_resource.id + http_method = aws_api_gateway_method.apigw_lambda_method.http_method + integration_http_method = "POST" + type = "AWS_PROXY" + uri = aws_lambda_function.sampleLambdaFunction.invoke_arn +} + +resource "aws_api_gateway_deployment" "apigw_lambda_deployment" { + depends_on = [ + aws_api_gateway_integration.apigw_lambda_integration + ] + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id +} + +resource "aws_api_gateway_stage" "test" { + stage_name = "default" + rest_api_id = aws_api_gateway_rest_api.apigw_lambda_api.id + deployment_id = aws_api_gateway_deployment.apigw_lambda_deployment.id + xray_tracing_enabled = var.apigw_tracing_enabled +} + +resource "aws_lambda_permission" "apigw_lambda_invoke" { + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.sampleLambdaFunction.function_name + principal = "apigateway.amazonaws.com" + source_arn = "${aws_api_gateway_rest_api.apigw_lambda_api.execution_arn}/*/*" +} + +# Output the API Gateway URL +output "invoke_url" { + value = "${aws_api_gateway_stage.test.invoke_url}/lambda" +} diff --git a/sample-apps/apigateway-lambda/terraform/variables.tf b/sample-apps/apigateway-lambda/terraform/variables.tf new file mode 100644 index 0000000000..2dc99f0685 --- /dev/null +++ b/sample-apps/apigateway-lambda/terraform/variables.tf @@ -0,0 +1,43 @@ +## Lambda function related configurations +variable "function_name" { + type = string + description = "Name of sample app function" + default = "aws-opentelemetry-distro-java" +} + +variable "architecture" { + type = string + description = "Lambda function architecture, either arm64 or x86_64" + default = "x86_64" +} + +variable "runtime" { + type = string + description = "Java runtime version used for Lambda Function" + default = "java17" +} + +variable "lambda_tracing_mode" { + type = string + description = "Lambda function tracing mode" + default = "Active" +} + +variable "adot_layer_arn" { + type = string + description = "ARN of the ADOT JAVA layer" + default = null +} + +## API Gateway related configurations +variable "api_gateway_name" { + type = string + description = "Name of API gateway to create" + default = "aws-opentelemetry-distro-java" +} + +variable "apigw_tracing_enabled" { + type = string + description = "API Gateway REST API tracing enabled or not" + default = "true" +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f4ec99f115..decf81d145 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,6 +51,7 @@ include(":smoke-tests:spring-boot") include(":sample-apps:springboot") include(":sample-apps:spark") include(":sample-apps:spark-awssdkv1") +include(":sample-apps:apigateway-lambda") // Used for contract tests include("appsignals-tests:images:mock-collector")