diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java index 350a89459e648..ab745941977be 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/OpenTelemetryProcessor.java @@ -27,12 +27,14 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.exporter.otlp.internal.OtlpMetricExporterProvider; import io.opentelemetry.exporter.otlp.internal.OtlpSpanExporterProvider; import io.opentelemetry.instrumentation.annotations.AddingSpanAttributes; import io.opentelemetry.instrumentation.annotations.WithSpan; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider; import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider; import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; @@ -105,6 +107,7 @@ AdditionalBeanBuildItem ensureProducerIsRetained() { AutoConfiguredOpenTelemetrySdkBuilderCustomizer.ResourceCustomizer.class, AutoConfiguredOpenTelemetrySdkBuilderCustomizer.SamplerCustomizer.class, AutoConfiguredOpenTelemetrySdkBuilderCustomizer.TracerProviderCustomizer.class, + AutoConfiguredOpenTelemetrySdkBuilderCustomizer.MetricProviderCustomizer.class, AutoConfiguredOpenTelemetrySdkBuilderCustomizer.TextMapPropagatorCustomizers.class) .build(); } @@ -144,11 +147,12 @@ void handleServices(OTelBuildConfig config, BuildProducer removedResources, BuildProducer runtimeReinitialized) throws IOException { - List spanExporterProviders = ServiceUtil.classNamesNamedIn( + final List spanExporterProviders = ServiceUtil.classNamesNamedIn( Thread.currentThread().getContextClassLoader(), SPI_ROOT + ConfigurableSpanExporterProvider.class.getName()) .stream() - .filter(p -> !OtlpSpanExporterProvider.class.getName().equals(p)).collect(toList()); // filter out OtlpSpanExporterProvider since it depends on OkHttp + .filter(p -> !OtlpSpanExporterProvider.class.getName().equals(p)) + .collect(toList()); // filter out OtlpSpanExporterProvider since it depends on OkHttp if (!spanExporterProviders.isEmpty()) { services.produce( new ServiceProviderBuildItem(ConfigurableSpanExporterProvider.class.getName(), spanExporterProviders)); @@ -160,8 +164,26 @@ void handleServices(OTelBuildConfig config, Set.of("META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider"))); } + final List metricExporterProviders = ServiceUtil.classNamesNamedIn( + Thread.currentThread().getContextClassLoader(), + SPI_ROOT + ConfigurableMetricExporterProvider.class.getName()) + .stream() + .filter(p -> !OtlpMetricExporterProvider.class.getName().equals(p)) + .collect(toList()); // filter out OtlpMetricExporterProvider since it depends on OkHttp + if (!metricExporterProviders.isEmpty()) { + services.produce( + new ServiceProviderBuildItem(ConfigurableMetricExporterProvider.class.getName(), metricExporterProviders)); + } + if (config.metrics().exporter().stream().noneMatch(ExporterType.Constants.OTLP_VALUE::equals)) { + removedResources.produce(new RemovedResourceBuildItem( + ArtifactKey.fromString("io.opentelemetry:opentelemetry-exporter-otlp"), + Set.of("META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider"))); + } + runtimeReinitialized.produce( new RuntimeReinitializedClassBuildItem("io.opentelemetry.sdk.autoconfigure.TracerProviderConfiguration")); + runtimeReinitialized.produce( + new RuntimeReinitializedClassBuildItem("io.opentelemetry.sdk.autoconfigure.MeterProviderConfiguration")); services.produce(ServiceProviderBuildItem.allProvidersFromClassPath( ConfigurableSamplerProvider.class.getName())); diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java index 4365478d1183c..1b6eac1ebadd4 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterProcessor.java @@ -13,8 +13,10 @@ import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; +import io.opentelemetry.sdk.metrics.export.MetricExporter; import io.opentelemetry.sdk.trace.SpanProcessor; import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.deployment.annotations.*; import io.quarkus.deployment.annotations.Record; @@ -22,16 +24,18 @@ import io.quarkus.opentelemetry.runtime.config.build.exporter.OtlpExporterBuildConfig; import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; import io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterRuntimeConfig; -import io.quarkus.opentelemetry.runtime.exporter.otlp.LateBoundBatchSpanProcessor; import io.quarkus.opentelemetry.runtime.exporter.otlp.OTelExporterRecorder; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundBatchSpanProcessor; import io.quarkus.tls.TlsConfigurationRegistry; import io.quarkus.tls.TlsRegistryBuildItem; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; -@BuildSteps(onlyIf = OtlpExporterProcessor.OtlpExporterEnabled.class) +@BuildSteps public class OtlpExporterProcessor { - static class OtlpExporterEnabled implements BooleanSupplier { + private static final DotName METRIC_EXPORTER = DotName.createSimple(MetricExporter.class.getName()); + + static class OtlpTracingExporterEnabled implements BooleanSupplier { OtlpExporterBuildConfig exportBuildConfig; OTelBuildConfig otelBuildConfig; @@ -43,8 +47,20 @@ public boolean getAsBoolean() { } } + static class OtlpMetricsExporterEnabled implements BooleanSupplier { + OtlpExporterBuildConfig exportBuildConfig; + OTelBuildConfig otelBuildConfig; + + public boolean getAsBoolean() { + return otelBuildConfig.enabled() && + otelBuildConfig.metrics().enabled().orElse(Boolean.TRUE) && + otelBuildConfig.metrics().exporter().contains(CDI_VALUE) && + exportBuildConfig.enabled(); + } + } + @SuppressWarnings("deprecation") - @BuildStep + @BuildStep(onlyIf = OtlpExporterProcessor.OtlpTracingExporterEnabled.class) @Record(ExecutionTime.RUNTIME_INIT) @Consume(TlsRegistryBuildItem.class) void createBatchSpanProcessor(OTelExporterRecorder recorder, @@ -70,4 +86,41 @@ void createBatchSpanProcessor(OTelExporterRecorder recorder, vertxBuildItem.getVertx())) .done()); } + + @BuildStep(onlyIf = OtlpMetricsExporterEnabled.class) + @Record(ExecutionTime.RUNTIME_INIT) + @Consume(TlsRegistryBuildItem.class) + void createMetricsExporterProcessor( + BeanDiscoveryFinishedBuildItem beanDiscovery, + OTelExporterRecorder recorder, + List externalOtelExporterBuildItem, + OTelRuntimeConfig otelRuntimeConfig, + OtlpExporterRuntimeConfig exporterRuntimeConfig, + CoreVertxBuildItem vertxBuildItem, + BuildProducer syntheticBeanBuildItemBuildProducer) { + + if (!externalOtelExporterBuildItem.isEmpty()) { + // if there is an external exporter, we don't want to create the default one. + // External exporter also use synthetic beans. However, synthetic beans don't show in the BeanDiscoveryFinishedBuildItem + return; + } + + if (!beanDiscovery.beanStream().withBeanType(METRIC_EXPORTER).isEmpty()) { + // if there is a MetricExporter bean impl around, we don't want to create the default one + return; + } + + syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem + .configure(MetricExporter.class) + .types(MetricExporter.class) + .setRuntimeInit() + .scope(Singleton.class) + .unremovable() + .addInjectionPoint(ParameterizedType.create(DotName.createSimple(Instance.class), + new Type[] { ClassType.create(DotName.createSimple(MetricExporter.class.getName())) }, null)) + .addInjectionPoint(ClassType.create(DotName.createSimple(TlsConfigurationRegistry.class))) + .createWith(recorder.createMetricExporter(otelRuntimeConfig, exporterRuntimeConfig, + vertxBuildItem.getVertx())) + .done()); + } } diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/metric/MetricProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/metric/MetricProcessor.java new file mode 100644 index 0000000000000..e4af5ce8cfeae --- /dev/null +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/metric/MetricProcessor.java @@ -0,0 +1,101 @@ +package io.quarkus.opentelemetry.deployment.metric; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.function.BooleanSupplier; +import java.util.function.Function; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; + +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.MetricReader; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.arc.processor.DotNames; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig; +import io.quarkus.opentelemetry.runtime.metrics.cdi.MetricsProducer; + +@BuildSteps(onlyIf = MetricProcessor.MetricEnabled.class) +public class MetricProcessor { + private static final DotName METRIC_EXPORTER = DotName.createSimple(MetricExporter.class.getName()); + private static final DotName METRIC_READER = DotName.createSimple(MetricReader.class.getName()); + private static final DotName METRIC_PROCESSOR = DotName.createSimple(MetricProcessor.class.getName()); + + @BuildStep + UnremovableBeanBuildItem ensureProducersAreRetained( + CombinedIndexBuildItem indexBuildItem, + BuildProducer additionalBeans) { + + additionalBeans.produce(AdditionalBeanBuildItem.builder() + .setUnremovable() + .addBeanClass(MetricsProducer.class) + .build()); + + IndexView index = indexBuildItem.getIndex(); + + // Find all known SpanExporters and SpanProcessors + Collection knownClasses = new HashSet<>(); + knownClasses.add(METRIC_EXPORTER.toString()); + index.getAllKnownImplementors(METRIC_EXPORTER) + .forEach(classInfo -> knownClasses.add(classInfo.name().toString())); + + knownClasses.add(METRIC_READER.toString()); + index.getAllKnownImplementors(METRIC_READER) + .forEach(classInfo -> knownClasses.add(classInfo.name().toString())); + + knownClasses.add(METRIC_PROCESSOR.toString()); + index.getAllKnownImplementors(METRIC_PROCESSOR) + .forEach(classInfo -> knownClasses.add(classInfo.name().toString())); + + Set retainProducers = new HashSet<>(); + + for (AnnotationInstance annotation : index.getAnnotations(DotNames.PRODUCES)) { + AnnotationTarget target = annotation.target(); + switch (target.kind()) { + case METHOD: + MethodInfo method = target.asMethod(); + String returnType = method.returnType().name().toString(); + if (knownClasses.contains(returnType)) { + retainProducers.add(method.declaringClass().name().toString()); + } + break; + case FIELD: + FieldInfo field = target.asField(); + String fieldType = field.type().name().toString(); + if (knownClasses.contains(fieldType)) { + retainProducers.add(field.declaringClass().name().toString()); + } + break; + default: + break; + } + } + + return new UnremovableBeanBuildItem(new UnremovableBeanBuildItem.BeanClassNamesExclusion(retainProducers)); + } + + public static class MetricEnabled implements BooleanSupplier { + OTelBuildConfig otelBuildConfig; + + public boolean getAsBoolean() { + return otelBuildConfig.metrics().enabled() + .map(new Function() { + @Override + public Boolean apply(Boolean enabled) { + return otelBuildConfig.enabled() && enabled; + } + }) + .orElseGet(() -> otelBuildConfig.enabled()); + } + } +} diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerEnabled.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerEnabled.java index 96478719d5f3d..5102a51329bbc 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerEnabled.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerEnabled.java @@ -9,12 +9,13 @@ public class TracerEnabled implements BooleanSupplier { OTelBuildConfig otelConfig; public boolean getAsBoolean() { - return otelConfig.traces().enabled().map(new Function() { - @Override - public Boolean apply(Boolean tracerEnabled) { - return otelConfig.enabled() && tracerEnabled; - } - }) + return otelConfig.traces().enabled() + .map(new Function() { + @Override + public Boolean apply(Boolean tracerEnabled) { + return otelConfig.enabled() && tracerEnabled; + } + }) .orElseGet(() -> otelConfig.enabled()); } } diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryContinuousTestingTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryContinuousTestingTest.java index a1fb1c731deec..bc6bf553f91d2 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryContinuousTestingTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryContinuousTestingTest.java @@ -10,6 +10,7 @@ import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; import io.quarkus.opentelemetry.deployment.common.TracerRouter; +import io.quarkus.opentelemetry.deployment.traces.TracerRouterUT; import io.quarkus.test.ContinuousTestingTestUtils; import io.quarkus.test.ContinuousTestingTestUtils.TestStatus; import io.quarkus.test.QuarkusDevModeTest; @@ -23,7 +24,8 @@ public class OpenTelemetryContinuousTestingTest { .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") .add(new StringAsset(ContinuousTestingTestUtils.appProperties( - "quarkus.otel.traces.exporter=test-span-exporter")), + "quarkus.otel.traces.exporter=test-span-exporter", + "quarkus.otel.metrics.exporter=none")), "application.properties")) .setTestArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) .addClass(TracerRouterUT.class)); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDestroyerTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDestroyerTest.java index d0fe61b90b23d..b8b72ee86db39 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDestroyerTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDestroyerTest.java @@ -26,8 +26,11 @@ public class OpenTelemetryDestroyerTest { .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") .add(new StringAsset( - "quarkus.otel.traces.exporter=test-span-exporter\n" + - "quarkus.otel.experimental.shutdown-wait-time=PT60S\n"), + """ + quarkus.otel.traces.exporter=test-span-exporter + quarkus.otel.metrics.exporter=none + quarkus.otel.experimental.shutdown-wait-time=PT60S + """), "application.properties")); @Test diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDevModeTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDevModeTest.java index 87b94744802ca..b32aab8c4acac 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDevModeTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDevModeTest.java @@ -23,7 +23,8 @@ public class OpenTelemetryDevModeTest { .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") .add(new StringAsset(ContinuousTestingTestUtils.appProperties( - "quarkus.otel.traces.exporter=test-span-exporter")), "application.properties")); + "quarkus.otel.traces.exporter=test-span-exporter", + "quarkus.otel.metrics.exporter=none")), "application.properties")); @Test void testDevMode() { diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDevServicesDatasourcesTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDevServicesDatasourcesTest.java index 1a7f03ab3d8e1..2eb1130ae02af 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDevServicesDatasourcesTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDevServicesDatasourcesTest.java @@ -41,11 +41,14 @@ public class OpenTelemetryDevServicesDatasourcesTest { .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") .add(new StringAsset( - "quarkus.datasource.db-kind=h2\n" + - "quarkus.datasource.jdbc.telemetry=true\n" + - "quarkus.otel.traces.exporter=test-span-exporter\n" + - "quarkus.otel.bsp.export.timeout=1s\n" + - "quarkus.otel.bsp.schedule.delay=50\n"), + """ + quarkus.datasource.db-kind=h2 + quarkus.datasource.jdbc.telemetry=true + quarkus.otel.traces.exporter=test-span-exporter + quarkus.otel.metrics.exporter=none + quarkus.otel.bsp.export.timeout=1s + quarkus.otel.bsp.schedule.delay=50 + """), "application.properties")); @Test diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDisabledSdkTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDisabledSdkTest.java index a239051302a0e..d77a2f14dbbb2 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDisabledSdkTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryDisabledSdkTest.java @@ -8,7 +8,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.opentelemetry.api.OpenTelemetry; -import io.quarkus.opentelemetry.runtime.exporter.otlp.LateBoundBatchSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundBatchSpanProcessor; import io.quarkus.test.QuarkusUnitTest; @Disabled("Not implemented") diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryMDCTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryMDCTest.java index 103ea2c4a531c..572a020cb89b7 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryMDCTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryMDCTest.java @@ -28,6 +28,8 @@ import io.opentelemetry.context.Scope; import io.opentelemetry.sdk.trace.data.SpanData; import io.quarkus.arc.Unremovable; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporter; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporterProvider; import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; import io.quarkus.test.QuarkusUnitTest; @@ -40,9 +42,12 @@ public class OpenTelemetryMDCTest { .addClass(MdcEntry.class) .addClass(TestMdcCapturer.class) .addClass(TestResource.class) - .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) + .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class, + InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), - "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")) .withConfigurationResource("application-default.properties"); @Inject diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java index 92c919c44fe80..f1ee913e35cd6 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryResourceTest.java @@ -13,12 +13,17 @@ import jakarta.ws.rs.Path; import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporter; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporterProvider; import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; import io.quarkus.test.QuarkusUnitTest; @@ -29,17 +34,21 @@ public class OpenTelemetryResourceTest { @RegisterExtension static final QuarkusUnitTest TEST = new QuarkusUnitTest().setArchiveProducer( () -> ShrinkWrap.create(JavaArchive.class) - .addClass(TestSpanExporter.class) - .addClass(TestSpanExporterProvider.class) + .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class, + InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class) .addAsResource("resource-config/application.properties", "application.properties") .addAsResource( "META-INF/services-config/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", - "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")); + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")); @Inject SmallRyeConfig config; @Inject TestSpanExporter spanExporter; + @Inject + InMemoryMetricExporter metricExporter; @Test void resource() { @@ -56,12 +65,19 @@ void resource() { assertEquals(config.getRawValue("quarkus.uuid"), server.getResource().getAttribute(AttributeKey.stringKey("service.instance.id"))); assertNotNull(server.getResource().getAttribute(AttributeKey.stringKey("host.name"))); + + metricExporter.assertCountAtLeast(1); + List finishedMetricItems = metricExporter.getFinishedMetricItems(); } @Path("/hello") public static class HelloResource { + @Inject + Meter meter; + @GET public String hello() { + meter.counterBuilder("hello").build().add(1); return "hello"; } } diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/InMemoryMetricExporter.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/InMemoryMetricExporter.java new file mode 100644 index 0000000000000..59bd8aa6f4399 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/InMemoryMetricExporter.java @@ -0,0 +1,209 @@ +package io.quarkus.opentelemetry.deployment.common; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toMap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Assertions; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.api.internal.SemconvStability; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.PointData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.semconv.SemanticAttributes; +import io.quarkus.arc.Unremovable; + +@Unremovable +@ApplicationScoped +public class InMemoryMetricExporter implements MetricExporter { + + private static final List LEGACY_KEY_COMPONENTS = List.of(SemanticAttributes.HTTP_METHOD.getKey(), + SemanticAttributes.HTTP_ROUTE.getKey(), + SemanticAttributes.HTTP_STATUS_CODE.getKey()); + private static final List KEY_COMPONENTS = List.of(SemanticAttributes.HTTP_REQUEST_METHOD.getKey(), + SemanticAttributes.HTTP_ROUTE.getKey(), + SemanticAttributes.HTTP_RESPONSE_STATUS_CODE.getKey()); + + private final Queue finishedMetricItems = new ConcurrentLinkedQueue<>(); + private final AggregationTemporality aggregationTemporality = AggregationTemporality.CUMULATIVE; + private boolean isStopped = false; + + public static Map getPointAttributes(final MetricData metricData, final String path) { + try { + return metricData.getData().getPoints().stream() + .filter(point -> isPathFound(path, point.getAttributes())) + .map(point -> point.getAttributes()) + .map(attributes1 -> attributes1.asMap()) + .flatMap(map -> map.entrySet().stream()) + .collect(toMap(map -> map.getKey().toString(), map -> map.getValue().toString())); + } catch (Exception e) { + System.out.println("Error getting point attributes for " + metricData.getName()); + metricData.getData().getPoints().stream() + .filter(point -> isPathFound(path, point.getAttributes())) + .map(point -> point.getAttributes()) + .map(attributes1 -> attributes1.asMap()) + .flatMap(map -> map.entrySet().stream()) + .forEach(attributeKeyObjectEntry -> System.out + .println(attributeKeyObjectEntry.getKey() + " " + attributeKeyObjectEntry.getValue())); + throw e; + } + } + + public static Map getMostRecentPointsMap(List finishedMetricItems) { + return finishedMetricItems.stream() + .flatMap(metricData -> metricData.getData().getPoints().stream()) + // exclude data from /export endpoint + .filter(InMemoryMetricExporter::notExporterPointData) + // newer first + .sorted(Comparator.comparingLong(PointData::getEpochNanos).reversed()) + .collect(toMap( + pointData -> pointData.getAttributes().asMap().entrySet().stream() + //valid attributes for the resulting map key + .filter(entry -> { + if (SemconvStability.emitOldHttpSemconv()) { + return LEGACY_KEY_COMPONENTS.contains(entry.getKey().getKey()); + } else { + return KEY_COMPONENTS.contains(entry.getKey().getKey()); + } + }) + // ensure order + .sorted(Comparator.comparing(o -> o.getKey().getKey())) + // build key + .map(entry -> entry.getKey().getKey() + ":" + entry.getValue().toString()) + .collect(joining(",")), + pointData -> pointData, + // most recent points will surface + (older, newer) -> newer)); + } + + /* + * ignore points with /export in the route + */ + private static boolean notExporterPointData(PointData pointData) { + return pointData.getAttributes().asMap().entrySet().stream() + .noneMatch(entry -> entry.getKey().getKey().equals(SemanticAttributes.HTTP_ROUTE.getKey()) && + entry.getValue().toString().contains("/export")); + } + + private static boolean isPathFound(String path, Attributes attributes) { + if (path == null) { + return true;// any match + } + Object value = attributes.asMap().get(AttributeKey.stringKey(SemanticAttributes.HTTP_ROUTE.getKey())); + if (value == null) { + return false; + } + return value.toString().equals(path); + } + + public void assertCount(final int count) { + Awaitility.await().atMost(5, SECONDS) + .untilAsserted(() -> Assertions.assertEquals(count, getFinishedMetricItems().size())); + } + + public void assertCount(final String name, final String target, final int count) { + Awaitility.await().atMost(5, SECONDS) + .untilAsserted(() -> Assertions.assertEquals(count, getFinishedMetricItems(name, target).size())); + } + + public void assertCountAtLeast(final int count) { + Awaitility.await().atMost(5, SECONDS) + .untilAsserted(() -> Assertions.assertTrue(count < getFinishedMetricItems().size())); + } + + public void assertCountAtLeast(final String name, final String target, final int count) { + Awaitility.await().atMost(5, SECONDS) + .untilAsserted(() -> Assertions.assertTrue(count < getFinishedMetricItems(name, target).size())); + } + + /** + * Returns a {@code List} of the finished {@code Metric}s, represented by {@code MetricData}. + * + * @return a {@code List} of the finished {@code Metric}s. + */ + public List getFinishedMetricItems() { + return Collections.unmodifiableList(new ArrayList<>(finishedMetricItems)); + } + + public List getFinishedMetricItems(final String name, final String target) { + return Collections.unmodifiableList(new ArrayList<>( + finishedMetricItems.stream() + .filter(metricData -> metricData.getName().equals(name)) + .filter(metricData -> metricData.getData().getPoints().stream() + .anyMatch(point -> isPathFound(target, point.getAttributes()))) + .collect(Collectors.toList()))); + } + + /** + * Clears the internal {@code List} of finished {@code Metric}s. + * + *

+ * Does not reset the state of this exporter if already shutdown. + */ + public void reset() { + finishedMetricItems.clear(); + } + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return aggregationTemporality; + } + + /** + * Exports the collection of {@code Metric}s into the inmemory queue. + * + *

+ * If this is called after {@code shutdown}, this will return {@code ResultCode.FAILURE}. + */ + @Override + public CompletableResultCode export(Collection metrics) { + if (isStopped) { + return CompletableResultCode.ofFailure(); + } + finishedMetricItems.addAll(metrics); + return CompletableResultCode.ofSuccess(); + } + + /** + * The InMemory exporter does not batch metrics, so this method will immediately return with + * success. + * + * @return always Success + */ + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + /** + * Clears the internal {@code List} of finished {@code Metric}s. + * + *

+ * Any subsequent call to export() function on this MetricExporter, will return {@code + * CompletableResultCode.ofFailure()} + */ + @Override + public CompletableResultCode shutdown() { + isStopped = true; + finishedMetricItems.clear(); + return CompletableResultCode.ofSuccess(); + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/InMemoryMetricExporterProvider.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/InMemoryMetricExporterProvider.java new file mode 100644 index 0000000000000..6927ccb5ef4ae --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/common/InMemoryMetricExporterProvider.java @@ -0,0 +1,19 @@ +package io.quarkus.opentelemetry.deployment.common; + +import jakarta.enterprise.inject.spi.CDI; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider; +import io.opentelemetry.sdk.metrics.export.MetricExporter; + +public class InMemoryMetricExporterProvider implements ConfigurableMetricExporterProvider { + @Override + public MetricExporter createExporter(ConfigProperties configProperties) { + return CDI.current().select(InMemoryMetricExporter.class).get(); + } + + @Override + public String getName() { + return "in-memory"; + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterConfigTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterConfigTest.java index 6f83f5bb76d1d..3692683973ad0 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterConfigTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterConfigTest.java @@ -15,8 +15,8 @@ public class OtlpExporterConfigTest { @RegisterExtension static final QuarkusUnitTest TEST = new QuarkusUnitTest() .withEmptyApplication() - .overrideConfigKey("otel.traces.exporter", "cdi") - .overrideConfigKey("otel.exporter.otlp.traces.protocol", "http/protobuf") + .overrideConfigKey("quarkus.otel.traces.exporter", "cdi") + .overrideConfigKey("quarkus.otel.exporter.otlp.traces.protocol", "http/protobuf") .overrideConfigKey("quarkus.otel.exporter.otlp.traces.legacy-endpoint", "http://localhost ") .overrideConfigKey("quarkus.otel.bsp.schedule.delay", "50") .overrideConfigKey("quarkus.otel.bsp.export.timeout", "PT1S"); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterDisabledTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpTraceExporterDisabledTest.java similarity index 57% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterDisabledTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpTraceExporterDisabledTest.java index 72ffaa6fbada3..7d9e074025c18 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpExporterDisabledTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/exporter/otlp/OtlpTraceExporterDisabledTest.java @@ -1,22 +1,24 @@ package io.quarkus.opentelemetry.deployment.exporter.otlp; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.opentelemetry.api.OpenTelemetry; -import io.quarkus.opentelemetry.runtime.exporter.otlp.LateBoundBatchSpanProcessor; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundBatchSpanProcessor; import io.quarkus.test.QuarkusUnitTest; -public class OtlpExporterDisabledTest { +public class OtlpTraceExporterDisabledTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .withEmptyApplication() - .overrideConfigKey("otel.traces.exporter", "cdi") .overrideConfigKey("quarkus.otel.exporter.otlp.enabled", "false"); @Inject @@ -25,9 +27,13 @@ public class OtlpExporterDisabledTest { @Inject Instance lateBoundBatchSpanProcessorInstance; + @Inject + Instance metricExporters; + @Test void testOpenTelemetryButNoBatchSpanProcessor() { - Assertions.assertNotNull(openTelemetry); - Assertions.assertFalse(lateBoundBatchSpanProcessorInstance.isResolvable()); + assertNotNull(openTelemetry); + assertFalse(lateBoundBatchSpanProcessorInstance.isResolvable()); + assertFalse(metricExporters.isResolvable()); } } diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GraphQLOpenTelemetryTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GraphQLOpenTelemetryTest.java index b685abb0ec4c4..293e5279f2fb3 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GraphQLOpenTelemetryTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GraphQLOpenTelemetryTest.java @@ -62,6 +62,7 @@ public class GraphQLOpenTelemetryTest { .addAsResource(new StringAsset("smallrye.graphql.allowGet=true"), "application.properties") .addAsResource(new StringAsset("smallrye.graphql.printDataFetcherException=true"), "application.properties") .addAsResource(new StringAsset("smallrye.graphql.events.enabled=true"), "application.properties") + .addAsResource(new StringAsset("quarkus.otel.metrics.exporter=none"), "application.properties") .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GrpcOpenInstrumentationDisabledTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GrpcOpenInstrumentationDisabledTest.java index 5c6bb07a37f23..6b1a4af1b4c82 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GrpcOpenInstrumentationDisabledTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GrpcOpenInstrumentationDisabledTest.java @@ -30,6 +30,8 @@ import io.quarkus.opentelemetry.deployment.HelloRequest; import io.quarkus.opentelemetry.deployment.HelloRequestOrBuilder; import io.quarkus.opentelemetry.deployment.MutinyGreeterGrpc; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporter; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporterProvider; import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; import io.quarkus.test.QuarkusUnitTest; @@ -41,13 +43,16 @@ public class GrpcOpenInstrumentationDisabledTest { static final QuarkusUnitTest TEST = new QuarkusUnitTest() .withApplicationRoot(root -> root .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) + .addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class) .addClasses(HelloService.class) .addClasses(GreeterGrpc.class, MutinyGreeterGrpc.class, Greeter.class, GreeterBean.class, GreeterClient.class, HelloProto.class, HelloRequest.class, HelloRequestOrBuilder.class, HelloReply.class, HelloReplyOrBuilder.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), - "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")) .withConfigurationResource("application-default.properties") .overrideConfigKey("quarkus.grpc.clients.hello.host", "localhost") .overrideConfigKey("quarkus.grpc.clients.hello.port", "9001") diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GrpcOpenTelemetryTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GrpcOpenTelemetryTest.java index e9d3d4c42c7b2..f2fc2c2c62638 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GrpcOpenTelemetryTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/GrpcOpenTelemetryTest.java @@ -58,6 +58,8 @@ import io.quarkus.opentelemetry.deployment.StreamingClient; import io.quarkus.opentelemetry.deployment.StreamingGrpc; import io.quarkus.opentelemetry.deployment.StreamingProto; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporter; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporterProvider; import io.quarkus.opentelemetry.deployment.common.SemconvResolver; import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; @@ -70,6 +72,7 @@ public class GrpcOpenTelemetryTest { static final QuarkusUnitTest TEST = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class, SemconvResolver.class) + .addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class) .addClasses(HelloService.class) .addClasses(GreeterGrpc.class, MutinyGreeterGrpc.class, Greeter.class, GreeterBean.class, GreeterClient.class, @@ -80,7 +83,9 @@ public class GrpcOpenTelemetryTest { Streaming.class, StreamingBean.class, StreamingClient.class, StreamingProto.class, Item.class, ItemOrBuilder.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), - "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")) .withConfigurationResource("application-default.properties") .overrideConfigKey("quarkus.grpc.clients.greeter.host", "localhost") .overrideConfigKey("quarkus.grpc.clients.greeter.port", "9001") diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/RestClientOpenTelemetryTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/RestClientOpenTelemetryTest.java index 23730f0360408..d0ecccaa1dd57 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/RestClientOpenTelemetryTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/RestClientOpenTelemetryTest.java @@ -36,6 +36,8 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporter; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporterProvider; import io.quarkus.opentelemetry.deployment.common.SemconvResolver; import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; @@ -46,8 +48,11 @@ public class RestClientOpenTelemetryTest { @RegisterExtension static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot((jar) -> jar .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class, SemconvResolver.class) + .addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), - "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")) .withConfigurationResource("application-default.properties") .overrideConfigKey("quarkus.rest-client.client.url", "${test.url}"); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxClientOpenTelemetryTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxClientOpenTelemetryTest.java index bfde0e210a3e5..fc828e8ccfbb6 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxClientOpenTelemetryTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxClientOpenTelemetryTest.java @@ -31,6 +31,8 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporter; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporterProvider; import io.quarkus.opentelemetry.deployment.common.SemconvResolver; import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; @@ -50,8 +52,11 @@ public class VertxClientOpenTelemetryTest { static final QuarkusUnitTest TEST = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class, SemconvResolver.class) + .addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), - "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")) .withConfigurationResource("application-default.properties"); @Inject diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryForwardedTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryForwardedTest.java index b7cb2da17bc83..5ecf22ad53dbb 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryForwardedTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryForwardedTest.java @@ -15,6 +15,8 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporter; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporterProvider; import io.quarkus.opentelemetry.deployment.common.SemconvResolver; import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; @@ -28,8 +30,11 @@ public class VertxOpenTelemetryForwardedTest { .withApplicationRoot((jar) -> jar .addClass(TracerRouter.class) .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class, SemconvResolver.class) + .addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), - "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")) .withConfigurationResource("application-default.properties"); @Inject diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryXForwardedTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryXForwardedTest.java index bbf97ebedb203..44c648d102ea2 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryXForwardedTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/instrumentation/VertxOpenTelemetryXForwardedTest.java @@ -15,6 +15,8 @@ import org.junit.jupiter.api.extension.RegisterExtension; import io.opentelemetry.sdk.trace.data.SpanData; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporter; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporterProvider; import io.quarkus.opentelemetry.deployment.common.SemconvResolver; import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; @@ -28,8 +30,11 @@ public class VertxOpenTelemetryXForwardedTest { .withApplicationRoot((jar) -> jar .addClass(TracerRouter.class) .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class, SemconvResolver.class) + .addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), - "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider")) .withConfigurationResource("application-default.properties"); @Inject diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/AddingSpanAttributesInterceptorTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/AddingSpanAttributesInterceptorTest.java index 9853edf6aa817..eec6fb9ebd107 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/AddingSpanAttributesInterceptorTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/AddingSpanAttributesInterceptorTest.java @@ -38,7 +38,7 @@ public class AddingSpanAttributesInterceptorTest { .addAsManifestResource( "META-INF/services-config/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", "services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") - .addAsResource("resource-config/application.properties", "application.properties")); + .addAsResource("resource-config/application-no-metrics.properties", "application.properties")); @Inject HelloRouter helloRouter; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanInterceptorTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanInterceptorTest.java index b7a0796320eb8..a27ffefde3dc1 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanInterceptorTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanInterceptorTest.java @@ -53,7 +53,7 @@ public class WithSpanInterceptorTest { .addAsManifestResource( "META-INF/services-config/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", "services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") - .addAsResource("resource-config/application.properties", "application.properties")); + .addAsResource("resource-config/application-no-metrics.properties", "application.properties")); @Inject SpanBean spanBean; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanLegacyInterceptorTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanLegacyInterceptorTest.java index a38c18c57f626..6e334e0797421 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanLegacyInterceptorTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/interceptor/WithSpanLegacyInterceptorTest.java @@ -44,7 +44,7 @@ public class WithSpanLegacyInterceptorTest { .addAsManifestResource( "META-INF/services-config/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", "services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") - .addAsResource("resource-config/application.properties", "application.properties")); + .addAsResource("resource-config/application-no-metrics.properties", "application.properties")); @Inject SpanBean spanBean; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/GaugeCdiTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/GaugeCdiTest.java new file mode 100644 index 0000000000000..ae133ff6a805d --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/GaugeCdiTest.java @@ -0,0 +1,85 @@ +package io.quarkus.opentelemetry.deployment.metrics; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.Meter; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporter; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporterProvider; +import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; +import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; +import io.quarkus.opentelemetry.deployment.common.TestUtil; +import io.quarkus.test.QuarkusUnitTest; + +public class GaugeCdiTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClass(TestUtil.class) + .addClass(MeterBean.class) + .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) + .addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class) + .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider") + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider") + .add(new StringAsset( + "quarkus.otel.metrics.enabled=true\n" + + "quarkus.datasource.db-kind=h2\n" + + "quarkus.datasource.jdbc.telemetry=true\n" + + "quarkus.otel.traces.exporter=test-span-exporter\n" + + "quarkus.otel.metrics.exporter=in-memory\n" + + "quarkus.otel.metric.export.interval=300ms\n" + + "quarkus.otel.bsp.export.timeout=1s\n" + + "quarkus.otel.bsp.schedule.delay=50\n"), + "application.properties")); + + @Inject + MeterBean meterBean; + + @Inject + InMemoryMetricExporter exporter; + + @BeforeEach + void setUp() { + exporter.reset(); + } + + @Test + void gauge() throws InterruptedException { + meterBean.getMeter() + .gaugeBuilder("jvm.memory.total") + .setDescription("Reports JVM memory usage.") + .setUnit("byte") + .buildWithCallback( + result -> result.record(Runtime.getRuntime().totalMemory(), Attributes.empty())); + exporter.assertCountAtLeast("jvm.memory.total", null, 1); + assertNotNull(exporter.getFinishedMetricItems("jvm.memory.total", null).get(0)); + } + + @Test + void meter() { + assertNotNull(meterBean.getMeter()); + } + + @ApplicationScoped + public static class MeterBean { + @Inject + Meter meter; + + public Meter getMeter() { + return meter; + } + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/MetricsDisabledTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/MetricsDisabledTest.java new file mode 100644 index 0000000000000..41196b759df8f --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/MetricsDisabledTest.java @@ -0,0 +1,30 @@ +package io.quarkus.opentelemetry.deployment.metrics; + +import jakarta.enterprise.inject.spi.DeploymentException; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.api.metrics.Meter; +import io.quarkus.test.QuarkusUnitTest; + +public class MetricsDisabledTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withEmptyApplication() + .overrideConfigKey("quarkus.otel.metrics.enabled", "false") + .assertException(t -> Assertions.assertEquals(DeploymentException.class, t.getClass())); + + @Inject + Meter openTelemetryMeter; + + @Test + void testNoOpenTelemetry() { + //Should not be reached: dump what was injected if it somehow passed + Assertions.assertNull(openTelemetryMeter, + "A OpenTelemetry Meter instance should not be found/injected when OpenTelemetry metrics is disabled"); + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/MetricsTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/MetricsTest.java new file mode 100644 index 0000000000000..9c6ed9e2191bc --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/metrics/MetricsTest.java @@ -0,0 +1,385 @@ +package io.quarkus.opentelemetry.deployment.metrics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleCounter; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporter; +import io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporterProvider; +import io.quarkus.opentelemetry.deployment.common.TestSpanExporter; +import io.quarkus.opentelemetry.deployment.common.TestSpanExporterProvider; +import io.quarkus.test.QuarkusUnitTest; + +public class MetricsTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) + .addClasses(InMemoryMetricExporter.class, InMemoryMetricExporterProvider.class) + .addAsResource(new StringAsset(InMemoryMetricExporterProvider.class.getCanonicalName()), + "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider") + .add(new StringAsset( + "quarkus.otel.metrics.enabled=true\n" + + "quarkus.otel.traces.exporter=none\n" + + "quarkus.otel.metrics.exporter=in-memory\n" + + "quarkus.otel.metric.export.interval=300ms\n"), + "application.properties")); + + @Inject + protected Meter meter; + @Inject + protected InMemoryMetricExporter metricExporter; + + protected static String mapToString(Map, ?> map) { + return (String) map.keySet().stream() + .map(key -> "" + key.getKey() + "=" + map.get(key)) + .collect(Collectors.joining(", ", "{", "}")); + } + + @BeforeEach + void setUp() { + metricExporter.reset(); + } + + @Test + void asyncDoubleCounter() { + final String counterName = "testAsyncDoubleCounter"; + final String counterDescription = "Testing double counter"; + final String counterUnit = "Metric Tonnes"; + assertNotNull( + meter.counterBuilder(counterName) + .ofDoubles() + .setDescription(counterDescription) + .setUnit(counterUnit) + .buildWithCallback(measurement -> { + measurement.record(1, Attributes.empty()); + })); + + metricExporter.assertCountAtLeast(counterName, null, 1); + MetricData metric = metricExporter.getFinishedMetricItems(counterName, null).get(0); + + assertEquals(metric.getName(), counterName); + assertEquals(metric.getDescription(), counterDescription); + assertEquals(metric.getUnit(), counterUnit); + + assertEquals(metric.getDoubleSumData() + .getPoints() + .stream() + .findFirst() + .get() + .getValue(), 1); + } + + @Test + void asyncLongCounter() { + final String counterName = "testAsyncLongCounter"; + final String counterDescription = "Testing Async long counter"; + final String counterUnit = "Metric Tonnes"; + assertNotNull( + meter.counterBuilder(counterName) + .setDescription(counterDescription) + .setUnit(counterUnit) + .buildWithCallback(measurement -> { + measurement.record(1, Attributes.empty()); + })); + + metricExporter.assertCountAtLeast(counterName, null, 1); + MetricData metric = metricExporter.getFinishedMetricItems(counterName, null).get(0); + + assertEquals(metric.getName(), counterName); + assertEquals(metric.getDescription(), counterDescription); + assertEquals(metric.getUnit(), counterUnit); + + assertEquals(metric.getLongSumData() + .getPoints() + .stream() + .findFirst() + .get() + .getValue(), 1); + } + + @Test + void doubleCounter() { + final String counterName = "testDoubleCounter"; + final String counterDescription = "Testing double counter"; + final String counterUnit = "Metric Tonnes"; + + final double doubleWithAttributes = 20.2; + final double doubleWithoutAttributes = 10.1; + DoubleCounter doubleCounter = meter.counterBuilder(counterName) + .ofDoubles() + .setDescription(counterDescription) + .setUnit(counterUnit) + .build(); + assertNotNull(doubleCounter); + + Map expectedResults = new HashMap(); + expectedResults.put(doubleWithAttributes, Attributes.builder().put("K", "V").build()); + expectedResults.put(doubleWithoutAttributes, Attributes.empty()); + expectedResults.keySet().stream() + .forEach(key -> doubleCounter.add(key, expectedResults.get(key))); + + metricExporter.assertCountAtLeast(counterName, null, 1); + MetricData metric = metricExporter.getFinishedMetricItems(counterName, null).get(0); + + assertEquals(metric.getName(), counterName); + assertEquals(metric.getDescription(), counterDescription); + assertEquals(metric.getUnit(), counterUnit); + + metric.getDoubleSumData().getPoints().stream() + .forEach(point -> { + assertTrue(expectedResults.containsKey(point.getValue()), + "Double" + point.getValue() + " was not an expected result"); + assertTrue(point.getAttributes().equals(expectedResults.get(point.getValue())), + "Attributes were not equal." + + System.lineSeparator() + "Actual values: " + + mapToString(point.getAttributes().asMap()) + + System.lineSeparator() + "Expected values: " + + mapToString(expectedResults.get(point.getValue()).asMap())); + }); + + } + + @Test + void doubleGauge() { + final String gaugeName = "testDoubleGauge"; + final String gaugeDescription = "Testing double gauge"; + final String gaugeUnit = "ms"; + assertNotNull( + meter.gaugeBuilder(gaugeName) + .setDescription(gaugeDescription) + .setUnit("ms") + .buildWithCallback(measurement -> { + measurement.record(1, Attributes.empty()); + })); + + metricExporter.assertCountAtLeast(gaugeName, null, 1); + MetricData metric = metricExporter.getFinishedMetricItems(gaugeName, null).get(0); + + assertEquals(metric.getName(), gaugeName); + assertEquals(metric.getDescription(), gaugeDescription); + assertEquals(metric.getUnit(), gaugeUnit); + + assertEquals(metric.getDoubleGaugeData() + .getPoints() + .stream() + .findFirst() + .get() + .getValue(), 1); + } + + @Test + void doubleHistogram() { + final String histogramName = "testDoubleHistogram"; + final String histogramDescription = "Testing double histogram"; + final String histogramUnit = "Metric Tonnes"; + + final double doubleWithAttributes = 20; + final double doubleWithoutAttributes = 10; + DoubleHistogram doubleHistogram = meter.histogramBuilder(histogramName) + .setDescription(histogramDescription) + .setUnit(histogramUnit) + .build(); + assertNotNull(doubleHistogram); + + Map expectedResults = new HashMap(); + expectedResults.put(doubleWithAttributes, Attributes.builder().put("K", "V").build()); + expectedResults.put(doubleWithoutAttributes, Attributes.empty()); + expectedResults.keySet().stream() + .forEach(key -> doubleHistogram.record(key, expectedResults.get(key))); + + metricExporter.assertCountAtLeast(histogramName, null, 1); + MetricData metric = metricExporter.getFinishedMetricItems(histogramName, null).get(0); + + assertEquals(metric.getName(), histogramName); + assertEquals(metric.getDescription(), histogramDescription); + assertEquals(metric.getUnit(), histogramUnit); + + metric.getHistogramData().getPoints().stream() + .forEach(point -> { + assertTrue(expectedResults.containsKey(point.getSum()), + "Double " + point.getSum() + " was not an expected result"); + assertTrue(point.getAttributes().equals(expectedResults.get(point.getSum())), + "Attributes were not equal." + + System.lineSeparator() + "Actual values: " + + mapToString(point.getAttributes().asMap()) + + System.lineSeparator() + "Expected values: " + + mapToString(expectedResults.get(point.getSum()).asMap())); + }); + } + + @Test + void longCounter() { + final String counterName = "testLongCounter"; + final String counterDescription = "Testing long counter"; + final String counterUnit = "Metric Tonnes"; + + final long longWithAttributes = 24; + final long longWithoutAttributes = 12; + LongCounter longCounter = meter.counterBuilder(counterName) + .setDescription(counterDescription) + .setUnit(counterUnit) + .build(); + assertNotNull(longCounter); + + Map expectedResults = new HashMap(); + expectedResults.put(longWithAttributes, Attributes.builder().put("K", "V").build()); + expectedResults.put(longWithoutAttributes, Attributes.empty()); + expectedResults.keySet().stream().forEach(key -> longCounter.add(key, expectedResults.get(key))); + + metricExporter.assertCountAtLeast(counterName, null, 1); + MetricData metric = metricExporter.getFinishedMetricItems(counterName, null).get(0); + + assertEquals(metric.getName(), counterName); + assertEquals(metric.getDescription(), counterDescription); + assertEquals(metric.getUnit(), counterUnit); + + metric.getLongSumData().getPoints().stream() + .forEach(point -> { + assertTrue(expectedResults.containsKey(point.getValue()), + "Long" + point.getValue() + " was not an expected result"); + assertTrue(point.getAttributes().equals(expectedResults.get(point.getValue())), + "Attributes were not equal." + + System.lineSeparator() + "Actual values: " + + mapToString(point.getAttributes().asMap()) + + System.lineSeparator() + "Expected values: " + + mapToString(expectedResults.get(point.getValue()).asMap())); + }); + } + + @Test + void longGauge() { + final String gaugeName = "testLongGauge"; + final String gaugeDescription = "Testing long gauge"; + final String gaugeUnit = "ms"; + assertNotNull( + meter.gaugeBuilder(gaugeName) + .ofLongs() + .setDescription(gaugeDescription) + .setUnit("ms") + .buildWithCallback(measurement -> { + measurement.record(1, Attributes.empty()); + })); + + metricExporter.assertCountAtLeast(gaugeName, null, 1); + MetricData metric = metricExporter.getFinishedMetricItems(gaugeName, null).get(0); + + assertEquals(metric.getName(), gaugeName); + assertEquals(metric.getDescription(), gaugeDescription); + assertEquals(metric.getUnit(), gaugeUnit); + + assertEquals(metric.getLongGaugeData() + .getPoints() + .stream() + .findFirst() + .get() + .getValue(), 1); + } + + @Test + void longHistogram() { + final String histogramName = "testLongHistogram"; + final String histogramDescription = "Testing long histogram"; + final String histogramUnit = "Metric Tonnes"; + + final long longWithAttributes = 20; + final long longWithoutAttributes = 10; + LongHistogram longHistogram = meter.histogramBuilder(histogramName) + .ofLongs() + .setDescription(histogramDescription) + .setUnit(histogramUnit) + .build(); + assertNotNull(longHistogram); + + Map expectedResults = new HashMap(); + expectedResults.put(longWithAttributes, Attributes.builder().put("K", "V").build()); + expectedResults.put(longWithoutAttributes, Attributes.empty()); + + expectedResults.keySet().stream() + .forEach(key -> longHistogram.record(key, expectedResults.get(key))); + + metricExporter.assertCountAtLeast(histogramName, null, 1); + MetricData metric = metricExporter.getFinishedMetricItems(histogramName, null).get(0); + + assertEquals(metric.getName(), histogramName); + assertEquals(metric.getDescription(), histogramDescription); + assertEquals(metric.getUnit(), histogramUnit); + + metric.getHistogramData().getPoints().stream() + .forEach(point -> { + assertTrue(expectedResults.containsKey((long) point.getSum()), + "Long " + (long) point.getSum() + " was not an expected result"); + assertTrue(point.getAttributes().equals(expectedResults.get((long) point.getSum())), + "Attributes were not equal." + + System.lineSeparator() + "Actual values: " + + mapToString(point.getAttributes().asMap()) + + System.lineSeparator() + "Expected values: " + + mapToString(expectedResults.get((long) point.getSum()).asMap())); + }); + } + + @Test + void longUpDownCounter() { + final String counterName = "testLongUpDownCounter"; + final String counterDescription = "Testing long up down counter"; + final String counterUnit = "Metric Tonnes"; + + final long longWithAttributes = -20; + final long longWithoutAttributes = -10; + LongUpDownCounter longUpDownCounter = meter.upDownCounterBuilder(counterName) + .setDescription(counterDescription) + .setUnit(counterUnit) + .build(); + assertNotNull(longUpDownCounter); + + Map expectedResults = new HashMap(); + expectedResults.put(longWithAttributes, Attributes.builder().put("K", "V").build()); + expectedResults.put(longWithoutAttributes, Attributes.empty()); + + expectedResults.keySet().stream() + .forEach(key -> longUpDownCounter.add(key, expectedResults.get(key))); + + metricExporter.assertCountAtLeast(counterName, null, 1); + MetricData metric = metricExporter.getFinishedMetricItems(counterName, null).get(0); + + assertEquals(metric.getName(), counterName); + assertEquals(metric.getDescription(), counterDescription); + assertEquals(metric.getUnit(), counterUnit); + + metric.getDoubleSumData().getPoints().stream() + .forEach(point -> { + assertTrue(expectedResults.containsKey(point.getValue()), + "Long" + point.getValue() + " was not an expected result"); + assertTrue(point.getAttributes().equals(expectedResults.get(point.getValue())), + "Attributes were not equal." + + System.lineSeparator() + "Actual values: " + + mapToString(point.getAttributes().asMap()) + + System.lineSeparator() + "Expected values: " + + mapToString(expectedResults.get(point.getValue()).asMap())); + }); + + } +} diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTextMapPropagatorCustomizerTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/propagation/OpenTelemetryTextMapPropagatorCustomizerTest.java similarity index 96% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTextMapPropagatorCustomizerTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/propagation/OpenTelemetryTextMapPropagatorCustomizerTest.java index a230d019f7c41..abb2a1c762f9d 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryTextMapPropagatorCustomizerTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/propagation/OpenTelemetryTextMapPropagatorCustomizerTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.propagation; import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; import static org.assertj.core.api.Assertions.assertThat; @@ -39,7 +39,7 @@ public class OpenTelemetryTextMapPropagatorCustomizerTest { .addClass(TestSpanExporter.class) .addClass(TestSpanExporterProvider.class) .addClass(TestTextMapPropagatorCustomizer.class) - .addAsResource("resource-config/application.properties", "application.properties") + .addAsResource("resource-config/application-no-metrics.properties", "application.properties") .addAsResource( "META-INF/services-config/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsDisabledTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsDisabledTest.java similarity index 92% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsDisabledTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsDisabledTest.java index 0cf4e370432fe..cd0a044062461 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsDisabledTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsDisabledTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; @@ -23,7 +23,7 @@ public class NonAppEndpointsDisabledTest { .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) - .withConfigurationResource("application-default.properties"); + .withConfigurationResource("resource-config/application-no-metrics.properties"); @Inject TestSpanExporter testSpanExporter; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsDisabledWithRootPathTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsDisabledWithRootPathTest.java similarity index 93% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsDisabledWithRootPathTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsDisabledWithRootPathTest.java index c468f987e7690..48589c7abb23b 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsDisabledWithRootPathTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsDisabledWithRootPathTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; @@ -23,7 +23,7 @@ public class NonAppEndpointsDisabledWithRootPathTest { .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) - .withConfigurationResource("application-default.properties") + .withConfigurationResource("resource-config/application-no-metrics.properties") .overrideConfigKey("quarkus.http.root-path", "/app") .overrideConfigKey("quarkus.http.non-application-root-path", "quarkus"); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsEnabledLegacyConfigurationTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsEnabledLegacyConfigurationTest.java similarity index 93% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsEnabledLegacyConfigurationTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsEnabledLegacyConfigurationTest.java index 9c094dc703990..8d8b7fef27720 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsEnabledLegacyConfigurationTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsEnabledLegacyConfigurationTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; @@ -23,7 +23,7 @@ public class NonAppEndpointsEnabledLegacyConfigurationTest { .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) - .withConfigurationResource("application-default.properties") + .withConfigurationResource("resource-config/application-no-metrics.properties") .overrideConfigKey("quarkus.otel.traces.suppress-non-application-uris", "false"); @Inject diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsEnabledTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsEnabledTest.java similarity index 93% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsEnabledTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsEnabledTest.java index cc7fe7621c4ee..8e36d68c90cbb 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsEnabledTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsEnabledTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; @@ -23,7 +23,7 @@ public class NonAppEndpointsEnabledTest { .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) - .withConfigurationResource("application-default.properties") + .withConfigurationResource("resource-config/application-no-metrics.properties") .overrideConfigKey("quarkus.otel.traces.suppress-non-application-uris", "false"); @Inject diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsEqualRootPath.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsEqualRootPath.java similarity index 93% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsEqualRootPath.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsEqualRootPath.java index a0785225cd75f..0ce7116abdbee 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/NonAppEndpointsEqualRootPath.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/NonAppEndpointsEqualRootPath.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; @@ -23,7 +23,7 @@ public class NonAppEndpointsEqualRootPath { .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) - .withConfigurationResource("application-default.properties") + .withConfigurationResource("resource-config/application-no-metrics.properties") .overrideConfigKey("quarkus.http.root-path", "/app") .overrideConfigKey("quarkus.http.non-application-root-path", "/app"); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryCustomSamplerBeanTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryCustomSamplerBeanTest.java similarity index 96% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryCustomSamplerBeanTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryCustomSamplerBeanTest.java index 95cb9c7ee9835..9196e280c3dbc 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryCustomSamplerBeanTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryCustomSamplerBeanTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; @@ -43,7 +43,7 @@ public class OpenTelemetryCustomSamplerBeanTest { .addClass(TestUtil.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) - .withConfigurationResource("application-default.properties"); + .withConfigurationResource("resource-config/application-no-metrics.properties"); @Inject OpenTelemetry openTelemetry; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDILegacyTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryHttpCDILegacyTest.java similarity index 94% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDILegacyTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryHttpCDILegacyTest.java index dd03e834fa0a9..fdb7218df99e9 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDILegacyTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryHttpCDILegacyTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static io.opentelemetry.api.trace.SpanKind.INTERNAL; import static io.opentelemetry.api.trace.SpanKind.SERVER; @@ -42,7 +42,7 @@ public class OpenTelemetryHttpCDILegacyTest { .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) - .withConfigurationResource("application-default.properties"); + .withConfigurationResource("resource-config/application-no-metrics.properties"); @Inject TestSpanExporter spanExporter; @@ -66,7 +66,7 @@ void telemetry() { assertEquals(SERVER, server.getKind()); // verify that OpenTelemetryServerFilter took place assertStringAttribute(server, SemanticAttributes.CODE_NAMESPACE, - "io.quarkus.opentelemetry.deployment.OpenTelemetryHttpCDILegacyTest$HelloResource"); + "io.quarkus.opentelemetry.deployment.traces.OpenTelemetryHttpCDILegacyTest$HelloResource"); assertStringAttribute(server, SemanticAttributes.CODE_FUNCTION, "hello"); SpanData internal = getSpanByKindAndParentId(spans, INTERNAL, server.getSpanId()); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryHttpCDITest.java similarity index 94% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryHttpCDITest.java index 19027b911c568..f8bd9be6817e5 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryHttpCDITest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryHttpCDITest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static io.opentelemetry.api.trace.SpanKind.INTERNAL; import static io.opentelemetry.api.trace.SpanKind.SERVER; @@ -42,7 +42,7 @@ public class OpenTelemetryHttpCDITest { .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) - .withConfigurationResource("application-default.properties"); + .withConfigurationResource("resource-config/application-no-metrics.properties"); @Inject TestSpanExporter spanExporter; @@ -65,7 +65,7 @@ void telemetry() { assertEquals("GET /hello", server.getName()); // verify that OpenTelemetryServerFilter took place assertStringAttribute(server, SemanticAttributes.CODE_NAMESPACE, - "io.quarkus.opentelemetry.deployment.OpenTelemetryHttpCDITest$HelloResource"); + "io.quarkus.opentelemetry.deployment.traces.OpenTelemetryHttpCDITest$HelloResource"); assertStringAttribute(server, SemanticAttributes.CODE_FUNCTION, "hello"); final SpanData internalFromBean = getSpanByKindAndParentId(spans, INTERNAL, server.getSpanId()); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryIdGeneratorTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryIdGeneratorTest.java similarity index 98% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryIdGeneratorTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryIdGeneratorTest.java index 1d4f148342567..fbf70b4ea0a65 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryIdGeneratorTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryIdGeneratorTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryJdbcInstrumentationValidationTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryJdbcInstrumentationValidationTest.java similarity index 93% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryJdbcInstrumentationValidationTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryJdbcInstrumentationValidationTest.java index 4f9f34bcade87..535bfcbd02cc0 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryJdbcInstrumentationValidationTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryJdbcInstrumentationValidationTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -17,6 +17,7 @@ public class OpenTelemetryJdbcInstrumentationValidationTest { .withApplicationRoot((jar) -> jar .addAsResource(new StringAsset( "quarkus.datasource.db-kind=h2\n" + + "quarkus.otel.metrics.exporter=none\n" + "quarkus.datasource.jdbc.driver=io.opentelemetry.instrumentation.jdbc.OpenTelemetryDriver\n"), "application.properties")) .assertException(t -> { diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryReactiveRoutesTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryReactiveRoutesTest.java similarity index 95% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryReactiveRoutesTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryReactiveRoutesTest.java index 2af44b8e60a37..fd9b3088e43e7 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetryReactiveRoutesTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetryReactiveRoutesTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static io.opentelemetry.api.trace.SpanKind.SERVER; import static io.quarkus.opentelemetry.deployment.common.TestSpanExporter.getSpanByKindAndParentId; @@ -35,7 +35,7 @@ public class OpenTelemetryReactiveRoutesTest { .addClass(TestUtil.class) .addClass(TestSpanExporter.class) .addClass(TestSpanExporterProvider.class) - .addAsResource("resource-config/application.properties", "application.properties") + .addAsResource("resource-config/application-no-metrics.properties", "application.properties") .addAsResource( "META-INF/services-config/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySamplerBeanTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerBeanTest.java similarity index 92% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySamplerBeanTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerBeanTest.java index c58566046a8be..d155ed7d7fb21 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySamplerBeanTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerBeanTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.stringContainsInOrder; @@ -27,7 +27,7 @@ public class OpenTelemetrySamplerBeanTest { .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) - .withConfigurationResource("application-default.properties"); + .withConfigurationResource("resource-config/application-no-metrics.properties"); @Inject OpenTelemetry openTelemetry; diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySamplerConfigTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerConfigTest.java similarity index 92% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySamplerConfigTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerConfigTest.java index 728c9b66af6c5..0ff64298d64b4 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySamplerConfigTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySamplerConfigTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -24,7 +24,7 @@ public class OpenTelemetrySamplerConfigTest { .addClasses(TestSpanExporter.class, TestSpanExporterProvider.class) .addAsResource(new StringAsset(TestSpanExporterProvider.class.getCanonicalName()), "META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider")) - .withConfigurationResource("application-default.properties") + .withConfigurationResource("resource-config/application-no-metrics.properties") .overrideConfigKey("quarkus.otel.traces.sampler", "traceidratio") .overrideConfigKey("quarkus.otel.traces.sampler.arg", "0.5") .overrideConfigKey("quarkus.otel.traces.suppress-non-application-uris", "false"); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySpanSecurityEventsTest.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySpanSecurityEventsTest.java similarity index 97% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySpanSecurityEventsTest.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySpanSecurityEventsTest.java index 616786d7ee668..41c2648e11c50 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/OpenTelemetrySpanSecurityEventsTest.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/OpenTelemetrySpanSecurityEventsTest.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -39,6 +39,7 @@ public class OpenTelemetrySpanSecurityEventsTest { CustomSecurityEvent.class) .addAsResource(new StringAsset(""" quarkus.otel.security-events.enabled=true + quarkus.otel.metrics.exporter=none quarkus.otel.security-events.event-types=AUTHENTICATION_SUCCESS,AUTHORIZATION_SUCCESS,OTHER """), "application.properties")); diff --git a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/TracerRouterUT.java b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/TracerRouterUT.java similarity index 89% rename from extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/TracerRouterUT.java rename to extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/TracerRouterUT.java index 094e26d8e89cf..ef125fe6bc45c 100644 --- a/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/TracerRouterUT.java +++ b/extensions/opentelemetry/deployment/src/test/java/io/quarkus/opentelemetry/deployment/traces/TracerRouterUT.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.deployment; +package io.quarkus.opentelemetry.deployment.traces; import static org.hamcrest.Matchers.is; diff --git a/extensions/opentelemetry/deployment/src/test/resources/META-INF/services-config/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider b/extensions/opentelemetry/deployment/src/test/resources/META-INF/services-config/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider new file mode 100644 index 0000000000000..52aa734e638e5 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/resources/META-INF/services-config/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider @@ -0,0 +1 @@ +io.quarkus.opentelemetry.deployment.common.InMemoryMetricExporterProvider \ No newline at end of file diff --git a/extensions/opentelemetry/deployment/src/test/resources/application-default.properties b/extensions/opentelemetry/deployment/src/test/resources/application-default.properties index 3284a760fc5eb..5871e27ba54f5 100644 --- a/extensions/opentelemetry/deployment/src/test/resources/application-default.properties +++ b/extensions/opentelemetry/deployment/src/test/resources/application-default.properties @@ -1,3 +1,5 @@ quarkus.otel.traces.exporter=test-span-exporter -quarkus.otel.bsp.schedule.delay=50 -quarkus.otel.bsp.export.timeout=1s \ No newline at end of file +quarkus.otel.bsp.schedule.delay=50ms +quarkus.otel.bsp.export.timeout=1s +quarkus.otel.metrics.exporter=in-memory +quarkus.otel.metric.export.interval=300ms \ No newline at end of file diff --git a/extensions/opentelemetry/deployment/src/test/resources/resource-config/application-no-metrics.properties b/extensions/opentelemetry/deployment/src/test/resources/resource-config/application-no-metrics.properties new file mode 100644 index 0000000000000..cb6529bd136d5 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/test/resources/resource-config/application-no-metrics.properties @@ -0,0 +1,8 @@ +quarkus.application.name=resource-test +quarkus.otel.resource.attributes=service.name=authservice,service.instance.id=${quarkus.uuid} + +quarkus.otel.traces.exporter=test-span-exporter +quarkus.otel.bsp.schedule.delay=50ms +quarkus.otel.metrics.exporter=none + +quarkus.rest-client.client.url=${test.url} diff --git a/extensions/opentelemetry/deployment/src/test/resources/resource-config/application.properties b/extensions/opentelemetry/deployment/src/test/resources/resource-config/application.properties index a63d6ddd2f6b9..3d4b806814f52 100644 --- a/extensions/opentelemetry/deployment/src/test/resources/resource-config/application.properties +++ b/extensions/opentelemetry/deployment/src/test/resources/resource-config/application.properties @@ -2,6 +2,9 @@ quarkus.application.name=resource-test quarkus.otel.resource.attributes=service.name=authservice,service.instance.id=${quarkus.uuid} quarkus.otel.traces.exporter=test-span-exporter -quarkus.otel.bsp.schedule.delay=50 +quarkus.otel.bsp.schedule.delay=50ms +quarkus.otel.metrics.enabled=true +quarkus.otel.metrics.exporter=in-memory +quarkus.otel.metric.export.interval=300ms quarkus.rest-client.client.url=${test.url} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/OpenTelemetryDestroyer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/OpenTelemetryDestroyer.java index b76b36bccfa1d..8807c18b74b37 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/OpenTelemetryDestroyer.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/OpenTelemetryDestroyer.java @@ -23,7 +23,8 @@ public void destroy(OpenTelemetry openTelemetry, CreationalContext !sn.equals(appConfig.name.orElse("unset"))) + .filter(new Predicate() { + @Override + public boolean test(String sn) { + return !sn.equals(appConfig.name.orElse("unset")); + } + }) .orElse(null); // must be resolved at startup, once. @@ -170,19 +178,54 @@ public void customize(AutoConfiguredOpenTelemetrySdkBuilder builder) { builder.addTracerProviderCustomizer( new BiFunction<>() { @Override - public SdkTracerProviderBuilder apply(SdkTracerProviderBuilder builder, + public SdkTracerProviderBuilder apply(SdkTracerProviderBuilder tracerProviderBuilder, ConfigProperties configProperties) { if (oTelBuildConfig.traces().enabled().orElse(TRUE)) { - idGenerator.stream().findFirst().ifPresent(builder::setIdGenerator); // from cdi - spanProcessors.stream().filter(sp -> !(sp instanceof RemoveableLateBoundBatchSpanProcessor)) - .forEach(builder::addSpanProcessor); + idGenerator.stream().findFirst().ifPresent(tracerProviderBuilder::setIdGenerator); // from cdi + spanProcessors.stream().filter(new Predicate() { + @Override + public boolean test(SpanProcessor sp) { + return !(sp instanceof RemoveableLateBoundBatchSpanProcessor); + } + }) + .forEach(tracerProviderBuilder::addSpanProcessor); } - return builder; + return tracerProviderBuilder; } }); } } + @Singleton + final class MetricProviderCustomizer implements AutoConfiguredOpenTelemetrySdkBuilderCustomizer { + private final OTelBuildConfig oTelBuildConfig; + private final Instance clock; + + public MetricProviderCustomizer(OTelBuildConfig oTelBuildConfig, + final Instance clock) { + this.oTelBuildConfig = oTelBuildConfig; + this.clock = clock; + } + + @Override + public void customize(AutoConfiguredOpenTelemetrySdkBuilder builder) { + if (oTelBuildConfig.metrics().enabled().orElse(TRUE)) { + builder.addMeterProviderCustomizer( + new BiFunction() { + @Override + public SdkMeterProviderBuilder apply(SdkMeterProviderBuilder metricProvider, + ConfigProperties configProperties) { + if (clock.isUnsatisfied()) { + throw new IllegalStateException("No Clock bean found"); + } + metricProvider.setClock(clock.get()); + return metricProvider; + } + }); + } + } + } + @Singleton final class TextMapPropagatorCustomizers implements AutoConfiguredOpenTelemetrySdkBuilderCustomizer { diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/ExporterType.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/ExporterType.java index 69c4378e170a8..85ce3e31e17c4 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/ExporterType.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/ExporterType.java @@ -2,7 +2,7 @@ public enum ExporterType { OTLP(Constants.OTLP_VALUE), - // HTTP(Constants.HTTP_VALUE), // TODO not supported yet + HTTP(Constants.HTTP_VALUE), // JAEGER(Constants.JAEGER), // Moved to Quarkiverse /** * To be used by legacy CDI beans setup. Will be removed soon. @@ -23,7 +23,7 @@ public String getValue() { public static class Constants { public static final String OTLP_VALUE = "otlp"; public static final String CDI_VALUE = "cdi"; - // public static final String HTTP_VALUE = "http"; + public static final String HTTP_VALUE = "http"; public static final String NONE_VALUE = "none"; public static final String JAEGER = "jaeger"; } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/MetricsBuildConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/MetricsBuildConfig.java new file mode 100644 index 0000000000000..eb58074ef2e77 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/MetricsBuildConfig.java @@ -0,0 +1,28 @@ +package io.quarkus.opentelemetry.runtime.config.build; + +import static io.quarkus.opentelemetry.runtime.config.build.ExporterType.Constants.CDI_VALUE; + +import java.util.List; +import java.util.Optional; + +import io.smallrye.config.WithDefault; + +public interface MetricsBuildConfig { + + /** + * Enable metrics with OpenTelemetry. + *

+ * This property is not available in the Open Telemetry SDK. It's Quarkus specific. + *

+ * Support for metrics will be enabled if OpenTelemetry support is enabled + * and either this value is true, or this value is unset. + */ + @WithDefault("false") + Optional enabled(); + + /** + * The Metrics exporter to use. + */ + @WithDefault(CDI_VALUE) + List exporter(); +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java index 6a7a5aa09928b..1e1bef92e25a3 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/OTelBuildConfig.java @@ -35,7 +35,6 @@ public interface OTelBuildConfig { *

* Defaults to true. */ - @Deprecated // TODO only use runtime (soon) @WithDefault("true") boolean enabled(); @@ -45,11 +44,9 @@ public interface OTelBuildConfig { TracesBuildConfig traces(); /** - * No Metrics exporter for now + * Metrics exporter configurations. */ - @WithName("metrics.exporter") - @WithDefault("none") - List metricsExporter(); + MetricsBuildConfig metrics(); /** * No Log exporter for now. diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/ExemplarsFilterType.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/ExemplarsFilterType.java new file mode 100644 index 0000000000000..9504070122212 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/ExemplarsFilterType.java @@ -0,0 +1,23 @@ +package io.quarkus.opentelemetry.runtime.config.runtime; + +public enum ExemplarsFilterType { + TRACE_BASED(Constants.TRACE_BASED), + ALWAYS_ON(Constants.ALWAYS_ON), + ALWAYS_OFF(Constants.ALWAYS_OFF); + + private final String value; + + ExemplarsFilterType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static class Constants { + public static final String TRACE_BASED = "trace_based"; + public static final String ALWAYS_ON = "always_on"; + public static final String ALWAYS_OFF = "always_off"; + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/MetricsRuntimeConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/MetricsRuntimeConfig.java new file mode 100644 index 0000000000000..da9abd483eb3e --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/MetricsRuntimeConfig.java @@ -0,0 +1,20 @@ +package io.quarkus.opentelemetry.runtime.config.runtime; + +import java.time.Duration; + +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithName; + +public interface MetricsRuntimeConfig { + + /** + * The interval, between the start of two metric export attempts. + *

+ * Default is 1min. + * + * @return the interval Duration. + */ + @WithName("export.interval") + @WithDefault("60s") + Duration exportInterval(); +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/OTelRuntimeConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/OTelRuntimeConfig.java index 3de7decd485ca..8960f04d76f53 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/OTelRuntimeConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/OTelRuntimeConfig.java @@ -28,6 +28,11 @@ public interface OTelRuntimeConfig { */ TracesRuntimeConfig traces(); + /** + * Metric runtime config. + */ + MetricsRuntimeConfig metric(); + /** * environment variables for the types of attributes, for which that SDK implements truncation mechanism. */ diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterConfig.java new file mode 100644 index 0000000000000..8c6ac92c6c330 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterConfig.java @@ -0,0 +1,134 @@ +package io.quarkus.opentelemetry.runtime.config.runtime.exporter; + +import static io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterRuntimeConfig.DEFAULT_GRPC_BASE_URI; + +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithName; + +@ConfigGroup +public interface OtlpExporterConfig { + + /** + * OTLP Exporter specific. Will override otel.exporter.otlp.endpoint, if set. + *

+ * Fallbacks to the legacy property quarkus.opentelemetry.tracer.exporter.otlp.endpoint< or + * defaults to {@value OtlpExporterRuntimeConfig#DEFAULT_GRPC_BASE_URI}. + */ + @WithDefault(DEFAULT_GRPC_BASE_URI) + Optional endpoint(); + + /** + * Key-value pairs to be used as headers associated with gRPC requests. + * The format is similar to the {@code OTEL_EXPORTER_OTLP_HEADERS} environment variable, + * a list of key-value pairs separated by the "=" character. i.e.: key1=value1,key2=value2 + */ + Optional> headers(); + + /** + * Sets the method used to compress payloads. If unset, compression is disabled. Currently + * supported compression methods include `gzip` and `none`. + */ + Optional compression(); + + /** + * Sets the maximum time to wait for the collector to process an exported batch of spans. If + * unset, defaults to {@value OtlpExporterRuntimeConfig#DEFAULT_TIMEOUT_SECS}s. + */ + @WithDefault("10s") + Duration timeout(); + + /** + * OTLP defines the encoding of telemetry data and the protocol used to exchange data between the client and the + * server. Depending on the exporter, the available protocols will be different. + *

+ * Currently, only {@code grpc} and {@code http/protobuf} are allowed. + */ + @WithDefault(Protocol.GRPC) + Optional protocol(); + + /** + * Key/cert configuration in the PEM format. + */ + @WithName("key-cert") + KeyCert keyCert(); + + /** + * Trust configuration in the PEM format. + */ + @WithName("trust-cert") + TrustCert trustCert(); + + /** + * The name of the TLS configuration to use. + *

+ * If not set and the default TLS configuration is configured ({@code quarkus.tls.*}) then that will be used. + * If a name is configured, it uses the configuration from {@code quarkus.tls..*} + * If a name is configured, but no TLS configuration is found with that name then an error will be thrown. + */ + Optional tlsConfigurationName(); + + /** + * Set proxy options + */ + ProxyConfig proxyOptions(); + + interface ProxyConfig { + /** + * If proxy connection must be used. + */ + @WithDefault("false") + boolean enabled(); + + /** + * Set proxy username. + */ + Optional username(); + + /** + * Set proxy password. + */ + Optional password(); + + /** + * Set proxy port. + */ + @ConfigDocDefault("3128") + OptionalInt port(); + + /** + * Set proxy host. + */ + Optional host(); + } + + interface KeyCert { + /** + * Comma-separated list of the path to the key files (Pem format). + */ + Optional> keys(); + + /** + * Comma-separated list of the path to the certificate files (Pem format). + */ + Optional> certs(); + } + + interface TrustCert { + /** + * Comma-separated list of the trust certificate files (Pem format). + */ + Optional> certs(); + } + + class Protocol { + public static final String GRPC = "grpc"; + public static final String HTTP_PROTOBUF = "http/protobuf"; + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterMetricsConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterMetricsConfig.java new file mode 100644 index 0000000000000..a97889a9f055b --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterMetricsConfig.java @@ -0,0 +1,33 @@ +package io.quarkus.opentelemetry.runtime.config.runtime.exporter; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.smallrye.config.WithDefault; + +@ConfigGroup +public interface OtlpExporterMetricsConfig extends OtlpExporterConfig { + + /** + * The preferred output aggregation temporality. Options include DELTA, LOWMEMORY, and CUMULATIVE. + *

+ * If CUMULATIVE, all instruments will have cumulative temporality. + * If DELTA, counter (sync and async) and histograms will be delta, up down counters (sync and async) will be cumulative. + * If LOWMEMORY, sync counter and histograms will be delta, async counter and up down counters (sync and async) will be + * cumulative. + *

+ * Default is CUMULATIVE. + */ + @WithDefault("cumulative") + Optional temporalityPreference(); + + /** + * The preferred default histogram aggregation. + *

+ * Options include BASE2_EXPONENTIAL_BUCKET_HISTOGRAM and EXPLICIT_BUCKET_HISTOGRAM. + *

+ * Default is EXPLICIT_BUCKET_HISTOGRAM. + */ + @WithDefault("explicit_bucket_histogram") + Optional defaultHistogramAggregation(); +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterRuntimeConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterRuntimeConfig.java index a6e8725cf3cff..d2d308c841296 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterRuntimeConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterRuntimeConfig.java @@ -31,7 +31,11 @@ public interface OtlpExporterRuntimeConfig { * OTLP traces exporter configuration. */ OtlpExporterTracesConfig traces(); - // TODO metrics(); + + /** + * OTLP metrics exporter configuration. + */ + OtlpExporterMetricsConfig metrics(); // TODO logs(); // TODO additional global exporter configuration diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterTracesConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterTracesConfig.java index e815230ac4fd2..12eac3685da91 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterTracesConfig.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterTracesConfig.java @@ -2,27 +2,14 @@ import static io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterRuntimeConfig.DEFAULT_GRPC_BASE_URI; -import java.time.Duration; -import java.util.List; import java.util.Optional; -import java.util.OptionalInt; -import io.quarkus.runtime.annotations.ConfigDocDefault; import io.quarkus.runtime.annotations.ConfigGroup; import io.smallrye.config.WithDefault; import io.smallrye.config.WithName; @ConfigGroup -public interface OtlpExporterTracesConfig { - - /** - * OTLP Exporter specific. Will override otel.exporter.otlp.endpoint, if set. - *

- * Fallbacks to the legacy property quarkus.opentelemetry.tracer.exporter.otlp.endpoint< or - * defaults to {@value OtlpExporterRuntimeConfig#DEFAULT_GRPC_BASE_URI}. - */ - @WithDefault(DEFAULT_GRPC_BASE_URI) - Optional endpoint(); +public interface OtlpExporterTracesConfig extends OtlpExporterConfig { /** * See {@link OtlpExporterTracesConfig#endpoint} @@ -32,113 +19,4 @@ public interface OtlpExporterTracesConfig { @WithName("legacy-endpoint") @WithDefault(DEFAULT_GRPC_BASE_URI) Optional legacyEndpoint(); - - /** - * Key-value pairs to be used as headers associated with gRPC requests. - * The format is similar to the {@code OTEL_EXPORTER_OTLP_HEADERS} environment variable, - * a list of key-value pairs separated by the "=" character. i.e.: key1=value1,key2=value2 - */ - Optional> headers(); - - /** - * Sets the method used to compress payloads. If unset, compression is disabled. Currently - * supported compression methods include `gzip` and `none`. - */ - Optional compression(); - - /** - * Sets the maximum time to wait for the collector to process an exported batch of spans. If - * unset, defaults to {@value OtlpExporterRuntimeConfig#DEFAULT_TIMEOUT_SECS}s. - */ - @WithDefault("10s") - Duration timeout(); - - /** - * OTLP defines the encoding of telemetry data and the protocol used to exchange data between the client and the - * server. Depending on the exporter, the available protocols will be different. - *

- * Currently, only {@code grpc} and {@code http/protobuf} are allowed. - */ - @WithDefault(Protocol.GRPC) - Optional protocol(); - - /** - * Key/cert configuration in the PEM format. - */ - @WithName("key-cert") - KeyCert keyCert(); - - /** - * Trust configuration in the PEM format. - */ - @WithName("trust-cert") - TrustCert trustCert(); - - /** - * The name of the TLS configuration to use. - *

- * If not set and the default TLS configuration is configured ({@code quarkus.tls.*}) then that will be used. - * If a name is configured, it uses the configuration from {@code quarkus.tls..*} - * If a name is configured, but no TLS configuration is found with that name then an error will be thrown. - */ - Optional tlsConfigurationName(); - - /** - * Set proxy options - */ - ProxyConfig proxyOptions(); - - interface KeyCert { - /** - * Comma-separated list of the path to the key files (Pem format). - */ - Optional> keys(); - - /** - * Comma-separated list of the path to the certificate files (Pem format). - */ - Optional> certs(); - } - - interface TrustCert { - /** - * Comma-separated list of the trust certificate files (Pem format). - */ - Optional> certs(); - } - - interface ProxyConfig { - - /** - * If proxy connection must be used. - */ - @WithDefault("false") - boolean enabled(); - - /** - * Set proxy username. - */ - Optional username(); - - /** - * Set proxy password. - */ - Optional password(); - - /** - * Set proxy port. - */ - @ConfigDocDefault("3128") - OptionalInt port(); - - /** - * Set proxy host. - */ - Optional host(); - } - - class Protocol { - public static final String GRPC = "grpc"; - public static final String HTTP_PROTOBUF = "http/protobuf"; - } } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterRecorder.java index 4ba8a52a6e6b9..607c5f03ab95e 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterRecorder.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterRecorder.java @@ -1,12 +1,15 @@ package io.quarkus.opentelemetry.runtime.exporter.otlp; +import static io.opentelemetry.sdk.metrics.Aggregation.explicitBucketHistogram; +import static io.quarkus.opentelemetry.runtime.config.build.ExporterType.Constants.OTLP_VALUE; +import static io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterConfig.Protocol.GRPC; +import static io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterConfig.Protocol.HTTP_PROTOBUF; import static io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterRuntimeConfig.DEFAULT_GRPC_BASE_URI; -import static io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterTracesConfig.Protocol.GRPC; -import static io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterTracesConfig.Protocol.HTTP_PROTOBUF; import java.net.URI; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.function.Consumer; @@ -18,17 +21,37 @@ import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.exporter.internal.ExporterBuilderUtil; +import io.opentelemetry.exporter.internal.grpc.GrpcExporter; import io.opentelemetry.exporter.internal.http.HttpExporter; +import io.opentelemetry.exporter.internal.otlp.metrics.MetricsRequestMarshaler; import io.opentelemetry.exporter.internal.otlp.traces.TraceRequestMarshaler; import io.opentelemetry.exporter.otlp.internal.OtlpUserAgent; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.internal.aggregator.AggregationUtil; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.quarkus.arc.SyntheticCreationalContext; import io.quarkus.opentelemetry.runtime.config.runtime.OTelRuntimeConfig; import io.quarkus.opentelemetry.runtime.config.runtime.exporter.CompressionType; +import io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterConfig; +import io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterMetricsConfig; import io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterRuntimeConfig; import io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterTracesConfig; +import io.quarkus.opentelemetry.runtime.exporter.otlp.metrics.NoopMetricExporter; +import io.quarkus.opentelemetry.runtime.exporter.otlp.metrics.VertxGrpcMetricExporter; +import io.quarkus.opentelemetry.runtime.exporter.otlp.metrics.VertxHttpMetricsExporter; +import io.quarkus.opentelemetry.runtime.exporter.otlp.sender.VertxGrpcSender; +import io.quarkus.opentelemetry.runtime.exporter.otlp.sender.VertxHttpSender; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.LateBoundBatchSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.RemoveableLateBoundBatchSpanProcessor; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.VertxGrpcSpanExporter; +import io.quarkus.opentelemetry.runtime.exporter.otlp.tracing.VertxHttpSpanExporter; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.tls.TlsConfiguration; import io.quarkus.tls.TlsConfigurationRegistry; @@ -42,9 +65,14 @@ @Recorder public class OTelExporterRecorder { + public static final String BASE2EXPONENTIAL_AGGREGATION_NAME = AggregationUtil + .aggregationName(Aggregation.base2ExponentialBucketHistogram()); + public static final String EXPLICIT_BUCKET_AGGREGATION_NAME = AggregationUtil.aggregationName(explicitBucketHistogram()); + public Function, LateBoundBatchSpanProcessor> batchSpanProcessorForOtlp( OTelRuntimeConfig otelRuntimeConfig, - OtlpExporterRuntimeConfig exporterRuntimeConfig, Supplier vertx) { + OtlpExporterRuntimeConfig exporterRuntimeConfig, + Supplier vertx) { URI baseUri = getBaseUri(exporterRuntimeConfig); // do the creation and validation here in order to preserve backward compatibility return new Function<>() { @Override @@ -107,17 +135,18 @@ private SpanExporter createOtlpGrpcSpanExporter(OtlpExporterRuntimeConfig export OtlpExporterTracesConfig tracesConfig = exporterRuntimeConfig.traces(); - return new VertxGrpcExporter( - "otlp", // use the same as OTel does + return new VertxGrpcSpanExporter(new GrpcExporter( + OTLP_VALUE, // use the same as OTel does "span", // use the same as OTel does - MeterProvider::noop, - baseUri, - determineCompression(tracesConfig), - tracesConfig.timeout(), - populateTracingExportHttpHeaders(tracesConfig), - new HttpClientOptionsConsumer(tracesConfig, baseUri, tlsConfigurationRegistry), - vertx); - + new VertxGrpcSender( + baseUri, + VertxGrpcSender.GRPC_TRACE_SERVICE_NAME, + determineCompression(tracesConfig), + tracesConfig.timeout(), + populateTracingExportHttpHeaders(tracesConfig), + new HttpClientOptionsConsumer(tracesConfig, baseUri, tlsConfigurationRegistry), + vertx), + MeterProvider::noop)); } private SpanExporter createHttpSpanExporter(OtlpExporterRuntimeConfig exporterRuntimeConfig, Vertx vertx, @@ -128,11 +157,12 @@ private SpanExporter createHttpSpanExporter(OtlpExporterRuntimeConfig exporterRu boolean exportAsJson = false; //TODO: this will be enhanced in the future - return new VertxHttpExporter(new HttpExporter( - "otlp", // use the same as OTel does + return new VertxHttpSpanExporter(new HttpExporter( + OTLP_VALUE, // use the same as OTel does "span", // use the same as OTel does - new VertxHttpExporter.VertxHttpSender( + new VertxHttpSender( baseUri, + VertxHttpSender.TRACES_PATH, determineCompression(tracesConfig), tracesConfig.timeout(), populateTracingExportHttpHeaders(tracesConfig), @@ -145,18 +175,127 @@ private SpanExporter createHttpSpanExporter(OtlpExporterRuntimeConfig exporterRu }; } - private static boolean determineCompression(OtlpExporterTracesConfig tracesConfig) { - if (tracesConfig.compression().isPresent()) { - return (tracesConfig.compression().get() == CompressionType.GZIP); + public Function, MetricExporter> createMetricExporter( + OTelRuntimeConfig otelRuntimeConfig, + OtlpExporterRuntimeConfig exporterRuntimeConfig, + Supplier vertx) { + + final URI baseUri = getBaseUri(exporterRuntimeConfig); + + return new Function<>() { + @Override + public MetricExporter apply(SyntheticCreationalContext context) { + + if (otelRuntimeConfig.sdkDisabled() || baseUri == null) { + return NoopMetricExporter.INSTANCE; + } + + MetricExporter metricExporter; + + try { + TlsConfigurationRegistry tlsConfigurationRegistry = context + .getInjectedReference(TlsConfigurationRegistry.class); + OtlpExporterMetricsConfig metricsConfig = exporterRuntimeConfig.metrics(); + if (metricsConfig.protocol().isEmpty()) { + throw new IllegalStateException("No OTLP protocol specified. " + + "Please check `quarkus.otel.exporter.otlp.traces.protocol` property"); + } + + String protocol = metricsConfig.protocol().get(); + if (GRPC.equals(protocol)) { + metricExporter = new VertxGrpcMetricExporter( + new GrpcExporter( + OTLP_VALUE, // use the same as OTel does + "metric", // use the same as OTel does + new VertxGrpcSender( + baseUri, + VertxGrpcSender.GRPC_METRIC_SERVICE_NAME, + determineCompression(metricsConfig), + metricsConfig.timeout(), + populateTracingExportHttpHeaders(metricsConfig), + new HttpClientOptionsConsumer(metricsConfig, baseUri, tlsConfigurationRegistry), + vertx.get()), + MeterProvider::noop), + aggregationTemporalityResolver(metricsConfig), + aggregationResolver(metricsConfig)); + } else if (HTTP_PROTOBUF.equals(protocol)) { + boolean exportAsJson = false; //TODO: this will be enhanced in the future + metricExporter = new VertxHttpMetricsExporter( + new HttpExporter( + OTLP_VALUE, // use the same as OTel does + "metric", // use the same as OTel does + new VertxHttpSender( + baseUri, + VertxHttpSender.METRICS_PATH, + determineCompression(metricsConfig), + metricsConfig.timeout(), + populateTracingExportHttpHeaders(metricsConfig), + exportAsJson ? "application/json" : "application/x-protobuf", + new HttpClientOptionsConsumer(metricsConfig, baseUri, tlsConfigurationRegistry), + vertx.get()), + MeterProvider::noop, + exportAsJson), + aggregationTemporalityResolver(metricsConfig), + aggregationResolver(metricsConfig)); + } else { + throw new IllegalArgumentException(String.format("Unsupported OTLP protocol %s specified. " + + "Please check `quarkus.otel.exporter.otlp.traces.protocol` property", protocol)); + } + + } catch (IllegalArgumentException iae) { + throw new IllegalStateException("Unable to install OTLP Exporter", iae); + } + return metricExporter; + } + }; + } + + private static DefaultAggregationSelector aggregationResolver(OtlpExporterMetricsConfig metricsConfig) { + String defaultHistogramAggregation = metricsConfig.defaultHistogramAggregation() + .map(s -> s.toLowerCase(Locale.ROOT)) + .orElse("explicit_bucket_histogram"); + + DefaultAggregationSelector aggregationSelector; + if (defaultHistogramAggregation.equals("explicit_bucket_histogram")) { + aggregationSelector = DefaultAggregationSelector.getDefault(); + } else if (BASE2EXPONENTIAL_AGGREGATION_NAME.equalsIgnoreCase(defaultHistogramAggregation)) { + + aggregationSelector = DefaultAggregationSelector + .getDefault() + .with(InstrumentType.HISTOGRAM, Aggregation.base2ExponentialBucketHistogram()); + + } else { + throw new ConfigurationException( + "Unrecognized default histogram aggregation: " + defaultHistogramAggregation); + } + return aggregationSelector; + } + + private static AggregationTemporalitySelector aggregationTemporalityResolver(OtlpExporterMetricsConfig metricsConfig) { + String temporalityValue = metricsConfig.temporalityPreference() + .map(s -> s.toLowerCase(Locale.ROOT)) + .orElse("cumulative"); + AggregationTemporalitySelector temporalitySelector = switch (temporalityValue) { + case "cumulative" -> AggregationTemporalitySelector.alwaysCumulative(); + case "delta" -> AggregationTemporalitySelector.deltaPreferred(); + case "lowmemory" -> AggregationTemporalitySelector.lowMemory(); + default -> throw new ConfigurationException("Unrecognized aggregation temporality: " + temporalityValue); + }; + return temporalitySelector; + } + + private static boolean determineCompression(OtlpExporterConfig config) { + if (config.compression().isPresent()) { + return (config.compression().get() == CompressionType.GZIP); } return false; } - private static Map populateTracingExportHttpHeaders(OtlpExporterTracesConfig tracesConfig) { + private static Map populateTracingExportHttpHeaders(OtlpExporterConfig config) { Map headersMap = new HashMap<>(); OtlpUserAgent.addUserAgentHeader(headersMap::put); - if (tracesConfig.headers().isPresent()) { - List headers = tracesConfig.headers().get(); + if (config.headers().isPresent()) { + List headers = config.headers().get(); if (!headers.isEmpty()) { for (String header : headers) { if (header.isEmpty()) { @@ -173,7 +312,7 @@ private static Map populateTracingExportHttpHeaders(OtlpExporter } private URI getBaseUri(OtlpExporterRuntimeConfig exporterRuntimeConfig) { - String endpoint = resolveEndpoint(exporterRuntimeConfig).trim(); + String endpoint = resolveEndpoint(exporterRuntimeConfig).trim(); // FIXME must be signal independent if (endpoint.isEmpty()) { return null; } @@ -196,23 +335,23 @@ private static boolean excludeDefaultEndpoint(String endpoint) { } static class HttpClientOptionsConsumer implements Consumer { - private final OtlpExporterTracesConfig tracesConfig; + private final OtlpExporterConfig config; private final URI baseUri; private final Optional maybeTlsConfiguration; private final TlsConfigurationRegistry tlsConfigurationRegistry; - public HttpClientOptionsConsumer(OtlpExporterTracesConfig tracesConfig, URI baseUri, + public HttpClientOptionsConsumer(OtlpExporterConfig config, URI baseUri, TlsConfigurationRegistry tlsConfigurationRegistry) { - this.tracesConfig = tracesConfig; + this.config = config; this.baseUri = baseUri; - this.maybeTlsConfiguration = TlsConfiguration.from(tlsConfigurationRegistry, tracesConfig.tlsConfigurationName()); + this.maybeTlsConfiguration = TlsConfiguration.from(tlsConfigurationRegistry, config.tlsConfigurationName()); this.tlsConfigurationRegistry = tlsConfigurationRegistry; } @Override public void accept(HttpClientOptions options) { configureTLS(options); - if (tracesConfig.proxyOptions().enabled()) { + if (config.proxyOptions().enabled()) { configureProxyOptions(options); } } @@ -240,7 +379,7 @@ public Boolean get() { } private void configureProxyOptions(HttpClientOptions options) { - var proxyConfig = tracesConfig.proxyOptions(); + var proxyConfig = config.proxyOptions(); Optional proxyHost = proxyConfig.host(); if (proxyHost.isPresent()) { ProxyOptions proxyOptions = new ProxyOptions() @@ -293,7 +432,7 @@ private void configureKeyCertOptions(HttpClientOptions options) { return; } - OtlpExporterTracesConfig.KeyCert keyCert = tracesConfig.keyCert(); + OtlpExporterTracesConfig.KeyCert keyCert = config.keyCert(); if (keyCert.certs().isEmpty() && keyCert.keys().isEmpty()) { return; } @@ -318,7 +457,7 @@ private void configureTrustOptions(HttpClientOptions options) { return; } - OtlpExporterTracesConfig.TrustCert trustCert = tracesConfig.trustCert(); + OtlpExporterTracesConfig.TrustCert trustCert = config.trustCert(); if (trustCert.certs().isPresent()) { List certs = trustCert.certs().get(); if (!certs.isEmpty()) { diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterUtil.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterUtil.java index 02aefe66744f9..7853c77875e9d 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterUtil.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OTelExporterUtil.java @@ -3,12 +3,12 @@ import java.net.URI; import java.util.Locale; -final class OTelExporterUtil { +public final class OTelExporterUtil { private OTelExporterUtil() { } - static int getPort(URI uri) { + public static int getPort(URI uri) { int originalPort = uri.getPort(); if (originalPort > -1) { return originalPort; @@ -20,7 +20,7 @@ static int getPort(URI uri) { return 80; } - static boolean isHttps(URI uri) { + public static boolean isHttps(URI uri) { return "https".equals(uri.getScheme().toLowerCase(Locale.ROOT)); } } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/VertxHttpExporter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/VertxHttpExporter.java deleted file mode 100644 index 23c0175621898..0000000000000 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/VertxHttpExporter.java +++ /dev/null @@ -1,305 +0,0 @@ -package io.quarkus.opentelemetry.runtime.exporter.otlp; - -import static io.quarkus.opentelemetry.runtime.exporter.otlp.OTelExporterUtil.getPort; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.URI; -import java.time.Duration; -import java.util.Collection; -import java.util.Map; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.zip.GZIPOutputStream; - -import io.opentelemetry.exporter.internal.http.HttpExporter; -import io.opentelemetry.exporter.internal.http.HttpSender; -import io.opentelemetry.exporter.internal.otlp.traces.TraceRequestMarshaler; -import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.internal.ThrottlingLogger; -import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.sdk.trace.export.SpanExporter; -import io.quarkus.vertx.core.runtime.BufferOutputStream; -import io.smallrye.mutiny.Uni; -import io.vertx.core.AsyncResult; -import io.vertx.core.Handler; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpClient; -import io.vertx.core.http.HttpClientOptions; -import io.vertx.core.http.HttpClientRequest; -import io.vertx.core.http.HttpClientResponse; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.tracing.TracingPolicy; - -final class VertxHttpExporter implements SpanExporter { - - private static final Logger internalLogger = Logger.getLogger(VertxHttpExporter.class.getName()); - private static final ThrottlingLogger logger = new ThrottlingLogger(internalLogger); - - private static final int MAX_ATTEMPTS = 3; - - private final HttpExporter delegate; - private final AtomicBoolean isShutdown = new AtomicBoolean(); - - VertxHttpExporter(HttpExporter delegate) { - this.delegate = delegate; - } - - @Override - public CompletableResultCode export(Collection spans) { - if (isShutdown.get()) { - return CompletableResultCode.ofFailure(); - } - - TraceRequestMarshaler exportRequest = TraceRequestMarshaler.create(spans); - return delegate.export(exportRequest, spans.size()); - } - - @Override - public CompletableResultCode flush() { - return CompletableResultCode.ofSuccess(); - } - - @Override - public CompletableResultCode shutdown() { - if (!isShutdown.compareAndSet(false, true)) { - logger.log(Level.FINE, "Calling shutdown() multiple times."); - return CompletableResultCode.ofSuccess(); - } - return delegate.shutdown(); - } - - static final class VertxHttpSender implements HttpSender { - - private static final String TRACES_PATH = "/v1/traces"; - private final String basePath; - private final boolean compressionEnabled; - private final Map headers; - private final String contentType; - private final HttpClient client; - - VertxHttpSender( - URI baseUri, - boolean compressionEnabled, - Duration timeout, - Map headersMap, - String contentType, - Consumer clientOptionsCustomizer, - Vertx vertx) { - this.basePath = determineBasePath(baseUri); - this.compressionEnabled = compressionEnabled; - this.headers = headersMap; - this.contentType = contentType; - var httpClientOptions = new HttpClientOptions() - .setReadIdleTimeout((int) timeout.getSeconds()) - .setDefaultHost(baseUri.getHost()) - .setDefaultPort(getPort(baseUri)) - .setTracingPolicy(TracingPolicy.IGNORE); // needed to avoid tracing the calls from this http client - clientOptionsCustomizer.accept(httpClientOptions); - this.client = vertx.createHttpClient(httpClientOptions); - } - - private final CompletableResultCode shutdownResult = new CompletableResultCode(); - - private static String determineBasePath(URI baseUri) { - String path = baseUri.getPath(); - if (path.isEmpty() || path.equals("/")) { - return ""; - } - if (path.endsWith("/")) { // strip ending slash - path = path.substring(0, path.length() - 1); - } - if (!path.startsWith("/")) { // prepend leading slash - path = "/" + path; - } - return path; - } - - @Override - public void send(Consumer marshaler, - int contentLength, - Consumer onHttpResponseRead, - Consumer onError) { - - String requestURI = basePath + TRACES_PATH; - var clientRequestSuccessHandler = new ClientRequestSuccessHandler(client, requestURI, headers, compressionEnabled, - contentType, - contentLength, onHttpResponseRead, - onError, marshaler, 1); - initiateSend(client, requestURI, MAX_ATTEMPTS, clientRequestSuccessHandler, onError); - } - - private static void initiateSend(HttpClient client, String requestURI, - int numberOfAttempts, - Handler clientRequestSuccessHandler, - Consumer onError) { - Uni.createFrom().completionStage(new Supplier>() { - @Override - public CompletionStage get() { - return client.request(HttpMethod.POST, requestURI).toCompletionStage(); - } - }).onFailure().retry() - .withBackOff(Duration.ofMillis(100)) - .atMost(numberOfAttempts) - .subscribe().with(new Consumer<>() { - @Override - public void accept(HttpClientRequest request) { - clientRequestSuccessHandler.handle(request); - } - }, onError); - } - - @Override - public CompletableResultCode shutdown() { - client.close() - .onSuccess( - new Handler<>() { - @Override - public void handle(Void event) { - shutdownResult.succeed(); - } - }) - .onFailure(new Handler<>() { - @Override - public void handle(Throwable event) { - shutdownResult.fail(); - } - }); - return shutdownResult; - } - - private static class ClientRequestSuccessHandler implements Handler { - private final HttpClient client; - private final String requestURI; - private final Map headers; - private final boolean compressionEnabled; - private final String contentType; - private final int contentLength; - private final Consumer onHttpResponseRead; - private final Consumer onError; - private final Consumer marshaler; - - private final int attemptNumber; - - public ClientRequestSuccessHandler(HttpClient client, - String requestURI, Map headers, - boolean compressionEnabled, - String contentType, - int contentLength, - Consumer onHttpResponseRead, - Consumer onError, - Consumer marshaler, - int attemptNumber) { - this.client = client; - this.requestURI = requestURI; - this.headers = headers; - this.compressionEnabled = compressionEnabled; - this.contentType = contentType; - this.contentLength = contentLength; - this.onHttpResponseRead = onHttpResponseRead; - this.onError = onError; - this.marshaler = marshaler; - this.attemptNumber = attemptNumber; - } - - @Override - public void handle(HttpClientRequest request) { - - HttpClientRequest clientRequest = request.response(new Handler<>() { - @Override - public void handle(AsyncResult callResult) { - if (callResult.succeeded()) { - HttpClientResponse clientResponse = callResult.result(); - clientResponse.body(new Handler<>() { - @Override - public void handle(AsyncResult bodyResult) { - if (bodyResult.succeeded()) { - if (clientResponse.statusCode() >= 500) { - if (attemptNumber <= MAX_ATTEMPTS) { - // we should retry for 5xx error as they might be recoverable - initiateSend(client, requestURI, - MAX_ATTEMPTS - attemptNumber, - newAttempt(), - onError); - return; - } - } - onHttpResponseRead.accept(new Response() { - @Override - public int statusCode() { - return clientResponse.statusCode(); - } - - @Override - public String statusMessage() { - return clientResponse.statusMessage(); - } - - @Override - public byte[] responseBody() { - return bodyResult.result().getBytes(); - } - }); - } else { - if (attemptNumber <= MAX_ATTEMPTS) { - // retry - initiateSend(client, requestURI, - MAX_ATTEMPTS - attemptNumber, - newAttempt(), - onError); - } else { - onError.accept(bodyResult.cause()); - } - } - } - }); - } else { - if (attemptNumber <= MAX_ATTEMPTS) { - // retry - initiateSend(client, requestURI, - MAX_ATTEMPTS - attemptNumber, - newAttempt(), - onError); - } else { - onError.accept(callResult.cause()); - } - } - } - }) - .putHeader("Content-Type", contentType); - - Buffer buffer = Buffer.buffer(contentLength); - OutputStream os = new BufferOutputStream(buffer); - if (compressionEnabled) { - clientRequest.putHeader("Content-Encoding", "gzip"); - try (var gzos = new GZIPOutputStream(os)) { - marshaler.accept(gzos); - } catch (IOException e) { - throw new IllegalStateException(e); - } - } else { - marshaler.accept(os); - } - - if (!headers.isEmpty()) { - for (var entry : headers.entrySet()) { - clientRequest.putHeader(entry.getKey(), entry.getValue()); - } - } - - clientRequest.send(buffer); - } - - public ClientRequestSuccessHandler newAttempt() { - return new ClientRequestSuccessHandler(client, requestURI, headers, compressionEnabled, - contentType, contentLength, onHttpResponseRead, - onError, marshaler, attemptNumber + 1); - } - } - } -} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/graal/Target_io_opentelemetry_exporter_otlp_internal_OtlpMetricExporterProvider.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/graal/Target_io_opentelemetry_exporter_otlp_internal_OtlpMetricExporterProvider.java new file mode 100644 index 0000000000000..d63ca09dad004 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/graal/Target_io_opentelemetry_exporter_otlp_internal_OtlpMetricExporterProvider.java @@ -0,0 +1,9 @@ +package io.quarkus.opentelemetry.runtime.exporter.otlp.graal; + +import com.oracle.svm.core.annotate.Delete; +import com.oracle.svm.core.annotate.TargetClass; + +@TargetClass(className = "io.opentelemetry.exporter.otlp.internal.OtlpMetricExporterProvider") +@Delete +public final class Target_io_opentelemetry_exporter_otlp_internal_OtlpMetricExporterProvider { +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/metrics/NoopMetricExporter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/metrics/NoopMetricExporter.java new file mode 100644 index 0000000000000..a02cfd815c7fb --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/metrics/NoopMetricExporter.java @@ -0,0 +1,37 @@ +package io.quarkus.opentelemetry.runtime.exporter.otlp.metrics; + +import java.util.Collection; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; + +public class NoopMetricExporter implements MetricExporter { + + public static final NoopMetricExporter INSTANCE = new NoopMetricExporter(); + + private NoopMetricExporter() { + } + + @Override + public CompletableResultCode export(Collection metrics) { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return AggregationTemporality.CUMULATIVE; + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/metrics/VertxGrpcMetricExporter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/metrics/VertxGrpcMetricExporter.java new file mode 100644 index 0000000000000..61eeb1d1390ee --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/metrics/VertxGrpcMetricExporter.java @@ -0,0 +1,60 @@ +package io.quarkus.opentelemetry.runtime.exporter.otlp.metrics; + +import java.util.Collection; + +import io.opentelemetry.exporter.internal.grpc.GrpcExporter; +import io.opentelemetry.exporter.internal.otlp.metrics.MetricsRequestMarshaler; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.export.MemoryMode; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; +import io.opentelemetry.sdk.metrics.export.MetricExporter; + +public class VertxGrpcMetricExporter implements MetricExporter { + + private final GrpcExporter delegate; + private final AggregationTemporalitySelector aggregationTemporalitySelector; + private final DefaultAggregationSelector defaultAggregationSelector; + + public VertxGrpcMetricExporter(GrpcExporter grpcExporter, + AggregationTemporalitySelector aggregationTemporalitySelector, + DefaultAggregationSelector defaultAggregationSelector) { + this.delegate = grpcExporter; + this.aggregationTemporalitySelector = aggregationTemporalitySelector; + this.defaultAggregationSelector = defaultAggregationSelector; + } + + @Override + public CompletableResultCode export(Collection metrics) { + return delegate.export(MetricsRequestMarshaler.create(metrics), metrics.size()); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return delegate.shutdown(); + } + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return this.aggregationTemporalitySelector.getAggregationTemporality(instrumentType); + } + + @Override + public Aggregation getDefaultAggregation(InstrumentType instrumentType) { + return defaultAggregationSelector.getDefaultAggregation(instrumentType); + } + + @Override + public MemoryMode getMemoryMode() { + return MemoryMode.IMMUTABLE_DATA; // Same as the default in the OTLP exporter + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/metrics/VertxHttpMetricsExporter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/metrics/VertxHttpMetricsExporter.java new file mode 100644 index 0000000000000..bf4afaede728b --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/metrics/VertxHttpMetricsExporter.java @@ -0,0 +1,60 @@ +package io.quarkus.opentelemetry.runtime.exporter.otlp.metrics; + +import java.util.Collection; + +import io.opentelemetry.exporter.internal.http.HttpExporter; +import io.opentelemetry.exporter.internal.otlp.metrics.MetricsRequestMarshaler; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.export.MemoryMode; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; +import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; +import io.opentelemetry.sdk.metrics.export.MetricExporter; + +public class VertxHttpMetricsExporter implements MetricExporter { + + private final HttpExporter delegate; + private final AggregationTemporalitySelector aggregationTemporalitySelector; + private final DefaultAggregationSelector defaultAggregationSelector; + + public VertxHttpMetricsExporter(HttpExporter delegate, + AggregationTemporalitySelector aggregationTemporalitySelector, + DefaultAggregationSelector defaultAggregationSelector) { + this.delegate = delegate; + this.aggregationTemporalitySelector = aggregationTemporalitySelector; + this.defaultAggregationSelector = defaultAggregationSelector; + } + + @Override + public CompletableResultCode export(Collection metrics) { + return delegate.export(MetricsRequestMarshaler.create(metrics), metrics.size()); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return delegate.shutdown(); + } + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return this.aggregationTemporalitySelector.getAggregationTemporality(instrumentType); + } + + @Override + public Aggregation getDefaultAggregation(InstrumentType instrumentType) { + return defaultAggregationSelector.getDefaultAggregation(instrumentType); + } + + @Override + public MemoryMode getMemoryMode() { + return MemoryMode.IMMUTABLE_DATA; // Same as the default in the OTLP exporter + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/VertxGrpcExporter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/sender/VertxGrpcSender.java similarity index 74% rename from extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/VertxGrpcExporter.java rename to extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/sender/VertxGrpcSender.java index cb6e208632afd..d7c856243609a 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/VertxGrpcExporter.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/sender/VertxGrpcSender.java @@ -1,26 +1,27 @@ -package io.quarkus.opentelemetry.runtime.exporter.otlp; +package io.quarkus.opentelemetry.runtime.exporter.otlp.sender; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.util.Collection; import java.util.Map; import java.util.concurrent.CompletionStage; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import io.netty.handler.codec.http.QueryStringDecoder; -import io.opentelemetry.api.metrics.MeterProvider; -import io.opentelemetry.exporter.internal.ExporterMetrics; -import io.opentelemetry.exporter.internal.otlp.traces.TraceRequestMarshaler; +import io.opentelemetry.exporter.internal.grpc.GrpcResponse; +import io.opentelemetry.exporter.internal.grpc.GrpcSender; +import io.opentelemetry.exporter.internal.marshal.Marshaler; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.internal.ThrottlingLogger; -import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.sdk.trace.export.SpanExporter; +import io.quarkus.opentelemetry.runtime.exporter.otlp.OTelExporterUtil; import io.quarkus.vertx.core.runtime.BufferOutputStream; import io.smallrye.mutiny.Uni; import io.vertx.core.Handler; @@ -36,15 +37,16 @@ import io.vertx.grpc.common.GrpcStatus; import io.vertx.grpc.common.ServiceName; -final class VertxGrpcExporter implements SpanExporter { +public final class VertxGrpcSender implements GrpcSender { - private static final String GRPC_SERVICE_NAME = "opentelemetry.proto.collector.trace.v1.TraceService"; + public static final String GRPC_TRACE_SERVICE_NAME = "opentelemetry.proto.collector.trace.v1.TraceService"; + public static final String GRPC_METRIC_SERVICE_NAME = "opentelemetry.proto.collector.metrics.v1.MetricsService"; private static final String GRPC_METHOD_NAME = "Export"; private static final String GRPC_STATUS = "grpc-status"; private static final String GRPC_MESSAGE = "grpc-message"; - private static final Logger internalLogger = Logger.getLogger(VertxGrpcExporter.class.getName()); + private static final Logger internalLogger = Logger.getLogger(VertxGrpcSender.class.getName()); private static final int MAX_ATTEMPTS = 3; private final ThrottlingLogger logger = new ThrottlingLogger(internalLogger); // TODO: is there something in JBoss Logging we can use? @@ -53,25 +55,22 @@ final class VertxGrpcExporter implements SpanExporter { private final AtomicBoolean loggedUnimplemented = new AtomicBoolean(); private final AtomicBoolean isShutdown = new AtomicBoolean(); private final CompletableResultCode shutdownResult = new CompletableResultCode(); - private final String type; - private final ExporterMetrics exporterMetrics; private final SocketAddress server; private final boolean compressionEnabled; private final Map headers; + private final String grpcEndpointPath; private final GrpcClient client; - VertxGrpcExporter( - String exporterName, - String type, - Supplier meterProviderSupplier, - URI grpcBaseUri, boolean compressionEnabled, + public VertxGrpcSender( + URI grpcBaseUri, + String grpcEndpointPath, + boolean compressionEnabled, Duration timeout, Map headersMap, Consumer clientOptionsCustomizer, Vertx vertx) { - this.type = type; - this.exporterMetrics = ExporterMetrics.createGrpcOkHttp(exporterName, type, meterProviderSupplier); + this.grpcEndpointPath = grpcEndpointPath; this.server = SocketAddress.inetSocketAddress(OTelExporterUtil.getPort(grpcBaseUri), grpcBaseUri.getHost()); this.compressionEnabled = compressionEnabled; this.headers = headersMap; @@ -83,74 +82,24 @@ final class VertxGrpcExporter implements SpanExporter { this.client = GrpcClient.client(vertx, httpClientOptions); } - private CompletableResultCode export(TraceRequestMarshaler marshaler, int numItems) { + @Override + public void send(Marshaler request, Runnable onSuccess, BiConsumer onError) { if (isShutdown.get()) { - return CompletableResultCode.ofFailure(); + return; } - exporterMetrics.addSeen(numItems); - - var result = new CompletableResultCode(); - var onSuccessHandler = new ClientRequestOnSuccessHandler(client, server, headers, compressionEnabled, exporterMetrics, - marshaler, - loggedUnimplemented, logger, type, numItems, result, 1); + final String marshalerType = request.getClass().getSimpleName(); + var onSuccessHandler = new ClientRequestOnSuccessHandler(client, server, headers, compressionEnabled, + request, + loggedUnimplemented, logger, marshalerType, onSuccess, onError, 1, grpcEndpointPath, + isShutdown::get); initiateSend(client, server, MAX_ATTEMPTS, onSuccessHandler, new Consumer<>() { @Override public void accept(Throwable throwable) { - failOnClientRequest(numItems, throwable, result); + failOnClientRequest(marshalerType, throwable, onError); } }); - - return result; - } - - private static void initiateSend(GrpcClient client, SocketAddress server, - int numberOfAttempts, - Handler> onSuccessHandler, - Consumer onFailureCallback) { - Uni.createFrom().completionStage(new Supplier>>() { - - @Override - public CompletionStage> get() { - return client.request(server).toCompletionStage(); - } - }).onFailure().retry() - .withBackOff(Duration.ofMillis(100)) - .atMost(numberOfAttempts).subscribe().with( - new Consumer<>() { - @Override - public void accept(GrpcClientRequest request) { - onSuccessHandler.handle(request); - } - }, onFailureCallback); - } - - private void failOnClientRequest(int numItems, Throwable t, CompletableResultCode result) { - exporterMetrics.addFailed(numItems); - logger.log( - Level.SEVERE, - "Failed to export " - + type - + "s. The request could not be executed. Full error message: " - + t.getMessage()); - result.fail(); - } - - @Override - public CompletableResultCode export(Collection spans) { - if (isShutdown.get()) { - return CompletableResultCode.ofFailure(); - } - - TraceRequestMarshaler request = TraceRequestMarshaler.create(spans); - - return export(request, spans.size()); - } - - @Override - public CompletableResultCode flush() { - return CompletableResultCode.ofSuccess(); } @Override @@ -177,47 +126,96 @@ public void handle(Throwable event) { return shutdownResult; } + private static void initiateSend(GrpcClient client, SocketAddress server, + int numberOfAttempts, + Handler> onSuccessHandler, + Consumer onFailureCallback) { + Uni.createFrom().completionStage(new Supplier>>() { + @Override + public CompletionStage> get() { + return client.request(server).toCompletionStage(); + } + }) + .onFailure(new Predicate() { + @Override + public boolean test(Throwable t) { + // Will not retry on shutdown + return t instanceof IllegalStateException || + t instanceof RejectedExecutionException; + } + }) + .recoverWithUni(new Supplier>>() { + @Override + public Uni> get() { + return Uni.createFrom().nothing(); + } + }) + .onFailure() + .retry() + .withBackOff(Duration.ofMillis(100)) + .atMost(numberOfAttempts) + .subscribe().with( + new Consumer<>() { + @Override + public void accept(GrpcClientRequest request) { + onSuccessHandler.handle(request); + } + }, onFailureCallback); + } + + private void failOnClientRequest(String type, Throwable t, BiConsumer onError) { + String message = "Failed to export " + + type + + "s. The request could not be executed. Full error message: " + + (t.getMessage() == null ? t.getClass().getName() : t.getMessage()); + logger.log(Level.WARNING, message); + onError.accept(GrpcResponse.create(2 /* UNKNOWN */, message), t); + } + private static final class ClientRequestOnSuccessHandler implements Handler> { private final GrpcClient client; private final SocketAddress server; private final Map headers; private final boolean compressionEnabled; - private final ExporterMetrics exporterMetrics; - private final TraceRequestMarshaler marshaler; + private final Marshaler marshaler; private final AtomicBoolean loggedUnimplemented; private final ThrottlingLogger logger; private final String type; - private final int numItems; - private final CompletableResultCode result; + private final Runnable onSuccess; + private final BiConsumer onError; + private final String grpcEndpointPath; private final int attemptNumber; + private final Supplier isShutdown; public ClientRequestOnSuccessHandler(GrpcClient client, SocketAddress server, Map headers, boolean compressionEnabled, - ExporterMetrics exporterMetrics, - TraceRequestMarshaler marshaler, + Marshaler marshaler, AtomicBoolean loggedUnimplemented, ThrottlingLogger logger, String type, - int numItems, - CompletableResultCode result, - int attemptNumber) { + Runnable onSuccess, + BiConsumer onError, + int attemptNumber, + String grpcEndpointPath, + Supplier isShutdown) { this.client = client; this.server = server; + this.grpcEndpointPath = grpcEndpointPath; this.headers = headers; this.compressionEnabled = compressionEnabled; - this.exporterMetrics = exporterMetrics; this.marshaler = marshaler; this.loggedUnimplemented = loggedUnimplemented; this.logger = logger; this.type = type; - this.numItems = numItems; - this.result = result; + this.onSuccess = onSuccess; + this.onError = onError; this.attemptNumber = attemptNumber; + this.isShutdown = isShutdown; } @Override @@ -227,7 +225,7 @@ public void handle(GrpcClientRequest request) { } // Set the service name and the method to call - request.serviceName(ServiceName.create(GRPC_SERVICE_NAME)); + request.serviceName(ServiceName.create(grpcEndpointPath)); request.methodName(GRPC_METHOD_NAME); if (!headers.isEmpty()) { @@ -248,7 +246,7 @@ public void handle(GrpcClientResponse response) { response.exceptionHandler(new Handler<>() { @Override public void handle(Throwable t) { - if (attemptNumber <= MAX_ATTEMPTS) { + if (attemptNumber <= MAX_ATTEMPTS && !isShutdown.get()) { // retry initiateSend(client, server, MAX_ATTEMPTS - attemptNumber, @@ -256,19 +254,12 @@ public void handle(Throwable t) { new Consumer<>() { @Override public void accept(Throwable throwable) { - failOnClientRequest(numItems, throwable, result); + failOnClientRequest(throwable, onError, attemptNumber); } }); } else { - exporterMetrics.addFailed(numItems); - logger.log( - Level.SEVERE, - "Failed to export " - + type - + "s. The stream failed. Full error message: " - + t.getMessage()); - result.fail(); + failOnClientRequest(t, onError, attemptNumber); } } }).errorHandler(new Handler<>() { @@ -281,8 +272,7 @@ public void handle(GrpcError error) { public void handle(Void ignored) { GrpcStatus status = getStatus(response); if (status == GrpcStatus.OK) { - exporterMetrics.addSuccess(numItems); - result.succeed(); + onSuccess.run(); } else { handleError(status, response); } @@ -293,8 +283,9 @@ public void handle(Void ignored) { private void handleError(GrpcStatus status, GrpcClientResponse response) { String statusMessage = getStatusMessage(response); logAppropriateWarning(status, statusMessage); - exporterMetrics.addFailed(numItems); - result.fail(); + onError.accept( + GrpcResponse.create(2 /* UNKNOWN */, statusMessage), + new IllegalStateException(statusMessage)); } private void logAppropriateWarning(GrpcStatus status, @@ -305,7 +296,7 @@ private void logAppropriateWarning(GrpcStatus status, } } else if (status == GrpcStatus.UNAVAILABLE) { logger.log( - Level.SEVERE, + Level.WARNING, "Failed to export " + type + "s. Server is UNAVAILABLE. " @@ -359,7 +350,7 @@ private void logUnimplemented(Logger logger, String type, String fullErrorMessag } logger.log( - Level.SEVERE, + Level.WARNING, "Failed to export " + type + "s. Server responded with UNIMPLEMENTED. " @@ -401,7 +392,7 @@ private String getStatusMessage(GrpcClientResponse response) { }).onFailure(new Handler<>() { @Override public void handle(Throwable t) { - if (attemptNumber <= MAX_ATTEMPTS) { + if (attemptNumber <= MAX_ATTEMPTS && !isShutdown.get()) { // retry initiateSend(client, server, MAX_ATTEMPTS - attemptNumber, @@ -409,47 +400,38 @@ public void handle(Throwable t) { new Consumer<>() { @Override public void accept(Throwable throwable) { - failOnClientRequest(numItems, throwable, result); + failOnClientRequest(throwable, onError, attemptNumber); } }); } else { - exporterMetrics.addFailed(numItems); - logger.log( - Level.SEVERE, - "Failed to export " - + type - + "s. The request could not be executed. Full error message: " - + t.getMessage()); - result.fail(); + failOnClientRequest(t, onError, attemptNumber); } } }); } catch (IOException e) { - exporterMetrics.addFailed(numItems); - logger.log( - Level.SEVERE, - "Failed to export " - + type - + "s. Unable to serialize payload. Full error message: " - + e.getMessage()); - result.fail(); + final String message = "Failed to export " + + type + + "s. Unable to serialize payload. Full error message: " + + (e.getMessage() == null ? e.getClass().getName() : e.getMessage()); + logger.log(Level.WARNING, message); + onError.accept(GrpcResponse.create(2 /* UNKNOWN */, message), e); } } - private void failOnClientRequest(int numItems, Throwable t, CompletableResultCode result) { - exporterMetrics.addFailed(numItems); - logger.log( - Level.SEVERE, - "Failed to export " - + type - + "s. The request could not be executed. Full error message: " - + t.getMessage()); - result.fail(); + private void failOnClientRequest(Throwable t, BiConsumer onError, int attemptNumber) { + final String message = "Failed to export " + + type + + "s. The request could not be executed after " + attemptNumber + + " attempts. Full error message: " + + (t != null ? t.getMessage() : ""); + logger.log(Level.WARNING, message); + onError.accept(GrpcResponse.create(2 /* UNKNOWN */, message), t); } public ClientRequestOnSuccessHandler newAttempt() { - return new ClientRequestOnSuccessHandler(client, server, headers, compressionEnabled, exporterMetrics, marshaler, - loggedUnimplemented, logger, type, numItems, result, attemptNumber + 1); + return new ClientRequestOnSuccessHandler(client, server, headers, compressionEnabled, marshaler, + loggedUnimplemented, logger, type, onSuccess, onError, attemptNumber + 1, + grpcEndpointPath, isShutdown); } } } diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/sender/VertxHttpSender.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/sender/VertxHttpSender.java new file mode 100644 index 0000000000000..e3178c18f856e --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/sender/VertxHttpSender.java @@ -0,0 +1,307 @@ +package io.quarkus.opentelemetry.runtime.exporter.otlp.sender; + +import static io.quarkus.opentelemetry.runtime.exporter.otlp.OTelExporterUtil.getPort; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.GZIPOutputStream; + +import io.opentelemetry.exporter.internal.http.HttpSender; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.ThrottlingLogger; +import io.quarkus.vertx.core.runtime.BufferOutputStream; +import io.smallrye.mutiny.Uni; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClient; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.tracing.TracingPolicy; + +public final class VertxHttpSender implements HttpSender { + + public static final String TRACES_PATH = "/v1/traces"; + public static final String METRICS_PATH = "/v1/metrics"; + + private static final Logger internalLogger = Logger.getLogger(VertxHttpSender.class.getName()); + private static final ThrottlingLogger logger = new ThrottlingLogger(internalLogger); + + private static final int MAX_ATTEMPTS = 3; + + private final String basePath; + private final boolean compressionEnabled; + private final Map headers; + private final String contentType; + private final HttpClient client; + private final String signalPath; + + public VertxHttpSender( + URI baseUri, + String signalPath, + boolean compressionEnabled, + Duration timeout, + Map headersMap, + String contentType, + Consumer clientOptionsCustomizer, + Vertx vertx) { + this.basePath = determineBasePath(baseUri); + this.signalPath = signalPath; + this.compressionEnabled = compressionEnabled; + this.headers = headersMap; + this.contentType = contentType; + var httpClientOptions = new HttpClientOptions() + .setReadIdleTimeout((int) timeout.getSeconds()) + .setDefaultHost(baseUri.getHost()) + .setDefaultPort(getPort(baseUri)) + .setTracingPolicy(TracingPolicy.IGNORE); // needed to avoid tracing the calls from this http client + clientOptionsCustomizer.accept(httpClientOptions); + this.client = vertx.createHttpClient(httpClientOptions); + } + + private final AtomicBoolean isShutdown = new AtomicBoolean(); + private final CompletableResultCode shutdownResult = new CompletableResultCode(); + + private static String determineBasePath(URI baseUri) { + String path = baseUri.getPath(); + if (path.isEmpty() || path.equals("/")) { + return ""; + } + if (path.endsWith("/")) { // strip ending slash + path = path.substring(0, path.length() - 1); + } + if (!path.startsWith("/")) { // prepend leading slash + path = "/" + path; + } + return path; + } + + @Override + public void send(Consumer marshaler, + int contentLength, + Consumer onHttpResponseRead, + Consumer onError) { + if (isShutdown.get()) { + return; + } + + String requestURI = basePath + signalPath; + var clientRequestSuccessHandler = new ClientRequestSuccessHandler(client, requestURI, headers, compressionEnabled, + contentType, + contentLength, onHttpResponseRead, + onError, marshaler, 1, isShutdown::get); + initiateSend(client, requestURI, MAX_ATTEMPTS, clientRequestSuccessHandler, onError, isShutdown::get); + } + + private static void initiateSend(HttpClient client, String requestURI, + int numberOfAttempts, + Handler clientRequestSuccessHandler, + Consumer onError, + Supplier isShutdown) { + Uni.createFrom().completionStage(new Supplier>() { + @Override + public CompletionStage get() { + return client.request(HttpMethod.POST, requestURI).toCompletionStage(); + } + }) + .onFailure(new Predicate() { + @Override + public boolean test(Throwable t) { + // Will not retry on shutdown + return t instanceof IllegalStateException || + t instanceof RejectedExecutionException; + } + }) + .recoverWithUni(new Supplier>() { + @Override + public Uni get() { + return Uni.createFrom().nothing(); + } + }) + .onFailure() + .retry() + .withBackOff(Duration.ofMillis(100)) + .atMost(numberOfAttempts) + .subscribe().with( + new Consumer<>() { + @Override + public void accept(HttpClientRequest request) { + clientRequestSuccessHandler.handle(request); + } + }, onError); + } + + @Override + public CompletableResultCode shutdown() { + if (!isShutdown.compareAndSet(false, true)) { + logger.log(Level.FINE, "Calling shutdown() multiple times."); + return shutdownResult; + } + + client.close() + .onSuccess( + new Handler<>() { + @Override + public void handle(Void event) { + shutdownResult.succeed(); + } + }) + .onFailure(new Handler<>() { + @Override + public void handle(Throwable event) { + shutdownResult.fail(); + } + }); + return shutdownResult; + } + + private static class ClientRequestSuccessHandler implements Handler { + private final HttpClient client; + private final String requestURI; + private final Map headers; + private final boolean compressionEnabled; + private final String contentType; + private final int contentLength; + private final Consumer onHttpResponseRead; + private final Consumer onError; + private final Consumer marshaler; + + private final int attemptNumber; + private final Supplier isShutdown; + + public ClientRequestSuccessHandler(HttpClient client, + String requestURI, Map headers, + boolean compressionEnabled, + String contentType, + int contentLength, + Consumer onHttpResponseRead, + Consumer onError, + Consumer marshaler, + int attemptNumber, + Supplier isShutdown) { + this.client = client; + this.requestURI = requestURI; + this.headers = headers; + this.compressionEnabled = compressionEnabled; + this.contentType = contentType; + this.contentLength = contentLength; + this.onHttpResponseRead = onHttpResponseRead; + this.onError = onError; + this.marshaler = marshaler; + this.attemptNumber = attemptNumber; + this.isShutdown = isShutdown; + } + + @Override + public void handle(HttpClientRequest request) { + + HttpClientRequest clientRequest = request.response(new Handler<>() { + @Override + public void handle(AsyncResult callResult) { + if (callResult.succeeded()) { + HttpClientResponse clientResponse = callResult.result(); + Throwable cause = callResult.cause(); + clientResponse.body(new Handler<>() { + @Override + public void handle(AsyncResult bodyResult) { + if (bodyResult.succeeded()) { + if (clientResponse.statusCode() >= 500) { + if (attemptNumber <= MAX_ATTEMPTS && !isShutdown.get()) { + // we should retry for 5xx error as they might be recoverable + initiateSend(client, requestURI, + MAX_ATTEMPTS - attemptNumber, + newAttempt(), + onError, + isShutdown); + return; + } + } + onHttpResponseRead.accept(new Response() { + @Override + public int statusCode() { + return clientResponse.statusCode(); + } + + @Override + public String statusMessage() { + return clientResponse.statusMessage(); + } + + @Override + public byte[] responseBody() { + return bodyResult.result().getBytes(); + } + }); + } else { + if (attemptNumber <= MAX_ATTEMPTS && !isShutdown.get()) { + // retry + initiateSend(client, requestURI, + MAX_ATTEMPTS - attemptNumber, + newAttempt(), + onError, + isShutdown); + } else { + onError.accept(bodyResult.cause()); + } + } + } + }); + } else { + if (attemptNumber <= MAX_ATTEMPTS && !isShutdown.get()) { + // retry + initiateSend(client, requestURI, + MAX_ATTEMPTS - attemptNumber, + newAttempt(), + onError, + isShutdown); + } else { + onError.accept(callResult.cause()); + } + } + } + }) + .putHeader("Content-Type", contentType); + + Buffer buffer = Buffer.buffer(contentLength); + OutputStream os = new BufferOutputStream(buffer); + if (compressionEnabled) { + clientRequest.putHeader("Content-Encoding", "gzip"); + try (var gzos = new GZIPOutputStream(os)) { + marshaler.accept(gzos); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } else { + marshaler.accept(os); + } + + if (!headers.isEmpty()) { + for (var entry : headers.entrySet()) { + clientRequest.putHeader(entry.getKey(), entry.getValue()); + } + } + + clientRequest.send(buffer); + } + + public ClientRequestSuccessHandler newAttempt() { + return new ClientRequestSuccessHandler(client, requestURI, headers, compressionEnabled, + contentType, contentLength, onHttpResponseRead, + onError, marshaler, attemptNumber + 1, isShutdown); + } + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/LateBoundBatchSpanProcessor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundBatchSpanProcessor.java similarity index 97% rename from extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/LateBoundBatchSpanProcessor.java rename to extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundBatchSpanProcessor.java index dde43e7c9dcc0..90318940e3497 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/LateBoundBatchSpanProcessor.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/LateBoundBatchSpanProcessor.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.runtime.exporter.otlp; +package io.quarkus.opentelemetry.runtime.exporter.otlp.tracing; import org.jboss.logging.Logger; diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/RemoveableLateBoundBatchSpanProcessor.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundBatchSpanProcessor.java similarity index 90% rename from extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/RemoveableLateBoundBatchSpanProcessor.java rename to extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundBatchSpanProcessor.java index da44a0084b5b4..d8654e5ff634e 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/RemoveableLateBoundBatchSpanProcessor.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/RemoveableLateBoundBatchSpanProcessor.java @@ -1,4 +1,4 @@ -package io.quarkus.opentelemetry.runtime.exporter.otlp; +package io.quarkus.opentelemetry.runtime.exporter.otlp.tracing; import io.quarkus.opentelemetry.runtime.AutoConfiguredOpenTelemetrySdkBuilderCustomizer.TracerProviderCustomizer; diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/VertxGrpcSpanExporter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/VertxGrpcSpanExporter.java new file mode 100644 index 0000000000000..58e03f24a4e45 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/VertxGrpcSpanExporter.java @@ -0,0 +1,34 @@ +package io.quarkus.opentelemetry.runtime.exporter.otlp.tracing; + +import java.util.Collection; + +import io.opentelemetry.exporter.internal.grpc.GrpcExporter; +import io.opentelemetry.exporter.internal.otlp.traces.TraceRequestMarshaler; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; + +public final class VertxGrpcSpanExporter implements SpanExporter { + + private final GrpcExporter delegate; + + public VertxGrpcSpanExporter(GrpcExporter delegate) { + this.delegate = delegate; + } + + @Override + public CompletableResultCode export(Collection spans) { + TraceRequestMarshaler exportRequest = TraceRequestMarshaler.create(spans); + return delegate.export(exportRequest, spans.size()); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return delegate.shutdown(); + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/VertxHttpSpanExporter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/VertxHttpSpanExporter.java new file mode 100644 index 0000000000000..dedba95064e45 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/tracing/VertxHttpSpanExporter.java @@ -0,0 +1,34 @@ +package io.quarkus.opentelemetry.runtime.exporter.otlp.tracing; + +import java.util.Collection; + +import io.opentelemetry.exporter.internal.http.HttpExporter; +import io.opentelemetry.exporter.internal.otlp.traces.TraceRequestMarshaler; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; + +public final class VertxHttpSpanExporter implements SpanExporter { + + private final HttpExporter delegate; + + public VertxHttpSpanExporter(HttpExporter delegate) { + this.delegate = delegate; + } + + @Override + public CompletableResultCode export(Collection spans) { + TraceRequestMarshaler exportRequest = TraceRequestMarshaler.create(spans); + return delegate.export(exportRequest, spans.size()); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return delegate.shutdown(); + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/graal/Substitutions.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/graal/Substitutions.java index 9893313af25a5..f5809d4d27a19 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/graal/Substitutions.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/graal/Substitutions.java @@ -1,7 +1,6 @@ package io.quarkus.opentelemetry.runtime.graal; import java.io.Closeable; -import java.util.Collections; import java.util.List; import java.util.function.BiFunction; @@ -14,26 +13,9 @@ import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; import io.opentelemetry.sdk.logs.export.LogRecordExporter; -import io.opentelemetry.sdk.metrics.export.MetricExporter; -import io.opentelemetry.sdk.metrics.export.MetricReader; public class Substitutions { - @TargetClass(className = "io.opentelemetry.sdk.autoconfigure.MeterProviderConfiguration") - static final class Target_MeterProviderConfiguration { - - @Substitute - static List configureMetricReaders( - ConfigProperties config, - SpiHelper spiHelper, - BiFunction metricExporterCustomizer, - List closeables) { - // OTel metrics not supported and there is no need to call - // MetricExporterConfiguration.configurePrometheusMetricReader down the line. - return Collections.emptyList(); - } - } - @TargetClass(className = "io.opentelemetry.sdk.autoconfigure.LoggerProviderConfiguration") static final class Target_LoggerProviderConfiguration { diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/metrics/cdi/MetricsProducer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/metrics/cdi/MetricsProducer.java new file mode 100644 index 0000000000000..14972b1c82341 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/metrics/cdi/MetricsProducer.java @@ -0,0 +1,29 @@ +package io.quarkus.opentelemetry.runtime.metrics.cdi; + +import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.INSTRUMENTATION_NAME; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Singleton; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.common.Clock; +import io.quarkus.arc.DefaultBean; + +@Singleton +public class MetricsProducer { + @Produces + @ApplicationScoped + @DefaultBean + public Meter getMeter() { + return GlobalOpenTelemetry.getMeter(INSTRUMENTATION_NAME); + } + + @Produces + @Singleton + @DefaultBean + public Clock getClock() { + return Clock.getDefault(); + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/metrics/spi/MetricsExporterCDIProvider.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/metrics/spi/MetricsExporterCDIProvider.java new file mode 100644 index 0000000000000..03c63704d44df --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/metrics/spi/MetricsExporterCDIProvider.java @@ -0,0 +1,38 @@ +package io.quarkus.opentelemetry.runtime.metrics.spi; + +import static io.quarkus.opentelemetry.runtime.config.build.ExporterType.Constants.CDI_VALUE; + +import java.util.logging.Logger; + +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.CDI; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.quarkus.opentelemetry.runtime.exporter.otlp.metrics.NoopMetricExporter; + +public class MetricsExporterCDIProvider implements ConfigurableMetricExporterProvider { + + Logger log = Logger.getLogger(MetricsExporterCDIProvider.class.getName()); + + @Override + public MetricExporter createExporter(ConfigProperties configProperties) { + Instance exporters = CDI.current().select(MetricExporter.class, Any.Literal.INSTANCE); + log.fine("available exporters: " + exporters.stream() + .map(e -> e.getClass().getName()) + .reduce((a, b) -> a + ", " + b) + .orElse("none")); + if (exporters.isUnsatisfied()) { + return NoopMetricExporter.INSTANCE; + } else { + return exporters.get(); + } + } + + @Override + public String getName() { + return CDI_VALUE; + } +} diff --git a/extensions/opentelemetry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider b/extensions/opentelemetry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider new file mode 100644 index 0000000000000..124be714f4e1f --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider @@ -0,0 +1 @@ +io.quarkus.opentelemetry.runtime.metrics.spi.MetricsExporterCDIProvider \ No newline at end of file diff --git a/extensions/opentelemetry/runtime/src/main/resources/META-INF/services/org.jboss.resteasy.spi.concurrent.ThreadContext b/extensions/opentelemetry/runtime/src/main/resources/META-INF/services/org.jboss.resteasy.spi.concurrent.ThreadContext index c2b7e3c4bf1e7..972c5e8b0ec4f 100644 --- a/extensions/opentelemetry/runtime/src/main/resources/META-INF/services/org.jboss.resteasy.spi.concurrent.ThreadContext +++ b/extensions/opentelemetry/runtime/src/main/resources/META-INF/services/org.jboss.resteasy.spi.concurrent.ThreadContext @@ -1 +1 @@ -io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.OpenTelemetryClassicThreadContext +io.quarkus.opentelemetry.runtime.tracing.intrumentation.resteasy.OpenTelemetryClassicThreadContext \ No newline at end of file diff --git a/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OtlpExporterProviderTest.java b/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OtlpExporterProviderTest.java index b3391b3aa932e..7275aecf8b81b 100644 --- a/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OtlpExporterProviderTest.java +++ b/extensions/opentelemetry/runtime/src/test/java/io/quarkus/opentelemetry/runtime/exporter/otlp/OtlpExporterProviderTest.java @@ -11,6 +11,7 @@ import org.junit.jupiter.api.Test; import io.quarkus.opentelemetry.runtime.config.runtime.exporter.CompressionType; +import io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterMetricsConfig; import io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterRuntimeConfig; import io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterTracesConfig; @@ -172,6 +173,82 @@ public Optional host() { } }; } + + @Override + public OtlpExporterMetricsConfig metrics() { + return new OtlpExporterMetricsConfig() { + @Override + public Optional temporalityPreference() { + return Optional.empty(); + } + + @Override + public Optional defaultHistogramAggregation() { + return Optional.empty(); + } + + @Override + public Optional endpoint() { + return Optional.ofNullable(newTrace); + } + + @Override + public Optional> headers() { + return Optional.empty(); + } + + @Override + public Optional compression() { + return Optional.empty(); + } + + @Override + public Duration timeout() { + return null; + } + + @Override + public Optional protocol() { + return Optional.empty(); + } + + @Override + public KeyCert keyCert() { + return new KeyCert() { + @Override + public Optional> keys() { + return Optional.empty(); + } + + @Override + public Optional> certs() { + return Optional.empty(); + } + }; + } + + @Override + public TrustCert trustCert() { + return new TrustCert() { + @Override + public Optional> certs() { + return Optional.empty(); + } + }; + } + + @Override + public Optional tlsConfigurationName() { + return Optional.empty(); + } + + @Override + public ProxyConfig proxyOptions() { + return null; + } + }; + + } }; } } diff --git a/integration-tests/opentelemetry-vertx-exporter/src/main/java/io/quarkus/it/opentelemetry/vertx/exporter/HelloResource.java b/integration-tests/opentelemetry-vertx-exporter/src/main/java/io/quarkus/it/opentelemetry/vertx/exporter/HelloResource.java index f8c3ccc5a605b..a98fe5bfa1b10 100644 --- a/integration-tests/opentelemetry-vertx-exporter/src/main/java/io/quarkus/it/opentelemetry/vertx/exporter/HelloResource.java +++ b/integration-tests/opentelemetry-vertx-exporter/src/main/java/io/quarkus/it/opentelemetry/vertx/exporter/HelloResource.java @@ -1,13 +1,22 @@ package io.quarkus.it.opentelemetry.vertx.exporter; +import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.Meter; + @Path("hello") public class HelloResource { + @Inject + Meter meter; + @GET public String get() { + meter.counterBuilder("hello").build().add(1, Attributes.of(AttributeKey.stringKey("key"), "value")); return "get"; } diff --git a/integration-tests/opentelemetry-vertx-exporter/src/main/resources/application.properties b/integration-tests/opentelemetry-vertx-exporter/src/main/resources/application.properties index edb424258ea0a..b45db46c4211b 100644 --- a/integration-tests/opentelemetry-vertx-exporter/src/main/resources/application.properties +++ b/integration-tests/opentelemetry-vertx-exporter/src/main/resources/application.properties @@ -1 +1,3 @@ quarkus.application.name=integration test +quarkus.otel.metrics.enabled=true +quarkus.otel.metric.export.interval=1000ms \ No newline at end of file diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java index 0dc4e49f6413f..d3f0968c8f997 100644 --- a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/AbstractExporterTest.java @@ -1,18 +1,29 @@ package io.quarkus.it.opentelemetry.vertx.exporter; +import static io.opentelemetry.semconv.ResourceAttributes.SERVICE_NAME; import static io.restassured.RestAssured.when; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; import io.opentelemetry.proto.common.v1.AnyValue; import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.metrics.v1.AggregationTemporality; +import io.opentelemetry.proto.metrics.v1.Metric; +import io.opentelemetry.proto.metrics.v1.NumberDataPoint; +import io.opentelemetry.proto.metrics.v1.ResourceMetrics; +import io.opentelemetry.proto.metrics.v1.Sum; import io.opentelemetry.proto.trace.v1.ResourceSpans; import io.opentelemetry.proto.trace.v1.ScopeSpans; import io.opentelemetry.proto.trace.v1.Span; @@ -21,17 +32,20 @@ public abstract class AbstractExporterTest { Traces traces; + Metrics metrics; @BeforeEach @AfterEach void setUp() { traces.reset(); + metrics.reset(); } @Test void test() { verifyHttpResponse(); verifyTraces(); + verifyMetrics(); } private void verifyHttpResponse() { @@ -53,7 +67,7 @@ private void verifyTraces() { assertThat(resourceSpans.getResource().getAttributesList()) .contains( KeyValue.newBuilder() - .setKey(ResourceAttributes.SERVICE_NAME.getKey()) + .setKey(SERVICE_NAME.getKey()) .setValue(AnyValue.newBuilder() .setStringValue("integration test").build()) .build()) @@ -76,4 +90,46 @@ private void verifyTraces() { .setStringValue("GET").build()) .build()); } + + private void verifyMetrics() { + List metricRequests = metrics.getMetricRequests(); + await() + .atMost(Duration.ofSeconds(30)) + .untilAsserted(() -> assertThat(metricRequests).hasSizeGreaterThan(1)); + ExportMetricsServiceRequest request = metricRequests.get(metricRequests.size() - 1); + assertEquals(1, request.getResourceMetricsCount()); + + ResourceMetrics resourceMetrics = request.getResourceMetrics(0); + assertThat(resourceMetrics.getResource().getAttributesList()) + .contains( + KeyValue.newBuilder() + .setKey(SERVICE_NAME.getKey()) + .setValue(AnyValue.newBuilder().setStringValue("integration test").build()) + .build()); + assertThat(resourceMetrics.getScopeMetricsCount()).isEqualTo(2); + + Optional helloMetric = resourceMetrics.getScopeMetricsList().stream() + .map(scopeMetrics -> scopeMetrics.getMetricsList()) + .filter(metrics -> metrics.stream().anyMatch(metric -> metric.getName().equals("hello"))) + .flatMap(List::stream) + .findFirst(); + + assertThat(helloMetric).isPresent(); + assertThat(helloMetric.get().getDataCase()).isEqualTo(Metric.DataCase.SUM); + + Sum sum = helloMetric.get().getSum(); + assertThat(sum.getAggregationTemporality()) + .isEqualTo(AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE); + assertThat(sum.getDataPointsCount()).isEqualTo(1); + + NumberDataPoint dataPoint = sum.getDataPoints(0); + assertThat(dataPoint.getAsInt()).isEqualTo(1); + assertThat(dataPoint.getAttributesList()) + .isEqualTo( + Collections.singletonList( + KeyValue.newBuilder() + .setKey("key") + .setValue(AnyValue.newBuilder().setStringValue("value").build()) + .build())); + } } diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/Metrics.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/Metrics.java new file mode 100644 index 0000000000000..7448563dbd3af --- /dev/null +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/Metrics.java @@ -0,0 +1,19 @@ +package io.quarkus.it.opentelemetry.vertx.exporter; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; + +public final class Metrics { + + private final List metricRequests = new CopyOnWriteArrayList<>(); + + public List getMetricRequests() { + return metricRequests; + } + + public void reset() { + metricRequests.clear(); + } +} diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/OtelCollectorLifecycleManager.java b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/OtelCollectorLifecycleManager.java index bcb6bd7eda002..718817a3d543a 100644 --- a/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/OtelCollectorLifecycleManager.java +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/java/io/quarkus/it/opentelemetry/vertx/exporter/OtelCollectorLifecycleManager.java @@ -1,6 +1,6 @@ package io.quarkus.it.opentelemetry.vertx.exporter; -import static io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterTracesConfig.Protocol.GRPC; +import static io.quarkus.opentelemetry.runtime.config.runtime.exporter.OtlpExporterConfig.Protocol.GRPC; import static org.testcontainers.Testcontainers.exposeHostPorts; import java.util.HashMap; @@ -18,6 +18,7 @@ import com.google.protobuf.InvalidProtocolBufferException; +import io.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest; import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; @@ -42,7 +43,9 @@ public class OtelCollectorLifecycleManager implements QuarkusTestResourceLifecyc private static final Integer COLLECTOR_HEALTH_CHECK_PORT = 13133; private static final ServiceName TRACE_SERVICE_NAME = ServiceName .create("opentelemetry.proto.collector.trace.v1.TraceService"); - private static final String TRACE_METHOD_NAME = "Export"; + private static final ServiceName METRIC_SERVICE_NAME = ServiceName + .create("opentelemetry.proto.collector.metrics.v1.MetricsService"); + private static final String EXPORT_METHOD_NAME = "Export"; private SelfSignedCertificate serverTls; private SelfSignedCertificate clientTlS; @@ -58,6 +61,7 @@ public class OtelCollectorLifecycleManager implements QuarkusTestResourceLifecyc private GenericContainer collector; private Traces collectedTraces; + private Metrics collectedMetrics; @Override public void init(Map initArgs) { @@ -128,6 +132,7 @@ public Map start() { Map result = new HashMap<>(); result.put("quarkus.otel.exporter.otlp.traces.protocol", protocol); + result.put("quarkus.otel.exporter.otlp.metrics.protocol", protocol); boolean isGrpc = GRPC.equals(protocol); int secureEndpointPort = isGrpc ? COLLECTOR_OTLP_GRPC_MTLS_PORT : COLLECTOR_OTLP_HTTP_MTLS_PORT; @@ -136,8 +141,11 @@ public Map start() { if (enableTLS) { result.put("quarkus.otel.exporter.otlp.traces.endpoint", "https://" + collector.getHost() + ":" + collector.getMappedPort(secureEndpointPort)); + result.put("quarkus.otel.exporter.otlp.metrics.endpoint", + "https://" + collector.getHost() + ":" + collector.getMappedPort(secureEndpointPort)); if (tlsRegistryName != null) { result.put("quarkus.otel.exporter.otlp.traces.tls-configuration-name", tlsRegistryName); + result.put("quarkus.otel.exporter.otlp.metrics.tls-configuration-name", tlsRegistryName); if (!preventTrustCert) { result.put(String.format("quarkus.tls.%s.trust-store.pem.certs", tlsRegistryName), serverTls.certificatePath()); @@ -147,17 +155,23 @@ public Map start() { } else { if (!preventTrustCert) { result.put("quarkus.otel.exporter.otlp.traces.trust-cert.certs", serverTls.certificatePath()); + result.put("quarkus.otel.exporter.otlp.metrics.trust-cert.certs", serverTls.certificatePath()); } result.put("quarkus.otel.exporter.otlp.traces.key-cert.certs", clientTlS.certificatePath()); result.put("quarkus.otel.exporter.otlp.traces.key-cert.keys", clientTlS.privateKeyPath()); + result.put("quarkus.otel.exporter.otlp.metrics.key-cert.certs", clientTlS.certificatePath()); + result.put("quarkus.otel.exporter.otlp.metrics.key-cert.keys", clientTlS.privateKeyPath()); } } else { result.put("quarkus.otel.exporter.otlp.traces.endpoint", "http://" + collector.getHost() + ":" + collector.getMappedPort(inSecureEndpointPort)); + result.put("quarkus.otel.exporter.otlp.metrics.endpoint", + "http://" + collector.getHost() + ":" + collector.getMappedPort(inSecureEndpointPort)); } if (enableCompression) { result.put("quarkus.otel.exporter.otlp.traces.compression", "gzip"); + result.put("quarkus.otel.exporter.otlp.metrics.compression", "gzip"); } return result; @@ -166,15 +180,17 @@ public Map start() { @Override public void inject(TestInjector testInjector) { testInjector.injectIntoFields(collectedTraces, f -> f.getType().equals(Traces.class)); + testInjector.injectIntoFields(collectedMetrics, f -> f.getType().equals(Metrics.class)); } private void setupVertxGrpcServer() { vertx = Vertx.vertx(new VertxOptions().setWorkerPoolSize(1).setEventLoopPoolSize(1)); GrpcServer grpcServer = GrpcServer.server(vertx); collectedTraces = new Traces(); + collectedMetrics = new Metrics(); grpcServer.callHandler(request -> { - if (request.serviceName().equals(TRACE_SERVICE_NAME) && request.methodName().equals(TRACE_METHOD_NAME)) { + if (request.serviceName().equals(TRACE_SERVICE_NAME) && request.methodName().equals(EXPORT_METHOD_NAME)) { request.handler(message -> { try { @@ -186,12 +202,25 @@ private void setupVertxGrpcServer() { .end(); } }); + } else if (request.serviceName().equals(METRIC_SERVICE_NAME) && request.methodName().equals(EXPORT_METHOD_NAME)) { + + request.handler(message -> { + try { + collectedMetrics.getMetricRequests().add(ExportMetricsServiceRequest.parseFrom(message.getBytes())); + request.response().end(Buffer.buffer(ExportMetricsServiceRequest.getDefaultInstance().toByteArray())); + } catch (InvalidProtocolBufferException e) { + request.response() + .status(GrpcStatus.INVALID_ARGUMENT) + .end(); + } + }); } else { request.response() .status(GrpcStatus.NOT_FOUND) .end(); } }); + server = vertx.createHttpServer(new HttpServerOptions().setPort(0)); try { server.requestHandler(grpcServer).listen().toCompletionStage().toCompletableFuture().get(20, TimeUnit.SECONDS); diff --git a/integration-tests/opentelemetry-vertx-exporter/src/test/resources/otel-config.yaml b/integration-tests/opentelemetry-vertx-exporter/src/test/resources/otel-config.yaml index bab60e41819c7..427a292121c46 100644 --- a/integration-tests/opentelemetry-vertx-exporter/src/test/resources/otel-config.yaml +++ b/integration-tests/opentelemetry-vertx-exporter/src/test/resources/otel-config.yaml @@ -32,9 +32,9 @@ exporters: service: extensions: [health_check] pipelines: -# metrics: -# receivers: [otlp, otlp/mtls] -# exporters: [logging, otlp] + metrics: + receivers: [otlp, otlp/mtls] + exporters: [logging, otlp] traces: receivers: [otlp, otlp/mtls] exporters: [logging, otlp] diff --git a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/ExporterResource.java b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/ExporterResource.java index a611fda7c2c7b..19048542c7907 100644 --- a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/ExporterResource.java +++ b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/ExporterResource.java @@ -1,5 +1,7 @@ package io.quarkus.it.opentelemetry; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -9,32 +11,65 @@ import jakarta.inject.Singleton; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Response; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricExporter; import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.SemanticAttributes; @Path("") public class ExporterResource { @Inject InMemorySpanExporter inMemorySpanExporter; + @Inject + InMemoryMetricExporter inMemoryMetricExporter; @GET @Path("/reset") public Response reset() { inMemorySpanExporter.reset(); + inMemoryMetricExporter.reset(); return Response.ok().build(); } @GET @Path("/export") - public List export() { + public List exportTraces() { return inMemorySpanExporter.getFinishedSpanItems() .stream() .filter(sd -> !sd.getName().contains("export") && !sd.getName().contains("reset")) .collect(Collectors.toList()); } + @GET + @Path("/export/metrics") + public List exportMetrics(@QueryParam("name") String name, @QueryParam("target") String target) { + return Collections.unmodifiableList(new ArrayList<>( + inMemoryMetricExporter.getFinishedMetricItems().stream() + .filter(metricData -> name == null ? true : metricData.getName().equals(name)) + .filter(metricData -> target == null ? true + : metricData.getData() + .getPoints().stream() + .anyMatch(point -> isPathFound(target, point.getAttributes()))) + .collect(Collectors.toList()))); + } + + private static boolean isPathFound(String path, Attributes attributes) { + if (path == null) { + return true;// any match + } + Object value = attributes.asMap().get(AttributeKey.stringKey(SemanticAttributes.HTTP_ROUTE.getKey())); + if (value == null) { + return false; + } + return value.toString().equals(path); + } + @ApplicationScoped static class InMemorySpanExporterProducer { @Produces @@ -43,4 +78,13 @@ InMemorySpanExporter inMemorySpanExporter() { return InMemorySpanExporter.create(); } } + + @ApplicationScoped + static class InMemoryMetricExporterProducer { + @Produces + @Singleton + InMemoryMetricExporter inMemoryMetricsExporter() { + return InMemoryMetricExporter.create(); + } + } } diff --git a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/SimpleResource.java b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/SimpleResource.java index 088a66a943c29..878ea3a44e6ac 100644 --- a/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/SimpleResource.java +++ b/integration-tests/opentelemetry/src/main/java/io/quarkus/it/opentelemetry/SimpleResource.java @@ -11,6 +11,9 @@ import org.eclipse.microprofile.rest.client.inject.RestClient; import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.context.Scope; @Path("") @@ -41,6 +44,9 @@ public interface SimpleClient { @Inject Baggage baggage; + @Inject + Meter meter; + @GET public TraceData noPath() { TraceData data = new TraceData(); @@ -88,6 +94,19 @@ public TraceData directTrace() { return data; } + @GET + @Path("/direct-metrics") + public TraceData directTraceWithMetrics() { + meter.counterBuilder("direct-trace-counter") + .setUnit("items") + .setDescription("A counter of direct traces") + .build() + .add(1, Attributes.of(AttributeKey.stringKey("key"), "low-cardinality-value")); + TraceData data = new TraceData(); + data.message = "Direct trace"; + return data; + } + @GET @Path("/chained") public TraceData chainedTrace() { diff --git a/integration-tests/opentelemetry/src/main/resources/application.properties b/integration-tests/opentelemetry/src/main/resources/application.properties index 514f563c31933..6ea19c81c9929 100644 --- a/integration-tests/opentelemetry/src/main/resources/application.properties +++ b/integration-tests/opentelemetry/src/main/resources/application.properties @@ -5,6 +5,8 @@ quarkus.application.version=999-SNAPSHOT # speed up build quarkus.otel.bsp.schedule.delay=100 quarkus.otel.bsp.export.timeout=5s +quarkus.otel.metrics.enabled=true +quarkus.otel.metric.export.interval=100ms pingpong/mp-rest/url=${test.url} simple/mp-rest/url=${test.url} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/MetricsIT.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/MetricsIT.java new file mode 100644 index 0000000000000..ab89e166dd7ef --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/MetricsIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.opentelemetry; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class MetricsIT extends MetricsTest { +} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/MetricsTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/MetricsTest.java new file mode 100644 index 0000000000000..8dfe4fb3fea5f --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/MetricsTest.java @@ -0,0 +1,71 @@ +package io.quarkus.it.opentelemetry; + +import static io.restassured.RestAssured.get; +import static io.restassured.RestAssured.given; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.common.mapper.TypeRef; + +@QuarkusTest +public class MetricsTest { + @BeforeEach + @AfterEach + void reset() { + given().get("/reset").then().statusCode(HTTP_OK); + } + + private List> getSpans() { + return get("/export").body().as(new TypeRef<>() { + }); + } + + private List> getMetrics() { + return given() + .when() + .queryParam("name", "direct-trace-counter") + .get("/export/metrics") + .body().as(new TypeRef<>() { + }); + } + + @Test + public void directCounterTest() { + given() + .when() + .get("/direct-metrics") + .then() + .statusCode(200); + given() + .when().get("/direct-metrics") + .then() + .statusCode(200); + + await().atMost(5, SECONDS).until(() -> getSpans().size() == 2); + await().atMost(10, SECONDS).until(() -> getMetrics().size() > 2); + + List> metrics = getMetrics(); + Integer value = (Integer) ((Map) ((List) ((Map) (getMetrics() + .get(metrics.size() - 1) + .get("longSumData"))) + .get("points")) + .get(0)) + .get("value"); + + assertEquals(2, value, "received: " + given() + .when() + .get("/export/metrics") + .body().as(new TypeRef<>() { + })); + } +} diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/StaticResourceTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/StaticResourceTest.java index 6412826e33141..8d6934bc7ebe0 100644 --- a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/StaticResourceTest.java +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/StaticResourceTest.java @@ -24,8 +24,15 @@ public class StaticResourceTest { @BeforeEach @AfterEach void reset() { - given().get("/reset").then().statusCode(HTTP_OK); - await().atMost(5, TimeUnit.SECONDS).until(() -> getSpans().size() == 0); + await().atMost(5, TimeUnit.SECONDS).until(() -> { + List> spans = getSpans(); + if (spans.size() == 0) { + return true; + } else { + given().get("/reset").then().statusCode(HTTP_OK); + return false; + } + }); } @Test diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryIT.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/TracingIT.java similarity index 93% rename from integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryIT.java rename to integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/TracingIT.java index a7e516388cfd1..0d5e378981885 100644 --- a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryIT.java +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/TracingIT.java @@ -7,7 +7,7 @@ import io.quarkus.test.junit.QuarkusIntegrationTest; @QuarkusIntegrationTest -public class OpenTelemetryIT extends OpenTelemetryTest { +public class TracingIT extends TracingTest { @Override protected void buildGlobalTelemetryInstance() { // When running native tests the test class is outside the Quarkus application, diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/TracingTest.java similarity index 99% rename from integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java rename to integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/TracingTest.java index 8b9173127ea6d..afe87de653e74 100644 --- a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/OpenTelemetryTest.java +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/TracingTest.java @@ -49,7 +49,7 @@ import io.restassured.specification.RequestSpecification; @QuarkusTest -public class OpenTelemetryTest { +public class TracingTest { @TestHTTPResource("direct") URL directUrl; @TestHTTPResource("chained") @@ -62,8 +62,15 @@ public class OpenTelemetryTest { @BeforeEach @AfterEach void reset() { - given().get("/reset").then().statusCode(HTTP_OK); - await().atMost(5, SECONDS).until(() -> getSpans().size() == 0); + await().atMost(5, SECONDS).until(() -> { + List> spans = getSpans(); + if (spans.size() == 0) { + return true; + } else { + given().get("/reset").then().statusCode(HTTP_OK); + return false; + } + }); } private List> getSpans() { diff --git a/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/tck/SpanBeanTest.java b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/tck/SpanBeanTest.java new file mode 100644 index 0000000000000..4a4106d636adf --- /dev/null +++ b/integration-tests/opentelemetry/src/test/java/io/quarkus/it/opentelemetry/tck/SpanBeanTest.java @@ -0,0 +1,68 @@ +package io.quarkus.it.opentelemetry.tck; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.quarkus.test.junit.QuarkusTest; + +/** + * Same as in the TCK, for early warning and debugging + */ +@QuarkusTest +public class SpanBeanTest { + @Inject + private Span injectedSpan; + + @Inject + private Tracer tracer; + + @Test + public void spanBeanChange() { + Span originalSpan = Span.current(); + // Check the injected span reflects the current span initially + assertEquals(originalSpan.getSpanContext().getSpanId(), injectedSpan.getSpanContext().getSpanId()); + + // Create a new span + Span span1 = tracer.spanBuilder("span1").startSpan(); + // Check we have a real span with a different spanId + assertNotEquals(originalSpan.getSpanContext().getSpanId(), span1.getSpanContext().getSpanId()); + + // The original span should still be "current", so the injected span should still reflect it + assertEquals(originalSpan.getSpanContext().getSpanId(), injectedSpan.getSpanContext().getSpanId()); + + // Make span1 current + try (Scope s = span1.makeCurrent()) { + Span current = Span.current(); + // Now the injected span should reflect span1 + assertEquals(span1.getSpanContext().getSpanId(), injectedSpan.getSpanContext().getSpanId()); + + // Make a new span + Span span2 = tracer.spanBuilder("span2").startSpan(); + // Injected span should still reflect span1 + assertEquals(span1.getSpanContext().getSpanId(), injectedSpan.getSpanContext().getSpanId()); + + // Make span2 current + try (Scope s2 = span2.makeCurrent()) { + // Now the injected span should reflect span2 + assertEquals(span2.getSpanContext().getSpanId(), injectedSpan.getSpanContext().getSpanId()); + } finally { + span2.end(); + } + + // After closing the scope, span1 is current again and the injected bean should reflect that + assertEquals(span1.getSpanContext().getSpanId(), injectedSpan.getSpanContext().getSpanId()); + } finally { + span1.end(); + } + + // After closing the scope, the original span is current again + assertEquals(originalSpan.getSpanContext().getSpanId(), injectedSpan.getSpanContext().getSpanId()); + } +}