properties = Map.of(
+ "quarkus.jaeger.port", "" + jaegerContainer.getMappedPort(QUERY_PORT),
+ "quarkus.opentelemetry.tracer.exporter.jaeger.endpoint", "http://localhost:" +
+ jaegerContainer.getMappedPort(COLLECTOR_PORT));
+ return properties;
+ }
+
+ @Override
+ public void stop() {
+ jaegerContainer.stop();
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-jaeger/pom.xml b/quarkus-opentelemetry-exporter-jaeger/pom.xml
new file mode 100644
index 0000000..72e0e3c
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-jaeger/pom.xml
@@ -0,0 +1,23 @@
+
+
+ 4.0.0
+
+ io.quarkiverse.opentelemetry.exporter
+ quarkus-opentelemetry-exporter-parent
+ 999-SNAPSHOT
+
+
+ io.quarkiverse.opentelemetry.exporter
+ quarkus-opentelemetry-exporter-jaeger-parent
+ pom
+ Quarkus Opentelemetry Exporter - Jaeger
+
+
+ runtime
+ deployment
+ integration-tests
+
+
+
diff --git a/quarkus-opentelemetry-exporter-jaeger/runtime/pom.xml b/quarkus-opentelemetry-exporter-jaeger/runtime/pom.xml
new file mode 100644
index 0000000..bbac64d
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-jaeger/runtime/pom.xml
@@ -0,0 +1,103 @@
+
+
+ 4.0.0
+
+ io.quarkiverse.opentelemetry.exporter
+ quarkus-opentelemetry-exporter-jaeger-parent
+ 999-SNAPSHOT
+
+
+ quarkus-opentelemetry-exporter-jaeger
+ Quarkus Opentelemetry Exporter Jaeger - Runtime
+
+
+
+ io.quarkus
+ quarkus-core
+
+
+ io.quarkus
+ quarkus-arc
+
+
+
+ io.quarkus
+ quarkus-opentelemetry
+
+
+ io.quarkus
+ quarkus-grpc-common
+
+
+
+ io.opentelemetry
+ opentelemetry-exporter-otlp-common
+
+
+ org.codehaus.mojo
+ animal-sniffer-annotations
+
+
+ org.checkerframework
+ checker-qual
+
+
+
+
+
+ io.opentelemetry
+ opentelemetry-exporter-jaeger
+
+
+ org.codehaus.mojo
+ animal-sniffer-annotations
+
+
+ org.checkerframework
+ checker-qual
+
+
+
+
+
+ org.graalvm.nativeimage
+ svm
+ provided
+
+
+
+
+
+
+ io.quarkus
+ quarkus-extension-maven-plugin
+ ${quarkus.version}
+
+
+ compile
+
+ extension-descriptor
+
+
+ ${project.groupId}:${project.artifactId}-deployment:${project.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${quarkus.version}
+
+
+
+
+
+
+
diff --git a/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/JaegerExporterConfig.java b/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/JaegerExporterConfig.java
new file mode 100644
index 0000000..b78c1cc
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/JaegerExporterConfig.java
@@ -0,0 +1,42 @@
+package io.quarkiverse.opentelemetry.exporter.jaeger.runtime;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import io.quarkus.runtime.annotations.ConfigItem;
+import io.quarkus.runtime.annotations.ConfigPhase;
+import io.quarkus.runtime.annotations.ConfigRoot;
+import io.quarkus.runtime.annotations.ConvertWith;
+import io.quarkus.runtime.configuration.TrimmedStringConverter;
+
+public class JaegerExporterConfig {
+ @ConfigRoot(name = "opentelemetry.tracer.exporter.jaeger", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
+ public static class JaegerExporterBuildConfig {
+ /**
+ * Jaeger SpanExporter support.
+ *
+ * Jaeger SpanExporter support is enabled by default.
+ */
+ @ConfigItem(defaultValue = "true")
+ public Boolean enabled;
+ }
+
+ @ConfigRoot(name = "opentelemetry.tracer.exporter.jaeger", phase = ConfigPhase.RUN_TIME)
+ public static class JaegerExporterRuntimeConfig {
+ /**
+ * The Jaeger endpoint to connect to. The endpoint must start with either http:// or https://.
+ */
+ @ConfigItem
+ @ConvertWith(TrimmedStringConverter.class)
+ public Optional endpoint;
+
+ /**
+ * The maximum amount of time to wait for the collector to process exported spans before an exception is thrown.
+ * A value of `0` will disable the timeout: the exporter will continue waiting until either exported spans are
+ * processed,
+ * or the connection fails, or is closed for some other reason.
+ */
+ @ConfigItem(defaultValue = "10S")
+ public Duration exportTimeout;
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/JaegerExporterProvider.java b/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/JaegerExporterProvider.java
new file mode 100644
index 0000000..6b573d4
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/JaegerExporterProvider.java
@@ -0,0 +1,16 @@
+package io.quarkiverse.opentelemetry.exporter.jaeger.runtime;
+
+import javax.enterprise.inject.Produces;
+import javax.inject.Singleton;
+
+import io.quarkus.arc.DefaultBean;
+
+@Singleton
+public class JaegerExporterProvider {
+ @Produces
+ @Singleton
+ @DefaultBean
+ public LateBoundBatchSpanProcessor batchSpanProcessorForJaeger() {
+ return new LateBoundBatchSpanProcessor();
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/JaegerRecorder.java b/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/JaegerRecorder.java
new file mode 100644
index 0000000..80c2b5a
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/JaegerRecorder.java
@@ -0,0 +1,40 @@
+package io.quarkiverse.opentelemetry.exporter.jaeger.runtime;
+
+import java.util.Optional;
+
+import javax.enterprise.inject.Any;
+import javax.enterprise.inject.spi.CDI;
+
+import io.opentelemetry.exporter.jaeger.JaegerGrpcSpanExporter;
+import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
+import io.quarkus.runtime.LaunchMode;
+import io.quarkus.runtime.annotations.Recorder;
+
+@Recorder
+public class JaegerRecorder {
+ public void installBatchSpanProcessorForJaeger(JaegerExporterConfig.JaegerExporterRuntimeConfig runtimeConfig,
+ LaunchMode launchMode) {
+
+ if (launchMode == LaunchMode.DEVELOPMENT && !runtimeConfig.endpoint.isPresent()) {
+ // Default the endpoint for development only
+ runtimeConfig.endpoint = Optional.of("http://localhost:14250");
+ }
+
+ // Only create the JaegerGrpcSpanExporter if an endpoint was set in runtime config
+ if (runtimeConfig.endpoint.isPresent() && runtimeConfig.endpoint.get().trim().length() > 0) {
+ try {
+ JaegerGrpcSpanExporter jaegerSpanExporter = JaegerGrpcSpanExporter.builder()
+ .setEndpoint(runtimeConfig.endpoint.get())
+ .setTimeout(runtimeConfig.exportTimeout)
+ .build();
+
+ // Create BatchSpanProcessor for Jaeger and install into LateBoundBatchSpanProcessor
+ LateBoundBatchSpanProcessor delayedProcessor = CDI.current()
+ .select(LateBoundBatchSpanProcessor.class, Any.Literal.INSTANCE).get();
+ delayedProcessor.setBatchSpanProcessorDelegate(BatchSpanProcessor.builder(jaegerSpanExporter).build());
+ } catch (IllegalArgumentException iae) {
+ throw new IllegalStateException("Unable to install Jaeger Exporter", iae);
+ }
+ }
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/JaegerSubstitutions.java b/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/JaegerSubstitutions.java
new file mode 100644
index 0000000..742194d
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/JaegerSubstitutions.java
@@ -0,0 +1,49 @@
+package io.quarkiverse.opentelemetry.exporter.jaeger.runtime;
+
+import static java.util.Objects.requireNonNull;
+
+import javax.net.ssl.SSLException;
+import javax.net.ssl.X509TrustManager;
+
+import com.oracle.svm.core.annotate.Substitute;
+import com.oracle.svm.core.annotate.TargetClass;
+
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.netty.GrpcSslContexts;
+import io.grpc.netty.NettyChannelBuilder;
+import io.opentelemetry.exporter.internal.grpc.ManagedChannelUtil;
+
+/**
+ * Replace the {@link ManagedChannelUtil#setClientKeysAndTrustedCertificatesPem(ManagedChannelBuilder, byte[], byte[], byte[])}
+ * method in native
+ * because the method implementation tries to look for grpc-netty-shaded dependencies, which we don't support.
+ *
+ * Check:
+ * https://github.com/open-telemetry/opentelemetry-java/blob/v1.13.0/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/grpc/ManagedChannelUtil.java#L47-L91
+ */
+final class JaegerSubstitutions {
+ @TargetClass(ManagedChannelUtil.class)
+ static final class Target_ManagedChannelUtil {
+ @Substitute
+ public static void setClientKeysAndTrustedCertificatesPem(
+ ManagedChannelBuilder> managedChannelBuilder, byte[] privateKeyPem, byte[] certificatePem,
+ byte[] trustedCertificatesPem)
+ throws SSLException {
+ requireNonNull(managedChannelBuilder, "managedChannelBuilder");
+ requireNonNull(trustedCertificatesPem, "trustedCertificatesPem");
+
+ X509TrustManager tm = io.opentelemetry.exporter.internal.TlsUtil.trustManager(trustedCertificatesPem);
+
+ // gRPC does not abstract TLS configuration so we need to check the implementation and act
+ // accordingly.
+ if (managedChannelBuilder.getClass().getName().equals("io.grpc.netty.NettyChannelBuilder")) {
+ NettyChannelBuilder nettyBuilder = (NettyChannelBuilder) managedChannelBuilder;
+ nettyBuilder.sslContext(GrpcSslContexts.forClient().trustManager(tm).build());
+ } else {
+ throw new SSLException(
+ "TLS certificate configuration not supported for unrecognized ManagedChannelBuilder "
+ + managedChannelBuilder.getClass().getName());
+ }
+ }
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/LateBoundBatchSpanProcessor.java b/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/LateBoundBatchSpanProcessor.java
new file mode 100644
index 0000000..0d3c39a
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/jaeger/runtime/LateBoundBatchSpanProcessor.java
@@ -0,0 +1,112 @@
+package io.quarkiverse.opentelemetry.exporter.jaeger.runtime;
+
+import org.jboss.logging.Logger;
+
+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;
+import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
+
+/**
+ * Class to facilitate a delay in when the worker thread inside {@link BatchSpanProcessor}
+ * is started, enabling Quarkus to instantiate a {@link io.opentelemetry.api.trace.TracerProvider}
+ * during static initialization and set a {@link BatchSpanProcessor} delegate during runtime initialization.
+ */
+public class LateBoundBatchSpanProcessor implements SpanProcessor {
+ private static final Logger log = Logger.getLogger(LateBoundBatchSpanProcessor.class);
+
+ private boolean warningLogged = false;
+ private BatchSpanProcessor delegate;
+
+ /**
+ * Set the actual {@link BatchSpanProcessor} to use as the delegate.
+ *
+ * @param delegate Properly constructed {@link BatchSpanProcessor} for processing spans.
+ */
+ public void setBatchSpanProcessorDelegate(BatchSpanProcessor delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void onStart(Context parentContext, ReadWriteSpan span) {
+ if (delegate == null) {
+ logDelegateNotFound();
+ return;
+ }
+ delegate.onStart(parentContext, span);
+ }
+
+ @Override
+ public boolean isStartRequired() {
+ if (delegate == null) {
+ logDelegateNotFound();
+ return false;
+ }
+ return delegate.isStartRequired();
+ }
+
+ @Override
+ public void onEnd(ReadableSpan span) {
+ if (delegate == null) {
+ logDelegateNotFound();
+ return;
+ }
+ delegate.onEnd(span);
+ }
+
+ @Override
+ public boolean isEndRequired() {
+ if (delegate == null) {
+ logDelegateNotFound();
+ return true;
+ }
+ return delegate.isEndRequired();
+ }
+
+ @Override
+ public CompletableResultCode shutdown() {
+ if (delegate == null) {
+ logDelegateNotFound();
+ return CompletableResultCode.ofSuccess();
+ }
+ return delegate.shutdown();
+ }
+
+ @Override
+ public CompletableResultCode forceFlush() {
+ if (delegate == null) {
+ logDelegateNotFound();
+ return CompletableResultCode.ofSuccess();
+ }
+ return delegate.forceFlush();
+ }
+
+ @Override
+ public void close() {
+ if (delegate != null) {
+ delegate.close();
+ }
+ resetDelegate();
+ }
+
+ /**
+ * Clear the {@code delegate} and reset {@code warningLogged}.
+ */
+ private void resetDelegate() {
+ delegate = null;
+ warningLogged = false;
+ }
+
+ /**
+ * If we haven't previously logged an error,
+ * log an error about a missing {@code delegate} and set {@code warningLogged=true}
+ */
+ private void logDelegateNotFound() {
+ if (!warningLogged) {
+ log.warn("No BatchSpanProcessor delegate specified, no action taken.");
+ warningLogged = true;
+ }
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/resources/META-INF/quarkus-extension.yaml
new file mode 100644
index 0000000..722c0eb
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-jaeger/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -0,0 +1,15 @@
+name: "Quarkus OpenTelemetry exporter: Jaeger"
+artifact: ${project.groupId}:${project.artifactId}:${project.version}
+metadata:
+ keywords:
+ - "opentelemetry"
+ - "tracing"
+ - "distributed-tracing"
+ - "opentelemetry-jaeger-exporter"
+ - "monitoring"
+ guide: "https://quarkus.io/guides/opentelemetry"
+ categories:
+ - "observability"
+ status: "experimental"
+ config:
+ - "quarkus.opentelemetry."