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 2fff029e4344e..e228f1e5e5d78 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 @@ -23,7 +23,7 @@ import org.jboss.jandex.Type; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.exporter.otlp.internal.OtlpLogRecordExporterProvider; import io.opentelemetry.exporter.otlp.internal.OtlpMetricExporterProvider; import io.opentelemetry.exporter.otlp.internal.OtlpSpanExporterProvider; import io.opentelemetry.instrumentation.annotations.AddingSpanAttributes; @@ -31,6 +31,7 @@ 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.logs.ConfigurableLogRecordExporterProvider; import io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider; import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider; import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; @@ -86,7 +87,6 @@ public boolean test(AnnotationInstance annotationInstance) { return annotationInstance.name().equals(ADD_SPAN_ATTRIBUTES); } }; - private static final DotName SPAN_KIND = DotName.createSimple(SpanKind.class.getName()); private static final DotName WITH_SPAN_INTERCEPTOR = DotName.createSimple(WithSpanInterceptor.class.getName()); private static final DotName ADD_SPAN_ATTRIBUTES_INTERCEPTOR = DotName .createSimple(AddingSpanAttributesInterceptor.class.getName()); @@ -163,10 +163,30 @@ void handleServices(OTelBuildConfig config, Set.of("META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider"))); } + final List logRecordExporterProviders = ServiceUtil.classNamesNamedIn( + Thread.currentThread().getContextClassLoader(), + SPI_ROOT + ConfigurableLogRecordExporterProvider.class.getName()) + .stream() + .filter(p -> !OtlpLogRecordExporterProvider.class.getName().equals(p)) + .collect(toList()); // filter out OtlpLogRecordExporterProvider since it depends on OkHttp + if (!logRecordExporterProviders.isEmpty()) { + services.produce( + new ServiceProviderBuildItem(ConfigurableLogRecordExporterProvider.class.getName(), + logRecordExporterProviders)); + } + if (config.logs().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.logs.ConfigurableLogRecordExporterProvider"))); + } + + // TODO these classes don't exist! runtimeReinitialized.produce( new RuntimeReinitializedClassBuildItem("io.opentelemetry.sdk.autoconfigure.TracerProviderConfiguration")); runtimeReinitialized.produce( new RuntimeReinitializedClassBuildItem("io.opentelemetry.sdk.autoconfigure.MeterProviderConfiguration")); + runtimeReinitialized.produce( + new RuntimeReinitializedClassBuildItem("io.opentelemetry.sdk.autoconfigure.LogMeterProviderConfiguration")); 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 1b6eac1ebadd4..5d4bc5876ae83 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,6 +13,7 @@ import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; import io.opentelemetry.sdk.metrics.export.MetricExporter; import io.opentelemetry.sdk.trace.SpanProcessor; import io.opentelemetry.sdk.trace.export.SpanExporter; @@ -34,6 +35,7 @@ public class OtlpExporterProcessor { private static final DotName METRIC_EXPORTER = DotName.createSimple(MetricExporter.class.getName()); + private static final DotName LOG_RECORD_EXPORTER = DotName.createSimple(LogRecordExporter.class.getName()); static class OtlpTracingExporterEnabled implements BooleanSupplier { OtlpExporterBuildConfig exportBuildConfig; @@ -59,6 +61,18 @@ public boolean getAsBoolean() { } } + static class OtlpLogRecordExporterEnabled implements BooleanSupplier { + OtlpExporterBuildConfig exportBuildConfig; + OTelBuildConfig otelBuildConfig; + + public boolean getAsBoolean() { + return otelBuildConfig.enabled() && + otelBuildConfig.logs().enabled().orElse(Boolean.TRUE) && + otelBuildConfig.logs().exporter().contains(CDI_VALUE) && + exportBuildConfig.enabled(); + } + } + @SuppressWarnings("deprecation") @BuildStep(onlyIf = OtlpExporterProcessor.OtlpTracingExporterEnabled.class) @Record(ExecutionTime.RUNTIME_INIT) @@ -123,4 +137,41 @@ void createMetricsExporterProcessor( vertxBuildItem.getVertx())) .done()); } + + @BuildStep(onlyIf = OtlpLogRecordExporterEnabled.class) + @Record(ExecutionTime.RUNTIME_INIT) + @Consume(TlsRegistryBuildItem.class) + void createLogRecordExporterProcessor( + 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(LOG_RECORD_EXPORTER).isEmpty()) { + // if there is a MetricExporter bean impl around, we don't want to create the default one + return; + } + + syntheticBeanBuildItemBuildProducer.produce(SyntheticBeanBuildItem + .configure(LogRecordExporter.class) + .types(LogRecordExporter.class) + .setRuntimeInit() + .scope(Singleton.class) + .unremovable() + .addInjectionPoint(ParameterizedType.create(DotName.createSimple(Instance.class), + new Type[] { ClassType.create(DotName.createSimple(LogRecordExporter.class.getName())) }, null)) + .addInjectionPoint(ClassType.create(DotName.createSimple(TlsConfigurationRegistry.class))) + .createWith(recorder.createLogRecordExporter(otelRuntimeConfig, exporterRuntimeConfig, + vertxBuildItem.getVertx())) + .done()); + } } diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/logging/LogHandlerProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/logging/LogHandlerProcessor.java new file mode 100644 index 0000000000000..35332a7a9ead3 --- /dev/null +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/logging/LogHandlerProcessor.java @@ -0,0 +1,17 @@ +package io.quarkus.opentelemetry.deployment.logging; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.LogHandlerBuildItem; +import io.quarkus.opentelemetry.runtime.logs.OpenTelemetryLogConfig; +import io.quarkus.opentelemetry.runtime.logs.OpenTelemetryLogRecorder; + +class LogHandlerProcessor { + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + LogHandlerBuildItem build(OpenTelemetryLogRecorder recorder, OpenTelemetryLogConfig config) { + return new LogHandlerBuildItem(recorder.initializeHandler(config)); + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/LogsBuildConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/LogsBuildConfig.java new file mode 100644 index 0000000000000..6c81a56be0c3f --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/build/LogsBuildConfig.java @@ -0,0 +1,27 @@ +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 LogsBuildConfig { + /** + * Enable logs with OpenTelemetry. + *

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

