diff --git a/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java b/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java index 0a96cd0f78ef0d..c7469f1b83aa2e 100644 --- a/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java +++ b/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java @@ -58,7 +58,9 @@ void registerOpenTelemetryContextStorage( @BuildStep(onlyIf = OpenTelemetryEnabled.class) @Record(ExecutionTime.STATIC_INIT) - void createOpenTelemetry(OpenTelemetryRecorder recorder, Optional tracerProviderBuildItem, + void createOpenTelemetry(OpenTelemetryConfig openTelemetryConfig, + OpenTelemetryRecorder recorder, + Optional tracerProviderBuildItem, LaunchModeBuildItem launchMode) { if (launchMode.getLaunchMode() == LaunchMode.DEVELOPMENT) { recorder.resetGlobalOpenTelemetryForDevMode(); @@ -66,7 +68,7 @@ void createOpenTelemetry(OpenTelemetryRecorder recorder, Optional tracerProvider = tracerProviderBuildItem.map(TracerProviderBuildItem::getTracerProvider) .orElse(null); - recorder.createOpenTelemetry(tracerProvider); + recorder.createOpenTelemetry(tracerProvider, openTelemetryConfig); recorder.eagerlyCreateContextStorage(); } diff --git a/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java b/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java index d7e743ccd55887..f5510e98c0d571 100644 --- a/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java +++ b/extensions/opentelemetry/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java @@ -113,11 +113,13 @@ VertxOptionsConsumerBuildItem vertxTracingOptions(TracerRecorder recorder) { @Record(ExecutionTime.STATIC_INIT) TracerProviderBuildItem createTracerProvider(TracerRecorder recorder, ApplicationInfoBuildItem appInfo, + OpenTelemetryConfig config, ShutdownContextBuildItem shutdownContext, BeanContainerBuildItem beanContainerBuildItem) { String serviceName = appInfo.getName(); String serviceVersion = appInfo.getVersion(); - return new TracerProviderBuildItem(recorder.createTracerProvider(serviceName, serviceVersion, shutdownContext)); + return new TracerProviderBuildItem( + recorder.createTracerProvider(config.tracer, serviceName, serviceVersion, shutdownContext)); } @BuildStep(onlyIf = TracerEnabled.class) diff --git a/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/VertxOpenTelemetryTest.java b/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/VertxOpenTelemetryTest.java index f1adddbb6d747f..209cae8fd725db 100644 --- a/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/VertxOpenTelemetryTest.java +++ b/extensions/opentelemetry/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/VertxOpenTelemetryTest.java @@ -1,6 +1,7 @@ package io.quarkus.opentelemetry.deployment; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.semconv.resource.attributes.ResourceAttributes.SERVICE_NAME; import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_CLIENT_IP; import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_FLAVOR; import static io.opentelemetry.semconv.trace.attributes.SemanticAttributes.HTTP_HOST; @@ -12,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -21,12 +23,18 @@ import javax.enterprise.event.Observes; import javax.inject.Inject; +import org.hamcrest.MatcherAssert; +import org.hamcrest.collection.ArrayMatching; import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SpanExporter; @@ -38,19 +46,29 @@ public class VertxOpenTelemetryTest { @RegisterExtension static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .overrideConfigKey("quarkus.opentelemetry.propagators", "trace-context,baggage") + .overrideConfigKey("quarkus.opentelemetry.tracer.resource-attributes", "service.name=test") .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); @Inject MyExporter myExporter; + @Inject + OpenTelemetry openTelemetry; + @Test - void trace() { + void trace() throws NoSuchFieldException, IllegalAccessException { RestAssured.when().get("/tracer").then() .statusCode(200) .body(is("Hello Tracer!")); List spans = myExporter.getFinishedSpanItems(); + TextMapPropagator textMapPropagator = openTelemetry.getPropagators().getTextMapPropagator(); + Field privatePropagatorsField = textMapPropagator.getClass().getDeclaredField("textPropagators"); + privatePropagatorsField.setAccessible(true); + TextMapPropagator[] textMapPropagators = (TextMapPropagator[]) privatePropagatorsField.get(textMapPropagator); + assertEquals(2, spans.size()); assertEquals("io.quarkus.vertx.opentelemetry", spans.get(0).getName()); assertEquals("hello!", spans.get(0).getAttributes().get(stringKey("test.message"))); @@ -60,6 +78,10 @@ void trace() { assertEquals("http", spans.get(1).getAttributes().get(HTTP_SCHEME)); assertEquals("localhost:8081", spans.get(1).getAttributes().get(HTTP_HOST)); assertEquals("127.0.0.1", spans.get(1).getAttributes().get(HTTP_CLIENT_IP)); + assertEquals("test", spans.get(1).getResource().getAttributes().get(SERVICE_NAME)); + MatcherAssert.assertThat(textMapPropagators, + ArrayMatching.arrayContainingInAnyOrder(W3CTraceContextPropagator.getInstance(), + W3CBaggagePropagator.getInstance())); assertNotNull(spans.get(1).getAttributes().get(HTTP_USER_AGENT)); } diff --git a/extensions/opentelemetry/opentelemetry/runtime/pom.xml b/extensions/opentelemetry/opentelemetry/runtime/pom.xml index e4fde0e8f6ed11..1328462dd2548d 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/pom.xml +++ b/extensions/opentelemetry/opentelemetry/runtime/pom.xml @@ -40,10 +40,30 @@ true + + io.opentelemetry + opentelemetry-extension-aws + provided + + + io.opentelemetry + opentelemetry-extension-trace-propagators + provided + io.opentelemetry opentelemetry-sdk + + io.opentelemetry + opentelemetry-sdk-extension-aws + provided + + + io.opentelemetry + opentelemetry-sdk-extension-resources + provided + io.opentelemetry opentelemetry-semconv diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/ClassUtil.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/ClassUtil.java new file mode 100644 index 00000000000000..089901e4b0b0e8 --- /dev/null +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/ClassUtil.java @@ -0,0 +1,44 @@ +package io.quarkus.opentelemetry.runtime; + +public final class ClassUtil { + private ClassUtil() { + } + + public static void checkAwsSdkExtension(String feature) { + ClassUtil.checkClassExists("io.opentelemetry.sdk.extension.aws.resource.BeanstalkResource", + feature, + "opentelemetry-sdk-extension-aws"); + } + + public static void checkAwsExtension(String feature) { + ClassUtil.checkClassExists("io.opentelemetry.extension.aws.AwsXrayPropagator", + feature, + "opentelemetry-extension-aws"); + } + + public static void checkPropagatorsExtension(String feature) { + ClassUtil.checkClassExists( + "io.opentelemetry.extension.trace.propagation.B3Propagator", + feature, + "opentelemetry-extension-trace-propagators"); + } + + public static void checkResourcesExtension(String feature) { + ClassUtil.checkClassExists("io.opentelemetry.sdk.extension.resources.HostResource", + feature, + "opentelemetry-sdk-extension-resources"); + } + + public static void checkClassExists(String className, String featureName, String requiredLibrary) { + try { + Class.forName(className, true, Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException unused) { + throw new IllegalArgumentException( + featureName + + " enabled but " + + requiredLibrary + + " not found on classpath. " + + "Make sure to add it as a dependency to enable this feature."); + } + } +} diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryConfig.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryConfig.java index 163fafd2f8f7e9..bdfee0b25ec787 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryConfig.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryConfig.java @@ -1,5 +1,7 @@ package io.quarkus.opentelemetry.runtime; +import java.util.List; + import io.quarkus.opentelemetry.runtime.tracing.TracerConfig; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; @@ -15,6 +17,26 @@ public final class OpenTelemetryConfig { @ConfigItem(defaultValue = "true") public boolean enabled; + /** + * Comma separated list of OpenTelemetry propagators which must be supported. + *

+ * Valid values are {@code b3, b3-multi, baggage, jaeger, ot-trace, trace-context, xray}. + *

+ * Default value is {@code traceContext} + */ + @ConfigItem(defaultValue = "traceContext") + public List propagators; + /** Build / static runtime config for tracer */ public TracerConfig tracer; + + public enum Propagator { + B3, + B3_MULTI, + BAGGAGE, + JAEGER, + OT_TRACE, + TRACE_CONTEXT, + XRAY + } } diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java index f427ee304d8b1c..a45aeaf3e6ba2d 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryRecorder.java @@ -1,11 +1,12 @@ package io.quarkus.opentelemetry.runtime; import java.util.function.Supplier; +import java.util.stream.Collectors; import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.ContextStorage; import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.OpenTelemetrySdkBuilder; import io.opentelemetry.sdk.trace.SdkTracerProvider; @@ -23,7 +24,7 @@ public void resetGlobalOpenTelemetryForDevMode() { } /* STATIC INIT */ - public void createOpenTelemetry(RuntimeValue tracerProvider) { + public void createOpenTelemetry(RuntimeValue tracerProvider, OpenTelemetryConfig openTelemetryConfig) { OpenTelemetrySdkBuilder builder = OpenTelemetrySdk.builder(); // Set tracer provider if present @@ -31,8 +32,11 @@ public void createOpenTelemetry(RuntimeValue tracerProvider) builder.setTracerProvider(tracerProvider.getValue()); } - // Add propagators. //TODO Need a way to handle this with config - builder.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())); + builder.setPropagators( + ContextPropagators.create( + TextMapPropagator.composite(openTelemetryConfig.propagators.stream() + .map(OpenTelemetryUtil::mapPropagator) + .collect(Collectors.toSet())))); builder.buildAndRegisterGlobal(); } diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryUtil.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryUtil.java new file mode 100644 index 00000000000000..cf14243ec831dc --- /dev/null +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/OpenTelemetryUtil.java @@ -0,0 +1,46 @@ +package io.quarkus.opentelemetry.runtime; + +import static io.quarkus.opentelemetry.runtime.OpenTelemetryConfig.Propagator.BAGGAGE; +import static io.quarkus.opentelemetry.runtime.OpenTelemetryConfig.Propagator.TRACE_CONTEXT; +import static io.quarkus.opentelemetry.runtime.OpenTelemetryConfig.Propagator.XRAY; + +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.extension.aws.AwsXrayPropagator; +import io.opentelemetry.extension.trace.propagation.B3Propagator; +import io.opentelemetry.extension.trace.propagation.JaegerPropagator; +import io.opentelemetry.extension.trace.propagation.OtTracePropagator; + +public final class OpenTelemetryUtil { + private OpenTelemetryUtil() { + } + + public static TextMapPropagator mapPropagator(OpenTelemetryConfig.Propagator propagator) { + if (BAGGAGE == propagator) { + return W3CBaggagePropagator.getInstance(); + } + if (TRACE_CONTEXT == propagator) { + return W3CTraceContextPropagator.getInstance(); + } + if (XRAY == propagator) { + ClassUtil.checkAwsExtension(propagator.name() + " propagator"); + return AwsXrayPropagator.getInstance(); + } + // Other propagators are in the extension artifact. Check one of the propagators. + ClassUtil.checkPropagatorsExtension(propagator.name() + " propagator"); + + switch (propagator) { + case B3: + return B3Propagator.injectingSingleHeader(); + case B3_MULTI: + return B3Propagator.injectingMultiHeaders(); + case JAEGER: + return JaegerPropagator.getInstance(); + case OT_TRACE: + return OtTracePropagator.getInstance(); + default: + throw new IllegalStateException("Unrecognized value for propagator: " + propagator); + } + } +} diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerConfig.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerConfig.java index 05f34bb968f466..86b91ea57aa40c 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerConfig.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerConfig.java @@ -1,5 +1,6 @@ package io.quarkus.opentelemetry.runtime.tracing; +import java.util.List; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigGroup; @@ -19,8 +20,51 @@ public class TracerConfig { /** Build / static runtime config for span exporters */ public SpanExporterConfig exporter; + /** + * Tracing ID generator. + *

+ * Valid values are {@code xray}. + */ + @ConfigItem + public Optional idGenerator; + + /** + * A comma separated list of name = value resource attributes that + * represents the entity producing telemetry + * (eg. {@code service.name=authservice}). + */ + @ConfigItem + public Optional> resourceAttributes; + + /** + * Comma separated list of resources that represents the entity that is + * producing telemetry. + *

+ * Valid values are {@code beanstalk, ec2, ecs, eks, host, lambda, os, + * process, process_runtime}. + */ + @ConfigItem + public Optional> resources; + /** Build / static runtime config for span exporters */ @ConfigGroup public static class SpanExporterConfig { } + + public enum IdGenerator { + DEFAULT, + XRAY + } + + public enum Resource { + BEANSTALK, + EC2, + ECS, + EKS, + HOST, + LAMBDA, + OS, + PROCESS, + PROCESS_RUNTIME + } } diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java index 673296ab0ffa6f..5ecebb380120ef 100644 --- a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerRecorder.java @@ -31,19 +31,29 @@ public Consumer setVertxTracingOptions() { } /* STATIC INIT */ - public RuntimeValue createTracerProvider(String serviceName, String serviceVersion, + public RuntimeValue createTracerProvider(TracerConfig tracerConfig, + String serviceName, + String serviceVersion, ShutdownContext shutdownContext) { BeanManager beanManager = Arc.container().beanManager(); SdkTracerProviderBuilder builder = SdkTracerProvider.builder(); + // Define ID Generator if present + tracerConfig.idGenerator.map(TracerUtil::mapIdGenerator).ifPresent(builder::setIdGenerator); + // Define Service Resource builder.setResource( Resource.getDefault() - .merge( - Resource.create( - Attributes.of( - ResourceAttributes.SERVICE_NAME, serviceName, - ResourceAttributes.SERVICE_VERSION, serviceVersion)))); + .merge(Resource.create( + Attributes.of( + ResourceAttributes.SERVICE_NAME, serviceName, + ResourceAttributes.SERVICE_VERSION, serviceVersion))) + .merge(tracerConfig.resources.map(r -> r.stream() + .map(TracerUtil::mapResource) + .reduce(Resource.empty(), Resource::merge)) + .orElseGet(Resource::empty)) + .merge(tracerConfig.resourceAttributes.map(TracerUtil::mapResourceAttributes) + .orElseGet(Resource::empty))); // Find all SpanExporter instances Instance allExporters = beanManager.createInstance() diff --git a/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java new file mode 100644 index 00000000000000..1c959391e55553 --- /dev/null +++ b/extensions/opentelemetry/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/TracerUtil.java @@ -0,0 +1,80 @@ +package io.quarkus.opentelemetry.runtime.tracing; + +import java.util.AbstractMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.extension.aws.resource.BeanstalkResource; +import io.opentelemetry.sdk.extension.aws.resource.Ec2Resource; +import io.opentelemetry.sdk.extension.aws.resource.EcsResource; +import io.opentelemetry.sdk.extension.aws.resource.EksResource; +import io.opentelemetry.sdk.extension.aws.resource.LambdaResource; +import io.opentelemetry.sdk.extension.aws.trace.AwsXrayIdGenerator; +import io.opentelemetry.sdk.extension.resources.HostResource; +import io.opentelemetry.sdk.extension.resources.OsResource; +import io.opentelemetry.sdk.extension.resources.ProcessResource; +import io.opentelemetry.sdk.extension.resources.ProcessRuntimeResource; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.IdGenerator; +import io.quarkus.opentelemetry.runtime.ClassUtil; + +public final class TracerUtil { + private TracerUtil() { + } + + private static final List RESOURCE_EXTENSION_RESOURCES = List.of(TracerConfig.Resource.HOST, + TracerConfig.Resource.OS, TracerConfig.Resource.PROCESS, TracerConfig.Resource.PROCESS_RUNTIME); + + public static IdGenerator mapIdGenerator(TracerConfig.IdGenerator idGenerator) { + ClassUtil.checkAwsSdkExtension(idGenerator.name() + " resource"); + return AwsXrayIdGenerator.getInstance(); + } + + public static Resource mapResource(TracerConfig.Resource resource) { + if (RESOURCE_EXTENSION_RESOURCES.contains(resource)) { + ClassUtil.checkResourcesExtension(resource.name() + " resource"); + switch (resource) { + case HOST: + return HostResource.get(); + case OS: + return OsResource.get(); + case PROCESS: + return ProcessResource.get(); + case PROCESS_RUNTIME: + return ProcessRuntimeResource.get(); + } + } + // All the other possible resources are AWS + ClassUtil.checkAwsSdkExtension(resource.name() + " resource"); + switch (resource) { + case BEANSTALK: + return BeanstalkResource.get(); + case EC2: + return Ec2Resource.get(); + case ECS: + return EcsResource.get(); + case EKS: + return EksResource.get(); + case LAMBDA: + return LambdaResource.get(); + default: + throw new IllegalStateException("Unrecognized value for resource: " + resource); + } + } + + public static Resource mapResourceAttributes(List resourceAttributes) { + AttributesBuilder attributesBuilder = Attributes.builder(); + + resourceAttributes.stream() + .map(keyValuePair -> keyValuePair.split("=", 2)) + .map(keyValuePair -> new AbstractMap.SimpleImmutableEntry<>(keyValuePair[0], keyValuePair[1])) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (first, next) -> next, LinkedHashMap::new)) + .forEach(attributesBuilder::put); + + return Resource.create(attributesBuilder.build()); + } +}