diff --git a/adot-testbed/app/build.gradle.kts b/adot-testbed/app/build.gradle.kts index c4f55a89f..d8838e274 100644 --- a/adot-testbed/app/build.gradle.kts +++ b/adot-testbed/app/build.gradle.kts @@ -27,6 +27,14 @@ dependencies { testImplementation("com.github.rholder:guava-retrying:2.0.0") testImplementation("org.assertj:assertj-core:3.24.2") testImplementation("com.fasterxml.jackson.core:jackson-databind:2.13.0") + + // Trace ID (W3C & XRay) tests with XRay Exporter + api(platform("io.opentelemetry:opentelemetry-bom-alpha:1.30.0-alpha")) + testImplementation("io.opentelemetry:opentelemetry-exporter-otlp") + testImplementation("io.opentelemetry:opentelemetry-sdk"); + testImplementation("io.opentelemetry:opentelemetry-semconv"); + testImplementation("io.opentelemetry.contrib:opentelemetry-aws-xray:1.30.0") + testImplementation("software.amazon.awssdk:xray") } // Apply a specific Java toolchain to ease working on different environments. diff --git a/adot-testbed/app/src/test/java/software/amazon/adot/testbed/CollectorSetup.java b/adot-testbed/app/src/test/java/software/amazon/adot/testbed/CollectorSetup.java new file mode 100644 index 000000000..9e7e316e1 --- /dev/null +++ b/adot-testbed/app/src/test/java/software/amazon/adot/testbed/CollectorSetup.java @@ -0,0 +1,80 @@ +package software.amazon.adot.testbed; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Files; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.MountableFile; +import org.testcontainers.containers.FixedHostPortGenericContainer; +import org.testcontainers.containers.InternetProtocol; + +abstract class CollectorSetup { + private static final String TEST_IMAGE = System.getenv("TEST_IMAGE") != null && !System.getenv("TEST_IMAGE").isEmpty() + ? System.getenv("TEST_IMAGE") + : "public.ecr.aws/aws-observability/aws-otel-collector:latest"; + private final Logger collectorLogger = LoggerFactory.getLogger("collector"); + protected Path logDirectory; + protected GenericContainer collector; + + private Map createCollectorEnvVars(String logStreamName) { + // Create an environment variable map + Map envVariables = new HashMap<>(); + if (logStreamName != null) { + envVariables.put("LOG_STREAM_NAME", logStreamName); + } + //Set credentials + envVariables.put("AWS_REGION", System.getenv("AWS_REGION")); + envVariables.put("AWS_ACCESS_KEY_ID", System.getenv("AWS_ACCESS_KEY_ID")); + envVariables.put("AWS_SECRET_ACCESS_KEY", System.getenv("AWS_SECRET_ACCESS_KEY")); + // Check if AWS_SESSION_TOKEN is not null before adding it + if (System.getenv("AWS_SESSION_TOKEN") != null) { + envVariables.put("AWS_SESSION_TOKEN", System.getenv("AWS_SESSION_TOKEN")); + } + return envVariables; + } + + protected GenericContainer createAndStartCollectorForLogs(String configFilePath, String logStreamName) throws IOException { + try { + logDirectory = Files.createTempDirectory("tempLogs"); + } catch (IOException e) { + throw new RuntimeException("Failed to create log directory", e); + } + var collector = new GenericContainer<>(TEST_IMAGE) + .withCopyFileToContainer(MountableFile.forClasspathResource(configFilePath), "/etc/collector/config.yaml") + .withLogConsumer(new Slf4jLogConsumer(collectorLogger)) + .waitingFor(Wait.forLogMessage(".*Everything is ready. Begin running and processing data.*", 1)) + .withEnv(createCollectorEnvVars(logStreamName)) + .withClasspathResourceMapping("/logs", "/logs", BindMode.READ_WRITE) + .withCommand("--config", "/etc/collector/config.yaml", "--feature-gates=+adot.receiver.filelog,+adot.exporter.awscloudwatchlogs,+adot.extension.file_storage"); + + //Mount the Temp directory + collector.withFileSystemBind(logDirectory.toString(),"/tempLogs", BindMode.READ_WRITE); + + collector.start(); + return collector; + } + + protected GenericContainer createAndStartCollectorForXray(String configFilePath) throws IOException { + var collector = new FixedHostPortGenericContainer<>(TEST_IMAGE) + .withCopyFileToContainer(MountableFile.forClasspathResource(configFilePath), "/etc/collector/config.yaml") + .withFixedExposedPort(4317, 4317, InternetProtocol.TCP) + .withLogConsumer(new Slf4jLogConsumer(collectorLogger)) + .waitingFor(Wait.forLogMessage(".*Everything is ready. Begin running and processing data.*", 1)) + .withEnv(createCollectorEnvVars(null)) + .withCommand("--config", "/etc/collector/config.yaml"); + + collector.start(); + return collector; + } +} diff --git a/adot-testbed/app/src/test/java/software/amazon/adot/testbed/LogsTests.java b/adot-testbed/app/src/test/java/software/amazon/adot/testbed/LogsTests.java index 7c90ecf3f..0bb727bdb 100644 --- a/adot-testbed/app/src/test/java/software/amazon/adot/testbed/LogsTests.java +++ b/adot-testbed/app/src/test/java/software/amazon/adot/testbed/LogsTests.java @@ -17,9 +17,7 @@ import java.time.Duration; import java.time.Instant; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; import java.util.List; import java.util.ArrayList; import java.util.Objects; @@ -36,14 +34,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.BindMode; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.MountableFile; import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient; import software.amazon.awssdk.services.cloudwatchlogs.model.GetLogEventsRequest; @@ -51,52 +42,13 @@ @Testcontainers(disabledWithoutDocker = true) @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class LogsTests { - private static final String TEST_IMAGE = System.getenv("TEST_IMAGE") != null && !System.getenv("TEST_IMAGE").isEmpty() - ? System.getenv("TEST_IMAGE") - : "public.ecr.aws/aws-observability/aws-otel-collector:latest"; - private final Logger collectorLogger = LoggerFactory.getLogger("collector"); +class LogsTests extends CollectorSetup { private static final String uniqueID = UUID.randomUUID().toString(); - private Path logDirectory; - private GenericContainer collector; - - private GenericContainer createAndStartCollector(String configFilePath, String logStreamName) throws IOException { - - // Create an environment variable map - Map envVariables = new HashMap<>(); - envVariables.put("LOG_STREAM_NAME", logStreamName); - //Set credentials - envVariables.put("AWS_REGION", System.getenv("AWS_REGION")); - envVariables.put("AWS_ACCESS_KEY_ID", System.getenv("AWS_ACCESS_KEY_ID")); - envVariables.put("AWS_SECRET_ACCESS_KEY", System.getenv("AWS_SECRET_ACCESS_KEY")); - // Check if AWS_SESSION_TOKEN is not null before adding it - if (System.getenv("AWS_SESSION_TOKEN") != null) { - envVariables.put("AWS_SESSION_TOKEN", System.getenv("AWS_SESSION_TOKEN")); - } - try { - logDirectory = Files.createTempDirectory("tempLogs"); - } catch (IOException e) { - throw new RuntimeException("Failed to create log directory", e); - } - var collector = new GenericContainer<>(TEST_IMAGE) - .withCopyFileToContainer(MountableFile.forClasspathResource(configFilePath), "/etc/collector/config.yaml") - .withLogConsumer(new Slf4jLogConsumer(collectorLogger)) - .waitingFor(Wait.forLogMessage(".*Everything is ready. Begin running and processing data.*", 1)) - .withEnv(envVariables) - .withCreateContainerCmdModifier(cmd -> cmd.withUser("root")) - .withClasspathResourceMapping("/logs", "/logs", BindMode.READ_WRITE) - .withCommand("--config", "/etc/collector/config.yaml"); - - //Mount the Temp directory - collector.withFileSystemBind(logDirectory.toString(),"/tempLogs", BindMode.READ_WRITE); - collector.start(); - return collector; - } @Test void testSyslog() throws Exception { String logStreamName = "rfcsyslog-logstream-" + uniqueID; - collector = createAndStartCollector("/configurations/config-rfcsyslog.yaml", logStreamName); + collector = createAndStartCollectorForLogs("/configurations/config-rfcsyslog.yaml", logStreamName); List inputStreams = new ArrayList<>(); InputStream inputStream = getClass().getResourceAsStream("/logs/RFC5424.log"); inputStreams.add(inputStream); @@ -109,7 +61,7 @@ void testSyslog() throws Exception { @Test void testLog4j() throws Exception { String logStreamName = "log4j-logstream-" + uniqueID; - collector = createAndStartCollector("/configurations/config-log4j.yaml", logStreamName); + collector = createAndStartCollectorForLogs("/configurations/config-log4j.yaml", logStreamName); List inputStreams = new ArrayList<>(); InputStream inputStream = getClass().getResourceAsStream("/logs/log4j.log"); inputStreams.add(inputStream); @@ -121,7 +73,7 @@ void testLog4j() throws Exception { @Test void testJson() throws Exception { String logStreamName = "json-logstream-" + uniqueID; - collector = createAndStartCollector("/configurations/config-json.yaml", logStreamName); + collector = createAndStartCollectorForLogs("/configurations/config-json.yaml", logStreamName); List inputStreams = new ArrayList<>(); InputStream inputStream = getClass().getResourceAsStream("/logs/testingJSON.log"); inputStreams.add(inputStream); @@ -134,7 +86,7 @@ void testJson() throws Exception { @Test void testCollectorRestartStorageExtension() throws Exception { String logStreamName = "storageExtension-logstream-" + uniqueID; - collector = createAndStartCollector("/configurations/config-storageExtension.yaml", logStreamName); + collector = createAndStartCollectorForLogs("/configurations/config-storageExtension.yaml", logStreamName); File tempFile = new File(logDirectory.toString(), "storageExtension.log"); Thread.sleep(5000); @@ -178,7 +130,7 @@ void testCollectorRestartStorageExtension() throws Exception { @Test void testFileRotation() throws Exception { String logStreamName = "fileRotation-logstream-" + uniqueID; - collector = createAndStartCollector("/configurations/config-fileRotation.yaml", logStreamName); + collector = createAndStartCollectorForLogs("/configurations/config-fileRotation.yaml", logStreamName); Thread.sleep(5000); diff --git a/adot-testbed/app/src/test/java/software/amazon/adot/testbed/TraceIdsWithXRayTests.java b/adot-testbed/app/src/test/java/software/amazon/adot/testbed/TraceIdsWithXRayTests.java new file mode 100644 index 000000000..bc714f69a --- /dev/null +++ b/adot-testbed/app/src/test/java/software/amazon/adot/testbed/TraceIdsWithXRayTests.java @@ -0,0 +1,162 @@ +package software.amazon.adot.testbed; + +import java.util.HashSet; +import java.util.Set; +import java.util.List; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +import com.github.rholder.retry.RetryerBuilder; +import com.github.rholder.retry.StopStrategies; +import com.github.rholder.retry.WaitStrategies; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.TestInstance; +import org.testcontainers.junit.jupiter.Testcontainers; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.contrib.awsxray.AwsXrayIdGenerator; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import io.opentelemetry.sdk.trace.IdGenerator; + +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.xray.XRayClient; +import software.amazon.awssdk.services.xray.model.BatchGetTracesRequest; +import software.amazon.awssdk.services.xray.model.BatchGetTracesResponse; + +@Testcontainers(disabledWithoutDocker = true) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TraceIdsWithXRayTests extends CollectorSetup { + private static final IdGenerator XRAY_ID_GENERATOR = AwsXrayIdGenerator.getInstance(); + + @Test + void testXRayTraceIdSendToXRay() throws Exception { + List traceIds = createTraces(XRAY_ID_GENERATOR); + validateTracesInXRay(traceIds); + } + + @Test + void testW3CTraceIdSendToXRay() throws Exception { + List traceIds = createTraces(null); + validateTracesInXRay(traceIds); + } + + List createTraces(IdGenerator idGenerator) throws Exception { + collector = createAndStartCollectorForXray("/configurations/config-xrayExporter.yaml"); + + OpenTelemetry otel = openTelemetry(idGenerator); + Tracer tracer = otel.getTracer("adot-trace-test"); + + Attributes attributes = Attributes.of( + AttributeKey.stringKey("http.method"), "GET", + AttributeKey.stringKey("http.url"), "http://localhost:8080/randomEndpoint" + ); + + int numOfTraces = 5; + List traceIds = new ArrayList(); + for (int count = 0; count < numOfTraces; count++) { + Span span = tracer.spanBuilder("trace-id-test") + .setSpanKind(SpanKind.SERVER) + .setAllAttributes(attributes) + .startSpan(); + //Format trace IDs to XRay format ({16 bytes} --> {1-<4-bytes>-<12-bytes>}) + String id = new StringBuilder(span.getSpanContext().getTraceId()).insert(8, "-").insert(0, "1-").toString(); + traceIds.add(id); + span.end(); + } + + assertThat(traceIds).hasSize(numOfTraces); + return traceIds; + } + + void validateTracesInXRay(List traceIds) throws Exception { + Region region = Region.of(System.getenv("AWS_REGION")); + XRayClient xray = XRayClient.builder() + .region(region) + .build(); + + RetryerBuilder.newBuilder() + .retryIfException() + .retryIfRuntimeException() + .retryIfExceptionOfType(java.lang.AssertionError.class) + .withWaitStrategy(WaitStrategies.fixedWait(10, TimeUnit.SECONDS)) + .withStopStrategy(StopStrategies.stopAfterAttempt(5)) + .build() + .call(() -> { + BatchGetTracesResponse tracesResponse = xray.batchGetTraces(BatchGetTracesRequest.builder() + .traceIds(traceIds) + .build()); + + // Assertions + Set traceIdsSet = new HashSet(traceIds); + assertThat(tracesResponse.traces()).hasSize(traceIds.size()); + tracesResponse.traces().forEach(trace -> { + assertThat(traceIdsSet.contains(trace.id())).isTrue(); + }); + + return null; + }); + } + + @AfterEach + public void resetGlobalOpenTelemetry() { + // Cleanup collector and otel sdk + collector.stop(); + GlobalOpenTelemetry.resetForTest(); + } + + private OpenTelemetry openTelemetry(IdGenerator idGenerator) { + Resource resource = Resource.getDefault().toBuilder() + .put(ResourceAttributes.SERVICE_NAME, "xray-test") + .put(ResourceAttributes.SERVICE_VERSION, "0.1.0") + .build(); + + SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + .setSampler(Sampler.alwaysOn()) + .setResource(resource) + .build(); + + String exporter = System.getenv().getOrDefault("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"); + + SdkTracerProvider tracerProvider; + SdkTracerProviderBuilder tracerProviderBuilder = SdkTracerProvider.builder() + .addSpanProcessor( + BatchSpanProcessor.builder( + OtlpGrpcSpanExporter.builder() + .setEndpoint(exporter) + .build()) + .build()) + .setResource(resource); + + if (idGenerator != null) { + tracerProvider = tracerProviderBuilder + .setIdGenerator(idGenerator) + .build(); + } else { + tracerProvider = tracerProviderBuilder.build(); + } + + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider) + .buildAndRegisterGlobal(); + + return openTelemetry; + } +} diff --git a/adot-testbed/app/src/test/resources/configurations/config-xrayExporter.yaml b/adot-testbed/app/src/test/resources/configurations/config-xrayExporter.yaml new file mode 100644 index 000000000..88ff88152 --- /dev/null +++ b/adot-testbed/app/src/test/resources/configurations/config-xrayExporter.yaml @@ -0,0 +1,14 @@ +receivers: + otlp: + protocols: + grpc: + +exporters: + awsxray: + local_mode: true + +service: + pipelines: + traces: + receivers: [otlp] + exporters: [awsxray] \ No newline at end of file