diff --git a/README.md b/README.md index da5ff99..64afacc 100644 --- a/README.md +++ b/README.md @@ -220,8 +220,8 @@ The following properties are nested properties below the `inspectit-eum-server.e Tracing exporters are responsible for passing the recorded tracing data to a corresponding storage. The inspectIT Ocelot EUM Server currently supports the following trace exporters: -* [Jaeger](#jaeger-exporter) [[Homepage](https://www.jaegertracing.io/)] * [OTLP (Traces)](#otlp-exporter-traces) [[Homepage](https://github.com/open-telemetry/opentelemetry-java/tree/main/exporters/otlp/trace)] +* [Jaeger](#jaeger-exporter) [[Homepage](https://www.jaegertracing.io/)] (deprecated) ###### General Trace Exporter Settings @@ -231,22 +231,6 @@ These settings apply to all trace exporters and can set below the `inspectit-eum |-----------------|-----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `.service-name` | `${inspectit.service-name}` | The value of this property will be used to identify the service a trace came from. Please note that changes of this property only take effect after restarting the agent. | -###### Jaeger Exporter - -InspectIT EUM Server supports thrift and gRPC Jaeger exporter. - -By default, the Jaeger exporters are enabled but the URL/gRPC `endpoint` needed for the exporter to actually start is set to `null`. - -The following properties are nested properties below the `inspectit.exporters.tracing.jaeger` property: - -|Property | Default | Description | -|---|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -|`.enabled`| `IF_CONFIGURED` | If `ENABLED` or `IF_CONFIGURED`, the agent will try to start the Jaeger exporter. If the url is not set, it will log a warning if set to `ENABLED` but fail silently if set to `IF_CONFIGURED`. | -|`.endpoint`| `null` | URL endpoint under which the Jaeger server can be accessed (e.g. http://127.0.0.1:14268/api/traces). | -|`.protocol`| `grpc` | The transport protocol. Supported protocols are `grpc` and `http/thrift`. | -| `.compression` | `NONE` | The compression method, see [OTEL documentation](https://opentelemetry.io/docs/reference/specification/protocol/exporter/). Supported compression methods are `gzip` and `none`. This property only takes effect when the protocol is set to `grpc`. | -| `.timeout` | `10s` | Maximum time the OTLP exporter will wait for each batch export, see [OTEL documentation](https://opentelemetry.io/docs/reference/specification/protocol/exporter/). This property only takes effect when the protocol is set to `grpc`. | - ###### OTLP Exporter (Traces) The OpenTelemetry Protocol (OTLP) exporters export the Traces in OTLP to the desired endpoint at a specified interval. @@ -263,6 +247,24 @@ The following properties are nested properties below the `inspectit.exporters.tr | `.compression` | `NONE` | The compression method, see [OTEL documentation](https://opentelemetry.io/docs/reference/specification/protocol/exporter/). Supported compression methods are `gzip` and `none`. | | `.timeout` | `10s` | Maximum time the OTLP exporter will wait for each batch export, see [OTEL documentation](https://opentelemetry.io/docs/reference/specification/protocol/exporter/). | + +###### Jaeger Exporter + +InspectIT EUM Server supports thrift and gRPC Jaeger exporter. However, since OpenTelemetry has announced to [migrate away from the +Jaeger exporter](https://opentelemetry.io/blog/2023/jaeger-exporter-collector-migration/), it is **deprecated**. + +By default, the Jaeger exporters are enabled but the URL/gRPC `endpoint` needed for the exporter to actually start is set to `null`. + +The following properties are nested properties below the `inspectit.exporters.tracing.jaeger` property: + +|Property | Default | Description | +|---|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|`.enabled`| `IF_CONFIGURED` | If `ENABLED` or `IF_CONFIGURED`, the agent will try to start the Jaeger exporter. If the url is not set, it will log a warning if set to `ENABLED` but fail silently if set to `IF_CONFIGURED`. | +|`.endpoint`| `null` | URL endpoint under which the Jaeger server can be accessed (e.g. http://127.0.0.1:14268/api/traces). | +|`.protocol`| `grpc` | The transport protocol. Supported protocols are `grpc` and `http/thrift`. | +| `.compression` | `NONE` | The compression method, see [OTEL documentation](https://opentelemetry.io/docs/reference/specification/protocol/exporter/). Supported compression methods are `gzip` and `none`. This property only takes effect when the protocol is set to `grpc`. | +| `.timeout` | `10s` | Maximum time the OTLP exporter will wait for each batch export, see [OTEL documentation](https://opentelemetry.io/docs/reference/specification/protocol/exporter/). This property only takes effect when the protocol is set to `grpc`. | + ##### Security Currently, the EUM Server only supports a simple API token security concept. In future, additional authentication providers will be supported. diff --git a/build.gradle b/build.gradle index 064062d..b823828 100644 --- a/build.gradle +++ b/build.gradle @@ -138,24 +138,25 @@ dependencies { "io.opencensus:opencensus-api:${openCensusVersion}", "io.opencensus:opencensus-impl:${openCensusVersion}", "io.opencensus:opencensus-exporter-stats-prometheus:${openCensusVersion}", + "rocks.inspectit:opencensus-influxdb-exporter:${openCensusInfluxdbExporterVersion}", "io.grpc:grpc-context:${grpcVersion}", platform("io.opentelemetry:opentelemetry-bom-alpha:${openTelemetryAlphaVersion}"), platform("io.opentelemetry:opentelemetry-bom:${openTelemetryVersion}"), - "io.opentelemetry:opentelemetry-exporter-otlp:${openTelemetryVersion}", - "io.opentelemetry:opentelemetry-semconv:${openTelemetrySemConvVersion}", - "io.opentelemetry:opentelemetry-exporter-jaeger:${openTelemetryJaegerVersion}", - "io.opentelemetry:opentelemetry-exporter-jaeger-thrift:${openTelemetryJaegerVersion}", "io.opentelemetry:opentelemetry-sdk:${openTelemetryVersion}", + "io.opentelemetry:opentelemetry-exporter-otlp:${openTelemetryVersion}", + "io.opentelemetry:opentelemetry-exporter-jaeger:${openTelemetryVersion}", + "io.opentelemetry:opentelemetry-exporter-jaeger-thrift:${openTelemetryVersion}", + "io.opentelemetry:opentelemetry-semconv:${openTelemetryAlphaVersion}", "io.opentelemetry.proto:opentelemetry-proto:${openTelemetryProtoVersion}", "com.google.protobuf:protobuf-java:${protobufVersion}", "com.google.protobuf:protobuf-java-util:${protobufVersion}", "com.google.guava:guava:${guavaVersion}", - "com.maxmind.geoip2:geoip2:${geoip2Version}", + "commons-net:commons-net:${commonsNetVersion}", "org.apache.commons:commons-lang3:${commonsLang3Version}", "org.apache.commons:commons-math3:${commonsMath3Version}", @@ -165,9 +166,7 @@ dependencies { // If there is a higher new version, remove the dependency override of okio-jvm "org.influxdb:influxdb-java:${influxdbJavaVersion}", // Override transitive dependency with newer version, due to security concerns - "com.squareup.okio:okio-jvm:${okioJvmVersion}", - - "rocks.inspectit:opencensus-influxdb-exporter:${opencensusInfluxdbExporterVersion}", + "com.squareup.okio:okio-jvm:${okioJvmVersion}" ) compileOnly "org.projectlombok:lombok" @@ -182,8 +181,6 @@ dependencies { "com.linecorp.armeria:armeria-junit5:${armeriaVersion}", "com.linecorp.armeria:armeria-grpc-protocol:${armeriaVersion}", - "io.opentelemetry:opentelemetry-semconv:${openTelemetryAlphaVersion}", - // for docker test containers "org.testcontainers:testcontainers:${testContainersVersion}", "org.testcontainers:junit-jupiter:${testContainersVersion}" diff --git a/gradle.properties b/gradle.properties index 3c370d8..eea6f73 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,19 +9,20 @@ snakeYamlVersion=2.2 # Ensure to adapt the netty version (inspectit-ocelot-core/build.gradle) when changing the OpenCensus version openCensusVersion=0.31.1 +openCensusInfluxdbExporterVersion=1.2 grpcVersion=1.61.1 # pin Prometheus client to 0.6.0 to prevent auto prefixing counter metrics with "_total" # see: https://github.com/prometheus/client_java/issues/640, https://github.com/prometheus/client_java/pull/653 prometheusClientVersion = 0.6.0 # Keep the OpenTelemetry versions consistent -openTelemetryVersion=1.30.0 -openTelemetryAlphaVersion=1.30.0-alpha +openTelemetryVersion=1.30.1 +openTelemetryAlphaVersion=1.30.1-alpha + openTelemetryProtoVersion=1.1.0-alpha -openTelemetrySemConvVersion=1.30.1-alpha -openTelemetryJaegerVersion=1.34.1 +# Use version of opentelemetry-proto +protobufVersion=3.23.4 -protobufVersion=3.25.3 guavaVersion=33.0.0-jre geoip2Version=4.2.0 @@ -36,7 +37,6 @@ commonsIoVersion=2.14.0 influxdbJavaVersion=2.24 okioJvmVersion=3.5.0 -opencensusInfluxdbExporterVersion=1.2 armeriaVersion=1.27.1 testContainersVersion=1.19.5 diff --git a/src/main/java/io/opentelemetry/sdk/trace/OcelotSpanUtils.java b/src/main/java/io/opentelemetry/sdk/trace/OcelotSpanUtils.java index 6864bc5..6b65739 100644 --- a/src/main/java/io/opentelemetry/sdk/trace/OcelotSpanUtils.java +++ b/src/main/java/io/opentelemetry/sdk/trace/OcelotSpanUtils.java @@ -10,6 +10,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextKey; import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.ArrayValue; import io.opentelemetry.proto.common.v1.KeyValue; import io.opentelemetry.proto.trace.v1.Span; import io.opentelemetry.proto.trace.v1.Status; @@ -164,17 +165,12 @@ static SpanContext createSpanContext(String traceId, String spanId) { */ @VisibleForTesting static StatusCode toStatusCode(Status.StatusCode code) { - switch (code) { - case STATUS_CODE_UNSET: - return StatusCode.UNSET; - case STATUS_CODE_OK: - return StatusCode.OK; - case STATUS_CODE_ERROR: - return StatusCode.ERROR; - case UNRECOGNIZED: - default: - return null; - } + return switch (code) { + case STATUS_CODE_UNSET -> StatusCode.UNSET; + case STATUS_CODE_OK -> StatusCode.OK; + case STATUS_CODE_ERROR -> StatusCode.ERROR; + default -> null; + }; } /** @@ -233,25 +229,18 @@ public static Attributes toAttributes(List attributesList, Map builder.put(attributeKey, value.getStringValue()); + case BOOL_VALUE -> builder.put(attributeKey, value.getBoolValue()); + case INT_VALUE -> builder.put(attributeKey, value.getIntValue()); + case DOUBLE_VALUE -> builder.put(attributeKey, value.getDoubleValue()); + case ARRAY_VALUE -> builder.put(attributeKey, mergeArray(value.getArrayValue())); } } } @@ -270,38 +259,59 @@ public static Attributes toAttributes(List attributesList, Map toAttributeKey(KeyValue attribute) { String key = attribute.getKey(); AnyValue.ValueCase valueCase = attribute.getValue().getValueCase(); - switch (valueCase) { - case STRING_VALUE: - return AttributeKey.stringKey(key); - case BOOL_VALUE: - return AttributeKey.booleanKey(key); - case INT_VALUE: - return AttributeKey.longKey(key); - case DOUBLE_VALUE: - return AttributeKey.doubleKey(key); - case ARRAY_VALUE: - return AttributeKey.stringArrayKey(key); - } - return null; + return switch (valueCase) { + case STRING_VALUE -> AttributeKey.stringKey(key); + case BOOL_VALUE -> AttributeKey.booleanKey(key); + case INT_VALUE -> AttributeKey.longKey(key); + case DOUBLE_VALUE -> AttributeKey.doubleKey(key); + // Currently, OTel is not able to process arrayValue in attributes + case ARRAY_VALUE -> AttributeKey.stringKey(key); + default -> null; + }; } /** * @return Returns a {@link SpanKind} representing the given {@link Span.SpanKind} instance. */ private static SpanKind toSpanKind(Span.SpanKind spanKind) { - switch (spanKind) { - case SPAN_KIND_SERVER: - return SpanKind.SERVER; - case SPAN_KIND_CLIENT: - return SpanKind.CLIENT; - case SPAN_KIND_PRODUCER: - return SpanKind.PRODUCER; - case SPAN_KIND_CONSUMER: - return SpanKind.CONSUMER; - case SPAN_KIND_INTERNAL: - default: + return switch (spanKind) { + case SPAN_KIND_SERVER -> SpanKind.SERVER; + case SPAN_KIND_CLIENT -> SpanKind.CLIENT; + case SPAN_KIND_PRODUCER -> SpanKind.PRODUCER; + case SPAN_KIND_CONSUMER -> SpanKind.CONSUMER; + default -> // default value if we can not map - return SpanKind.INTERNAL; - } + SpanKind.INTERNAL; + }; + } + + /** + * Merges all values of an array. The values will always be converted to strings. + * Currently, OTel is not able to process arrayValue objects in Attributes. + * See issue + * + * @param arrayValue the array containing any values + * + * @return the merged string of all values + */ + private static String mergeArray(ArrayValue arrayValue) { + List values = arrayValue.getValuesList(); + String mergedString = values.stream() + .map(OcelotSpanUtils::getValueAsString) + .collect(Collectors.joining(", ")); + return mergedString; + } + + /** + * @return the {@link AnyValue} as string. + */ + private static String getValueAsString(AnyValue value) { + return switch (value.getValueCase()) { + case STRING_VALUE -> value.getStringValue(); + case INT_VALUE -> String.valueOf(value.getIntValue()); + case DOUBLE_VALUE -> String.valueOf(value.getDoubleValue()); + case BOOL_VALUE -> String.valueOf(value.getBoolValue()); + default -> ""; + }; } } diff --git a/src/test/java/io/opentelemetry/sdk/trace/OcelotSpanUtilsTest.java b/src/test/java/io/opentelemetry/sdk/trace/OcelotSpanUtilsTest.java index c3309a9..f878bab 100644 --- a/src/test/java/io/opentelemetry/sdk/trace/OcelotSpanUtilsTest.java +++ b/src/test/java/io/opentelemetry/sdk/trace/OcelotSpanUtilsTest.java @@ -1,12 +1,22 @@ package io.opentelemetry.sdk.trace; +import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.ArrayValue; +import io.opentelemetry.proto.common.v1.KeyValue; import io.opentelemetry.proto.trace.v1.Status; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class OcelotSpanUtilsTest { @@ -41,4 +51,120 @@ public void toStatusCode() { assertThat(OcelotSpanUtils.toStatusCode(Status.StatusCode.UNRECOGNIZED)).isNull(); } } -} \ No newline at end of file + + @Nested + class ToAttributes { + + @Test + public void verifyEmptyArray() { + Attributes attributes = OcelotSpanUtils.toAttributes(Collections.emptyList()); + + assertTrue(attributes.isEmpty()); + } + + @Test + public void verifyNullKeyValue() { + List keyValues = Collections.singletonList(null); + + Attributes attributes = OcelotSpanUtils.toAttributes(keyValues); + + assertTrue(attributes.isEmpty()); + } + + @Test + public void verifyEmptyKeyValue() { + KeyValue kv = KeyValue.newBuilder().build(); + List keyValues = Collections.singletonList(kv); + + Attributes attributes = OcelotSpanUtils.toAttributes(keyValues); + + assertTrue(attributes.isEmpty()); + } + + @Test + public void verifyNoValue() { + KeyValue kv = KeyValue.newBuilder() + .setKey("service.name") + .build(); + List keyValues = Collections.singletonList(kv); + + Attributes attributes = OcelotSpanUtils.toAttributes(keyValues); + + assertTrue(attributes.isEmpty()); + } + + @Test + public void verifyNoKey() { + KeyValue kv = KeyValue.newBuilder() + .setValue(AnyValue.newBuilder().setStringValue("frontend").build()) + .build(); + List keyValues = Collections.singletonList(kv); + + Attributes attributes = OcelotSpanUtils.toAttributes(keyValues); + + assertTrue(attributes.isEmpty()); + } + + @Test + public void verifyValidAttributes() { + KeyValue kvString = KeyValue.newBuilder() + .setKey("service.name") + .setValue(AnyValue.newBuilder().setStringValue("frontend").build()) + .build(); + KeyValue kvBool = KeyValue.newBuilder() + .setKey("browser.mobile") + .setValue(AnyValue.newBuilder().setBoolValue(false).build()) + .build(); + KeyValue kvInt = KeyValue.newBuilder() + .setKey("content_length") + .setValue(AnyValue.newBuilder().setIntValue(1000L).build()) + .build(); + KeyValue kvDouble = KeyValue.newBuilder() + .setKey("index") + .setValue(AnyValue.newBuilder().setDoubleValue(0.90).build()) + .build(); + KeyValue empty = KeyValue.newBuilder().build(); + List keyValues = List.of(kvString, kvBool, kvInt, kvDouble, empty); + + Attributes expected = Attributes.builder() + .put("service.name", "frontend") + .put("browser.mobile", false) + .put("content_length", 1000) + .put("index", 0.90) + .build(); + + Attributes attributes = OcelotSpanUtils.toAttributes(keyValues); + + assertEquals(expected, attributes); + } + + @Test + public void verifyMergedArrayValue() { + KeyValue kvArray = KeyValue.newBuilder() + .setKey("browser.brands") + .setValue(AnyValue.newBuilder().setArrayValue( + ArrayValue.newBuilder() + .addValues(AnyValue.newBuilder() + .setStringValue("Chrome") + .build()) + .addValues(AnyValue.newBuilder() + .setStringValue("Firefox") + .build()) + .addValues(AnyValue.newBuilder() + .setIntValue(100) + .build()) + .build()) + .build()) + .build(); + List keyValues = Collections.singletonList(kvArray); + + Attributes expected = Attributes.builder() + .put("browser.brands", "Chrome, Firefox, 100") + .build(); + + Attributes attributes = OcelotSpanUtils.toAttributes(keyValues); + + assertEquals(expected, attributes); + } + } +} diff --git a/src/test/java/rocks/inspectit/oce/eum/server/exporters/ExporterIntMockMvcTestBase.java b/src/test/java/rocks/inspectit/oce/eum/server/exporters/ExporterIntMockMvcTestBase.java index bf3e57a..63def14 100644 --- a/src/test/java/rocks/inspectit/oce/eum/server/exporters/ExporterIntMockMvcTestBase.java +++ b/src/test/java/rocks/inspectit/oce/eum/server/exporters/ExporterIntMockMvcTestBase.java @@ -1,14 +1,19 @@ package rocks.inspectit.oce.eum.server.exporters; +import com.google.common.io.CharStreams; import org.apache.http.NameValuePair; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import java.io.InputStreamReader; +import java.io.Reader; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -23,6 +28,9 @@ public class ExporterIntMockMvcTestBase { @Autowired protected MockMvc mockMvc; + @Value("classpath:ot-trace-array-v0.48.0.json") + private Resource resourceSpans; + public static final String SERVICE_NAME = "E2E-test"; final static String DEFAULT_TRACE_ID = "497d4e959f574a77d0d3abf05523ec5c"; @@ -31,6 +39,9 @@ public class ExporterIntMockMvcTestBase { static String SUT_URL = "http://test.com/login"; + // Trace-Id used in the resource spans + protected static String RESOURCE_TRACE_ID = "a4a68b53c52438381b6cb304410ff0be"; + protected static String FAKE_BEACON_KEY_NAME = "does_not_exist"; protected static String BEACON_PAGE_READY_TIME_KEY_NAME = "t_page"; @@ -45,6 +56,7 @@ public class ExporterIntMockMvcTestBase { protected static String METRIC_LOAD_TIME_KEY_NAME = "load_time/SUM"; protected static String METRIC_END_TIMESTAMP_KEY_NAME ="end_timestamp"; + /** * Sends a beacon to the mocked endpoint. */ @@ -84,6 +96,24 @@ protected void postSpan(String traceId) throws Exception { } + /** + * Posts a {@code Span} to {@link rocks.inspectit.oce.eum.server.rest.TraceController#spans(String)}. + * The span data will be read from a file. + *
+ * Currently, OTel is not able to process arrayValue objects in Attributes. + * Instead, all values will be merged to one string. + * See issue + * + */ + protected void postResourceSpans() throws Exception { + try (Reader reader = new InputStreamReader(resourceSpans.getInputStream())) { + String json = CharStreams.toString(reader); + + mockMvc.perform(post("/spans").contentType(MediaType.APPLICATION_JSON).content(json)) + .andExpect(status().isAccepted()); + } + } + /** * Returns a request string for a {@code Span} * diff --git a/src/test/java/rocks/inspectit/oce/eum/server/exporters/configuration/TraceExportersConfigurationOtlpTest.java b/src/test/java/rocks/inspectit/oce/eum/server/exporters/configuration/TraceExportersConfigurationOtlpTest.java index d6b66a4..f9e0dab 100644 --- a/src/test/java/rocks/inspectit/oce/eum/server/exporters/configuration/TraceExportersConfigurationOtlpTest.java +++ b/src/test/java/rocks/inspectit/oce/eum/server/exporters/configuration/TraceExportersConfigurationOtlpTest.java @@ -1,6 +1,5 @@ package rocks.inspectit.oce.eum.server.exporters.configuration; -import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; diff --git a/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/JaegerGrpcExporterIntTest.java b/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/JaegerGrpcExporterIntTest.java index 8ac5aab..4cef4ff 100644 --- a/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/JaegerGrpcExporterIntTest.java +++ b/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/JaegerGrpcExporterIntTest.java @@ -24,9 +24,15 @@ public void initialize(ConfigurableApplicationContext applicationContext) { } @Test - void verifyTraceSentGrpc() throws Exception { + void verifyTraceSentGrpcWithJaeger() throws Exception { String grpcTraceId = "497d4e959f574a77d0d3abf05523ec5a"; postSpan(grpcTraceId); awaitSpansExported(grpcTraceId); } + + @Test + void verifyTraceWithArrayValueSentGrpcWithJaeger() throws Exception { + postResourceSpans(); + awaitSpansExported(RESOURCE_TRACE_ID); + } } diff --git a/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/JaegerHttpExporterIntTest.java b/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/JaegerHttpExporterIntTest.java index 288f5e2..940ad7b 100644 --- a/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/JaegerHttpExporterIntTest.java +++ b/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/JaegerHttpExporterIntTest.java @@ -24,9 +24,15 @@ public void initialize(ConfigurableApplicationContext applicationContext) { } @Test - void verifyTraceSentHttp() throws Exception { + void verifyTraceSentHttpWithJaeger() throws Exception { String grpcTraceId = "497d4e959f574a77d0d3abf05523ec5d"; postSpan(grpcTraceId); awaitSpansExported(grpcTraceId); } -} \ No newline at end of file + + @Test + void verifyTraceWithArrayValueSentGrpcWithJaeger() throws Exception { + postResourceSpans(); + awaitSpansExported(RESOURCE_TRACE_ID); + } +} diff --git a/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/OtlpGrpcTraceExporterIntTest.java b/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/OtlpGrpcTraceExporterIntTest.java index 2fb9253..e10158f 100644 --- a/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/OtlpGrpcTraceExporterIntTest.java +++ b/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/OtlpGrpcTraceExporterIntTest.java @@ -26,9 +26,15 @@ public void initialize(ConfigurableApplicationContext applicationContext) { } @Test - void verifyTraceSentGrpc() throws Exception { + void verifyTraceSentGrpcWithOtlp() throws Exception { String grpcTraceId = "497d4e959f574a77d0d3abf05523ec5a"; postSpan(grpcTraceId); awaitSpansExported(grpcTraceId); } + + @Test + void verifyTraceWithArrayValueSentGrpcWithOtlp() throws Exception { + postResourceSpans(); + awaitSpansExported(RESOURCE_TRACE_ID); + } } diff --git a/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/OtlpHttpTraceExporterIntTest.java b/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/OtlpHttpTraceExporterIntTest.java index c185f88..89ca186 100644 --- a/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/OtlpHttpTraceExporterIntTest.java +++ b/src/test/java/rocks/inspectit/oce/eum/server/exporters/tracing/OtlpHttpTraceExporterIntTest.java @@ -26,9 +26,15 @@ public void initialize(ConfigurableApplicationContext applicationContext) { } @Test - void verifyTraceSentHttp() throws Exception { + void verifyTraceSentHttpWithOtlp() throws Exception { String httpTraceId = "497d4e959f574a77d0d3abf05523ec5b"; postSpan(httpTraceId); awaitSpansExported(httpTraceId); } + + @Test + void verifyTraceWithArrayValueSentHttpWithOtlp() throws Exception { + postResourceSpans(); + awaitSpansExported(RESOURCE_TRACE_ID); + } } diff --git a/src/test/java/rocks/inspectit/oce/eum/server/rest/TraceControllerIntTest.java b/src/test/java/rocks/inspectit/oce/eum/server/rest/TraceControllerIntTest.java index 6252d18..0ee3e30 100644 --- a/src/test/java/rocks/inspectit/oce/eum/server/rest/TraceControllerIntTest.java +++ b/src/test/java/rocks/inspectit/oce/eum/server/rest/TraceControllerIntTest.java @@ -5,7 +5,6 @@ import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SpanExporter; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -34,57 +33,78 @@ public class TraceControllerIntTest { @Autowired TestRestTemplate restTemplate; - @Value("classpath:ot-trace-large-v0.18.2.json") - private Resource resource; + @Value("classpath:ot-trace-large-v0.48.0.json") + private Resource resourceSpan; + + @Value("classpath:ot-trace-array-v0.48.0.json") + private Resource resourceSpans; @MockBean SpanExporter spanExporter; - @Nested - class Spans { + @Captor + ArgumentCaptor> spanCaptor; - @Captor - ArgumentCaptor> spanCaptor; + @Test + public void empty() { + ResponseEntity result = restTemplate.postForEntity("/spans", null, Void.class); - @Test - public void empty() { - ResponseEntity result = restTemplate.postForEntity("/spans", null, Void.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } - assertThat(result.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - } + @Test + public void badData() { + ResponseEntity result = restTemplate.postForEntity("/spans", "{\"bad\": \"data'\"}", Void.class); - @Test - public void badData() { - ResponseEntity result = restTemplate.postForEntity("/spans", "{\"bad\": \"data'\"}", Void.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } - assertThat(result.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + @Test + public void verifyLargeTrace() throws Exception { + try (Reader reader = new InputStreamReader(resourceSpan.getInputStream())) { + String json = CharStreams.toString(reader); + + ResponseEntity result = restTemplate.postForEntity("/spans", json, Void.class); + + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); + + verify(spanExporter).export(spanCaptor.capture()); + assertThat(spanCaptor.getValue()).hasSize(1).allSatisfy(data -> { + assertThat(data.getTraceId()).isEqualTo("03c2a546267d1e90d70269bdc02babef"); + assertThat(data.getSpanId()).isEqualTo("c29e6dd2a1e1e7ae"); + assertThat(data.getParentSpanId()).isEqualTo("915c20356ab50086"); + assertThat(data.getKind()).isEqualTo(SpanKind.CLIENT); + assertThat(data.getName()).isEqualTo("HTTP GET"); + assertThat(data.getStartEpochNanos()).isEqualTo(1619166153906575000L); + assertThat(data.getEndEpochNanos()).isEqualTo(1619166154225390000L); + assertThat(data.hasEnded()).isTrue(); + assertThat(data.getAttributes().asMap().keySet()).extracting(AttributeKey::getKey) + .containsExactlyInAnyOrder("client.ip", "http.response_content_length", "is.true", "http.method"); + assertThat(data.getEvents()).hasSize(1); + assertThat(data.getLinks()).isEmpty(); + }); } + } - @Test - public void happyPath() throws Exception { - try (Reader reader = new InputStreamReader(resource.getInputStream())) { - String json = CharStreams.toString(reader); - - ResponseEntity result = restTemplate.postForEntity("/spans", json, Void.class); - - assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); - - verify(spanExporter).export(spanCaptor.capture()); - assertThat(spanCaptor.getValue()).hasSize(1).allSatisfy(data -> { - assertThat(data.getTraceId()).isEqualTo("03c2a546267d1e90d70269bdc02babef"); - assertThat(data.getSpanId()).isEqualTo("c29e6dd2a1e1e7ae"); - assertThat(data.getParentSpanId()).isEqualTo("915c20356ab50086"); - assertThat(data.getKind()).isEqualTo(SpanKind.CLIENT); - assertThat(data.getName()).isEqualTo("HTTP GET"); - assertThat(data.getStartEpochNanos()).isEqualTo(1619166153906575000L); - assertThat(data.getEndEpochNanos()).isEqualTo(1619166154225390000L); - assertThat(data.hasEnded()).isTrue(); - assertThat(data.getAttributes().asMap().keySet()).extracting(AttributeKey::getKey) - .containsExactlyInAnyOrder("client.ip", "http.response_content_length", "is.true", "http.method"); - assertThat(data.getEvents()).hasSize(1); - assertThat(data.getLinks()).isEmpty(); - }); - } + @Test + public void verifyTraceWithMultipleResourceSpans() throws Exception { + try (Reader reader = new InputStreamReader(resourceSpans.getInputStream())) { + String json = CharStreams.toString(reader); + + ResponseEntity result = restTemplate.postForEntity("/spans", json, Void.class); + + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); + verify(spanExporter).export(spanCaptor.capture()); + assertThat(spanCaptor.getValue()).hasSize(2).allSatisfy(data -> { + assertThat(data.getTraceId()).isEqualTo("a4a68b53c52438381b6cb304410ff0be"); + assertThat(data.getSpanId()).isNotBlank(); + assertThat(data.getName()).isNotBlank(); + assertThat(data.getKind()).isNotNull(); + assertThat(data.getAttributes()).isNotNull(); + assertThat(data.getEvents()).isNotNull(); + assertThat(data.getResource()).isNotNull(); + assertThat(data.hasEnded()).isTrue(); + }); } } -} \ No newline at end of file +} diff --git a/src/test/java/rocks/inspectit/oce/eum/server/tracing/opentelemtry/OpenTelemetryProtoConverterTest.java b/src/test/java/rocks/inspectit/oce/eum/server/tracing/opentelemtry/OpenTelemetryProtoConverterTest.java index 790b2a7..2cdfb3f 100644 --- a/src/test/java/rocks/inspectit/oce/eum/server/tracing/opentelemtry/OpenTelemetryProtoConverterTest.java +++ b/src/test/java/rocks/inspectit/oce/eum/server/tracing/opentelemtry/OpenTelemetryProtoConverterTest.java @@ -27,6 +27,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collection; +import java.util.List; import java.util.Map; import static io.opentelemetry.api.common.AttributeKey.*; @@ -36,9 +37,11 @@ @ExtendWith(MockitoExtension.class) class OpenTelemetryProtoConverterTest { - public static final String TRACE_REQUEST_FILE_SMALL = "/ot-trace-small-v0.18.2.json"; + public static final String TRACE_REQUEST_FILE_SMALL = "/ot-trace-small-v0.48.0.json"; - public static final String TRACE_REQUEST_FILE_LARGE = "/ot-trace-large-v0.18.2.json"; + public static final String TRACE_REQUEST_FILE_LARGE = "/ot-trace-large-v0.48.0.json"; + + public static final String TRACE_REQUEST_FILE_ARRAY = "/ot-trace-array-v0.48.0.json"; @InjectMocks private OpenTelemetryProtoConverter converter; @@ -54,6 +57,10 @@ private ExportTraceServiceRequest getLargeTestRequest() throws Exception { return getTestRequest(TRACE_REQUEST_FILE_LARGE); } + private ExportTraceServiceRequest getArrayTestRequest() throws Exception { + return getTestRequest(TRACE_REQUEST_FILE_ARRAY); + } + private ExportTraceServiceRequest getTestRequest(String file) throws Exception { InputStream resource = this.getClass().getResourceAsStream(file); String traceRequestJson = IOUtils.toString(resource, StandardCharsets.UTF_8); @@ -144,6 +151,25 @@ public void convertLargeRequest() throws Exception { //@formatter:on } + @Test + void convertArrayRequest() throws Exception { + ExportTraceServiceRequest request = getArrayTestRequest(); + + Collection result = converter.convert(request); + List resultList = result.stream().toList(); + + assertThat(resultList).hasSize(2).allSatisfy(data -> { + assertThat(data.getTraceId()).isEqualTo("a4a68b53c52438381b6cb304410ff0be"); + assertThat(data.getSpanId()).isNotBlank(); + assertThat(data.getName()).isNotBlank(); + assertThat(data.getKind()).isNotNull(); + assertThat(data.getAttributes()).isNotNull(); + assertThat(data.getEvents()).isNotNull(); + assertThat(data.getResource()).isNotNull(); + assertThat(data.hasEnded()).isTrue(); + }); + } + @Test public void emptyIgnored() { ExportTraceServiceRequest data = ExportTraceServiceRequest.newBuilder() diff --git a/src/test/resources/ot-trace-array-v0.48.0.json b/src/test/resources/ot-trace-array-v0.48.0.json new file mode 100644 index 0000000..848e860 --- /dev/null +++ b/src/test/resources/ot-trace-array-v0.48.0.json @@ -0,0 +1,347 @@ +{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "testhtml-Frontend" + } + }, + { + "key": "telemetry.sdk.language", + "value": { + "stringValue": "webjs" + } + }, + { + "key": "telemetry.sdk.name", + "value": { + "stringValue": "opentelemetry" + } + }, + { + "key": "telemetry.sdk.version", + "value": { + "stringValue": "1.21.0" + } + }, + { + "key": "browser.platform", + "value": { + "stringValue": "Windows" + } + }, + { + "key": "browser.brands", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "Not A(Brand 99" + }, + { + "stringValue": "Google Chrome 121" + }, + { + "stringValue": "Chromium 121" + } + ] + } + } + }, + { + "key": "browser.mobile", + "value": { + "boolValue": false + } + }, + { + "key": "browser.language", + "value": { + "stringValue": "de-DE" + } + } + ], + "droppedAttributesCount": 0 + }, + "scopeSpans": [ + { + "scope": { + "name": "@opentelemetry/instrumentation-document-load", + "version": "0.35.0" + }, + "spans": [ + { + "traceId": "a4a68b53c52438381b6cb304410ff0be", + "spanId": "fe097515e5bb30ae", + "parentSpanId": "781d1d3e35febc94", + "name": "documentFetch", + "kind": 1, + "startTimeUnixNano": "1708521891468900000", + "endTimeUnixNano": "1708521891477000000", + "attributes": [ + { + "key": "application", + "value": { + "stringValue": "testhtml-Frontend" + } + }, + { + "key": "client_user_agent_original", + "value": { + "stringValue": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" + } + }, + { + "key": "http.url", + "value": { + "stringValue": "inspectit-eum.de/testeum.html" + } + }, + { + "key": "http.response_content_length", + "value": { + "intValue": 7783 + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "attributes": [], + "name": "fetchStart", + "timeUnixNano": "1708521891468900000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "domainLookupStart", + "timeUnixNano": "1708521891468900000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "domainLookupEnd", + "timeUnixNano": "1708521891468900000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "connectStart", + "timeUnixNano": "1708521891468900000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "connectEnd", + "timeUnixNano": "1708521891468900000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "requestStart", + "timeUnixNano": "1708521891471200000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "responseStart", + "timeUnixNano": "1708521891476300000", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "responseEnd", + "timeUnixNano": "1708521891477000000", + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0 + } + ] + } + ] + }, + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "testhtml-Frontend" + } + }, + { + "key": "telemetry.sdk.language", + "value": { + "stringValue": "webjs" + } + }, + { + "key": "telemetry.sdk.name", + "value": { + "stringValue": "opentelemetry" + } + }, + { + "key": "telemetry.sdk.version", + "value": { + "stringValue": "1.21.0" + } + }, + { + "key": "browser.platform", + "value": { + "stringValue": "Windows" + } + }, + { + "key": "browser.brands", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "Not A(Brand 99" + }, + { + "stringValue": "Google Chrome 121" + }, + { + "stringValue": "Chromium 121" + } + ] + } + } + }, + { + "key": "browser.mobile", + "value": { + "boolValue": false + } + }, + { + "key": "browser.language", + "value": { + "stringValue": "de-DE" + } + } + ], + "droppedAttributesCount": 0 + }, + "scopeSpans": [ + { + "scope": { + "name": "@opentelemetry/instrumentation-document-load", + "version": "0.35.0" + }, + "spans": [ + { + "traceId": "a4a68b53c52438381b6cb304410ff0be", + "spanId": "781d1d3e35febc94", + "name": "documentLoad", + "kind": 1, + "startTimeUnixNano": "1708521891469599951", + "endTimeUnixNano": "1708521892682999951", + "attributes": [ + { + "key": "application", + "value": { + "stringValue": "testhtml-Frontend" + } + }, + { + "key": "client_user_agent_original", + "value": { + "stringValue": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" + } + }, + { + "key": "http.url", + "value": { + "stringValue": "inspectit-eum.de/testeum.html" + } + }, + { + "key": "http.user_agent", + "value": { + "stringValue": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "attributes": [], + "name": "fetchStart", + "timeUnixNano": "1708521891469599951", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "unloadEventStart", + "timeUnixNano": "1708521891483099951", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "unloadEventEnd", + "timeUnixNano": "1708521891483499951", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "domInteractive", + "timeUnixNano": "1708521892680699951", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "domContentLoadedEventStart", + "timeUnixNano": "1708521892680899951", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "domContentLoadedEventEnd", + "timeUnixNano": "1708521892681399951", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "domComplete", + "timeUnixNano": "1708521892682699951", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "loadEventStart", + "timeUnixNano": "1708521892682699951", + "droppedAttributesCount": 0 + }, + { + "attributes": [], + "name": "loadEventEnd", + "timeUnixNano": "1708521892682999951", + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0 + } + ] + } + ] + } + ] +} diff --git a/src/test/resources/ot-trace-large-v0.18.2.json b/src/test/resources/ot-trace-large-v0.48.0.json similarity index 100% rename from src/test/resources/ot-trace-large-v0.18.2.json rename to src/test/resources/ot-trace-large-v0.48.0.json diff --git a/src/test/resources/ot-trace-small-v0.18.2.json b/src/test/resources/ot-trace-small-v0.48.0.json similarity index 100% rename from src/test/resources/ot-trace-small-v0.18.2.json rename to src/test/resources/ot-trace-small-v0.48.0.json