+ * Support for logs will be enabled if OpenTelemetry support is enabled + * and either this value is true, or this value is unset. + */ + @WithDefault("false") + Optional enabled(); + + /** + * The Logs 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 1e1bef92e25a3..2e4e3d906cec2 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 @@ -48,6 +48,11 @@ public interface OTelBuildConfig { */ MetricsBuildConfig metrics(); + /** + * Logs exporter configurations. + */ + LogsBuildConfig logs(); + /** * No Log exporter for now. */ diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterLogsConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterLogsConfig.java new file mode 100644 index 0000000000000..2a01f08af2299 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/config/runtime/exporter/OtlpExporterLogsConfig.java @@ -0,0 +1,7 @@ +package io.quarkus.opentelemetry.runtime.config.runtime.exporter; + +import io.quarkus.runtime.annotations.ConfigGroup; + +@ConfigGroup +public interface OtlpExporterLogsConfig extends OtlpExporterConfig { +} 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 30979461fa39c..98f6520c22d9f 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 @@ -22,7 +22,11 @@ public interface OtlpExporterRuntimeConfig extends OtlpExporterConfig { * OTLP metrics exporter configuration. */ OtlpExporterMetricsConfig metrics(); - // TODO logs(); + + /** + * OTLP logs exporter configuration. + */ + OtlpExporterLogsConfig logs(); // TODO additional global exporter configuration } 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 4d7d4956b61d5..65f80415778bd 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 @@ -22,10 +22,12 @@ 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.logs.LogsRequestMarshaler; 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.logs.export.LogRecordExporter; import io.opentelemetry.sdk.metrics.Aggregation; import io.opentelemetry.sdk.metrics.InstrumentType; import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector; @@ -37,11 +39,10 @@ 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.config.runtime.exporter.*; +import io.quarkus.opentelemetry.runtime.exporter.otlp.logs.NoopLogRecordExporter; +import io.quarkus.opentelemetry.runtime.exporter.otlp.logs.VertxGrpcLogRecordExporter; +import io.quarkus.opentelemetry.runtime.exporter.otlp.logs.VertxHttpLogRecordExporter; 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; @@ -196,7 +197,7 @@ public MetricExporter apply(SyntheticCreationalContext context) OtlpExporterMetricsConfig metricsConfig = exporterRuntimeConfig.metrics(); if (metricsConfig.protocol().isEmpty()) { throw new IllegalStateException("No OTLP protocol specified. " + - "Please check `quarkus.otel.exporter.otlp.traces.protocol` property"); + "Please check `quarkus.otel.exporter.otlp.metrics.protocol` property"); } String protocol = metricsConfig.protocol().get(); @@ -237,7 +238,7 @@ public MetricExporter apply(SyntheticCreationalContext context) aggregationResolver(metricsConfig)); } else { throw new IllegalArgumentException(String.format("Unsupported OTLP protocol %s specified. " + - "Please check `quarkus.otel.exporter.otlp.traces.protocol` property", protocol)); + "Please check `quarkus.otel.exporter.otlp.metrics.protocol` property", protocol)); } } catch (IllegalArgumentException iae) { @@ -248,6 +249,74 @@ public MetricExporter apply(SyntheticCreationalContext context) }; } + public Function, LogRecordExporter> createLogRecordExporter( + OTelRuntimeConfig otelRuntimeConfig, OtlpExporterRuntimeConfig exporterRuntimeConfig, Supplier vertx) { + final URI baseUri = getMetricsUri(exporterRuntimeConfig); + + return new Function<>() { + @Override + public LogRecordExporter apply(SyntheticCreationalContext context) { + + if (otelRuntimeConfig.sdkDisabled() || baseUri == null) { + return NoopLogRecordExporter.INSTANCE; + } + + LogRecordExporter logRecordExporter; + + try { + TlsConfigurationRegistry tlsConfigurationRegistry = context + .getInjectedReference(TlsConfigurationRegistry.class); + OtlpExporterLogsConfig logsConfig = exporterRuntimeConfig.logs(); + if (logsConfig.protocol().isEmpty()) { + throw new IllegalStateException("No OTLP protocol specified. " + + "Please check `quarkus.otel.exporter.otlp.logs.protocol` property"); + } + + String protocol = logsConfig.protocol().get(); + if (GRPC.equals(protocol)) { + logRecordExporter = new VertxGrpcLogRecordExporter( + new GrpcExporter( + OTLP_VALUE, // use the same as OTel does + "log", // use the same as OTel does + new VertxGrpcSender( + baseUri, + VertxGrpcSender.GRPC_LOG_SERVICE_NAME, + determineCompression(logsConfig), + logsConfig.timeout(), + populateTracingExportHttpHeaders(logsConfig), + new HttpClientOptionsConsumer(logsConfig, baseUri, tlsConfigurationRegistry), + vertx.get()), + MeterProvider::noop)); + } else if (HTTP_PROTOBUF.equals(protocol)) { + boolean exportAsJson = false; //TODO: this will be enhanced in the future + logRecordExporter = new VertxHttpLogRecordExporter( + new HttpExporter( + OTLP_VALUE, // use the same as OTel does + "log", // use the same as OTel does + new VertxHttpSender( + baseUri, + VertxHttpSender.LOGS_PATH, + determineCompression(logsConfig), + logsConfig.timeout(), + populateTracingExportHttpHeaders(logsConfig), + exportAsJson ? "application/json" : "application/x-protobuf", + new HttpClientOptionsConsumer(logsConfig, baseUri, tlsConfigurationRegistry), + vertx.get()), + MeterProvider::noop, + exportAsJson)); + } else { + throw new IllegalArgumentException(String.format("Unsupported OTLP protocol %s specified. " + + "Please check `quarkus.otel.exporter.otlp.logs.protocol` property", protocol)); + } + + } catch (IllegalArgumentException iae) { + throw new IllegalStateException("Unable to install OTLP Exporter", iae); + } + return logRecordExporter; + } + }; + } + private static DefaultAggregationSelector aggregationResolver(OtlpExporterMetricsConfig metricsConfig) { String defaultHistogramAggregation = metricsConfig.defaultHistogramAggregation() .map(s -> s.toLowerCase(Locale.ROOT)) diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/logs/NoopLogRecordExporter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/logs/NoopLogRecordExporter.java new file mode 100644 index 0000000000000..8e3b273a20066 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/logs/NoopLogRecordExporter.java @@ -0,0 +1,29 @@ +package io.quarkus.opentelemetry.runtime.exporter.otlp.logs; + +import java.util.Collection; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; + +public class NoopLogRecordExporter implements LogRecordExporter { + public static final NoopLogRecordExporter INSTANCE = new NoopLogRecordExporter(); + + private NoopLogRecordExporter() { + } + + @Override + public CompletableResultCode export(Collection collection) { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/logs/VertxGrpcLogRecordExporter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/logs/VertxGrpcLogRecordExporter.java new file mode 100644 index 0000000000000..dd7e5b245cf65 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/logs/VertxGrpcLogRecordExporter.java @@ -0,0 +1,32 @@ +package io.quarkus.opentelemetry.runtime.exporter.otlp.logs; + +import java.util.Collection; + +import io.opentelemetry.exporter.internal.grpc.GrpcExporter; +import io.opentelemetry.exporter.internal.otlp.logs.LogsRequestMarshaler; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; + +public class VertxGrpcLogRecordExporter implements LogRecordExporter { + private final GrpcExporter delegate; + + public VertxGrpcLogRecordExporter(GrpcExporter delegate) { + this.delegate = delegate; + } + + @Override + public CompletableResultCode export(Collection collection) { + return delegate.export(LogsRequestMarshaler.create(collection), collection.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/logs/VertxHttpLogRecordExporter.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/logs/VertxHttpLogRecordExporter.java new file mode 100644 index 0000000000000..9d23d65ec7c77 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/logs/VertxHttpLogRecordExporter.java @@ -0,0 +1,32 @@ +package io.quarkus.opentelemetry.runtime.exporter.otlp.logs; + +import java.util.Collection; + +import io.opentelemetry.exporter.internal.http.HttpExporter; +import io.opentelemetry.exporter.internal.otlp.logs.LogsRequestMarshaler; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; + +public class VertxHttpLogRecordExporter implements LogRecordExporter { + private final HttpExporter delegate; + + public VertxHttpLogRecordExporter(HttpExporter delegate) { + this.delegate = delegate; + } + + @Override + public CompletableResultCode export(Collection collection) { + return delegate.export(LogsRequestMarshaler.create(collection), collection.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/sender/VertxGrpcSender.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/sender/VertxGrpcSender.java index d7c856243609a..768e8ee20f70c 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/sender/VertxGrpcSender.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/exporter/otlp/sender/VertxGrpcSender.java @@ -41,6 +41,7 @@ public final class VertxGrpcSender implements GrpcSender { 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"; + public static final String GRPC_LOG_SERVICE_NAME = "opentelemetry.proto.collector.logs.v1.LogsService"; private static final String GRPC_METHOD_NAME = "Export"; private static final String GRPC_STATUS = "grpc-status"; 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 index aa56e704d2699..101c51d466c84 100644 --- 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 @@ -38,6 +38,7 @@ public final class VertxHttpSender implements HttpSender { public static final String TRACES_PATH = "/v1/traces"; public static final String METRICS_PATH = "/v1/metrics"; + public static final String LOGS_PATH = "/v1/logs"; // TODO check private static final Logger internalLogger = Logger.getLogger(VertxHttpSender.class.getName()); private static final ThrottlingLogger logger = new ThrottlingLogger(internalLogger); diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogConfig.java new file mode 100644 index 0000000000000..9ccdb093d1297 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogConfig.java @@ -0,0 +1,14 @@ +package io.quarkus.opentelemetry.runtime.logs; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; + +@ConfigMapping(prefix = "quarkus.log.handler.open-telemetry") +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public interface OpenTelemetryLogConfig { + /** + * Determine whether to enable the OpenTelemetry logging handler + */ + boolean enabled(); +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java new file mode 100644 index 0000000000000..03cb82e81fe2d --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java @@ -0,0 +1,57 @@ +package io.quarkus.opentelemetry.runtime.logs; + +import static io.quarkus.opentelemetry.runtime.config.build.OTelBuildConfig.INSTRUMENTATION_NAME; + +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.api.logs.Severity; + +public class OpenTelemetryLogHandler extends Handler { + private final Logger openTelemetry; + + public OpenTelemetryLogHandler(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry.getLogsBridge().get(INSTRUMENTATION_NAME); + } + + @Override + public void publish(LogRecord record) { + openTelemetry.logRecordBuilder() + .setSeverity(mapSeverity(record.getLevel())) + .setSeverityText(record.getLevel().getName()) + .setBody(record.getMessage()) // TODO check that we didn't need to format it + .setObservedTimestamp(record.getInstant()) + // TODO add attributes + .emit(); + } + + private Severity mapSeverity(Level level) { + if (Level.SEVERE.equals(level)) { + return Severity.ERROR; + } + if (Level.WARNING.equals(level)) { + return Severity.WARN; + } + if (Level.INFO.equals(level) || Level.CONFIG.equals(level)) { + return Severity.INFO; + } + if (Level.FINE.equals(level)) { + return Severity.DEBUG; + } + if (Level.FINER.equals(level) || Level.FINEST.equals(level) || Level.ALL.equals(level)) { + return Severity.TRACE; + } + return Severity.UNDEFINED_SEVERITY_NUMBER; + } + + @Override + public void flush() { + } + + @Override + public void close() throws SecurityException { + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogRecorder.java new file mode 100644 index 0000000000000..e12ca43633fa7 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogRecorder.java @@ -0,0 +1,21 @@ +package io.quarkus.opentelemetry.runtime.logs; + +import java.util.Optional; +import java.util.logging.Handler; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class OpenTelemetryLogRecorder { + public RuntimeValue> initializeHandler(final OpenTelemetryLogConfig config) { + if (!config.enabled()) { + return new RuntimeValue<>(Optional.empty()); + } + + OpenTelemetryLogHandler handler = new OpenTelemetryLogHandler(GlobalOpenTelemetry.get()); + + return new RuntimeValue<>(Optional.of(handler)); + } +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/spi/LogsExporterCDIProvider.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/spi/LogsExporterCDIProvider.java new file mode 100644 index 0000000000000..31f34b6be22a2 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/spi/LogsExporterCDIProvider.java @@ -0,0 +1,38 @@ +package io.quarkus.opentelemetry.runtime.logs.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.logs.ConfigurableLogRecordExporterProvider; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.quarkus.opentelemetry.runtime.exporter.otlp.logs.NoopLogRecordExporter; + +public class LogsExporterCDIProvider implements ConfigurableLogRecordExporterProvider { + + Logger log = Logger.getLogger(LogsExporterCDIProvider.class.getName()); + + @Override + public LogRecordExporter createExporter(ConfigProperties configProperties) { + Instance exporters = CDI.current().select(LogRecordExporter.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 NoopLogRecordExporter.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.logs.ConfigurableLogRecordExporterProvider b/extensions/opentelemetry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider new file mode 100644 index 0000000000000..305fdf382219b --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider @@ -0,0 +1 @@ +io.quarkus.opentelemetry.runtime.logs.spi.LogsExporterCDIProvider \ 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 625baaa74a337..ee4318a61accc 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 @@ -10,10 +10,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; +import io.quarkus.opentelemetry.runtime.config.runtime.exporter.*; class OtlpExporterProviderTest { @@ -309,6 +306,72 @@ public ProxyConfig proxyOptions() { }; } + + @Override + public OtlpExporterLogsConfig logs() { + return new OtlpExporterLogsConfig() { + @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-logging/pom.xml b/integration-tests/opentelemetry-logging/pom.xml new file mode 100644 index 0000000000000..0f95442fd09d2 --- /dev/null +++ b/integration-tests/opentelemetry-logging/pom.xml @@ -0,0 +1,118 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + + quarkus-logging-opentelemetry-integration-tests + Quarkus - Integration Tests - Logging - OpenTelemetry + + true + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-opentelemetry + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-opentelemetry-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + + native-image + + + native + + + + + + maven-surefire-plugin + + ${native.surefire.skip} + + + + + + false + native + + + + diff --git a/integration-tests/opentelemetry-logging/src/main/java/io/quarkus/logging/opentelemetry/it/LoggingResource.java b/integration-tests/opentelemetry-logging/src/main/java/io/quarkus/logging/opentelemetry/it/LoggingResource.java new file mode 100644 index 0000000000000..cf73b16a0ebd6 --- /dev/null +++ b/integration-tests/opentelemetry-logging/src/main/java/io/quarkus/logging/opentelemetry/it/LoggingResource.java @@ -0,0 +1,36 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF licenses this file to You under the Apache License, Version 2.0 +* (the "License"); you may not use this file except in compliance with +* the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package io.quarkus.logging.opentelemetry.it; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Path("/logging-opentelemetry") +@ApplicationScoped +public class LoggingResource { + private static final Logger LOG = LoggerFactory.getLogger(LoggingResource.class); + + @GET + public String hello() { + LOG.info("Hello {}", "World"); + return "Hello logging-opentelemetry"; + } +} diff --git a/integration-tests/opentelemetry-logging/src/main/resources/application.properties b/integration-tests/opentelemetry-logging/src/main/resources/application.properties new file mode 100644 index 0000000000000..036882a802e3f --- /dev/null +++ b/integration-tests/opentelemetry-logging/src/main/resources/application.properties @@ -0,0 +1,4 @@ +quarkus.log.handler.open-telemetry.enabled=true +quarkus.otel.exporter.otlp.traces.endpoint=http://localhost:4317 +quarkus.otel.exporter.otlp.logs.endpoint=http://localhost:4317 +quarkus.otel.logs.enabled=true \ No newline at end of file diff --git a/integration-tests/opentelemetry-logging/src/test/java/io/quarkus/logging/opentelemetry/it/LoggingResourceIT.java b/integration-tests/opentelemetry-logging/src/test/java/io/quarkus/logging/opentelemetry/it/LoggingResourceIT.java new file mode 100644 index 0000000000000..c3e8e518f2e51 --- /dev/null +++ b/integration-tests/opentelemetry-logging/src/test/java/io/quarkus/logging/opentelemetry/it/LoggingResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkus.logging.opentelemetry.it; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class LoggingResourceIT extends LoggingResourceTest { +} diff --git a/integration-tests/opentelemetry-logging/src/test/java/io/quarkus/logging/opentelemetry/it/LoggingResourceTest.java b/integration-tests/opentelemetry-logging/src/test/java/io/quarkus/logging/opentelemetry/it/LoggingResourceTest.java new file mode 100644 index 0000000000000..d672e198692ab --- /dev/null +++ b/integration-tests/opentelemetry-logging/src/test/java/io/quarkus/logging/opentelemetry/it/LoggingResourceTest.java @@ -0,0 +1,21 @@ +package io.quarkus.logging.opentelemetry.it; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class LoggingResourceTest { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/logging-opentelemetry") + .then() + .statusCode(200) + .body(is("Hello logging-opentelemetry")); + } +} diff --git a/integration-tests/opentelemetry-logging/src/test/resources/docker-compose.yml b/integration-tests/opentelemetry-logging/src/test/resources/docker-compose.yml new file mode 100644 index 0000000000000..29246fc7d173f --- /dev/null +++ b/integration-tests/opentelemetry-logging/src/test/resources/docker-compose.yml @@ -0,0 +1,14 @@ + +services: + otel-collector: + image: otel/opentelemetry-collector-contrib + volumes: + - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml + ports: + - 1888:1888 # pprof extension + - 8888:8888 # Prometheus metrics exposed by the Collector + - 8889:8889 # Prometheus exporter metrics + - 13133:13133 # health_check extension + - 4317:4317 # OTLP gRPC receiver + - 4318:4318 # OTLP http receiver + - 55679:55679 # zpages extension diff --git a/integration-tests/opentelemetry-logging/src/test/resources/otel-collector-config.yaml b/integration-tests/opentelemetry-logging/src/test/resources/otel-collector-config.yaml new file mode 100644 index 0000000000000..b581b1b4c5485 --- /dev/null +++ b/integration-tests/opentelemetry-logging/src/test/resources/otel-collector-config.yaml @@ -0,0 +1,33 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: otel-collector:4317 + http: + endpoint: otel-collector:4318 + +exporters: + logging: + loglevel: debug + +processors: + batch: + +extensions: + health_check: + +service: + extensions: [health_check] + pipelines: + traces: + receivers: [otlp] + processors: [] + exporters: [logging] + metrics: + receivers: [otlp] + processors: [] + exporters: [logging] + logs: + receivers: [otlp] + processors: [] + exporters: [logging] diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index ebffa37fc850d..b751a2de84957 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -410,6 +410,7 @@ virtual-threads mutiny-native-jctools + opentelemetry-logging