diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/logging/OpenTelemetryLogHandlerProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/logging/OpenTelemetryLogHandlerProcessor.java new file mode 100644 index 0000000000000..117ab09df430a --- /dev/null +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/logging/OpenTelemetryLogHandlerProcessor.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.logging.OpenTelemetryLogConfig; +import io.quarkus.opentelemetry.runtime.logging.OpenTelemetryLogRecorder; + +class OpenTelemetryLogHandlerProcessor { + + @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/logging/OpenTelemetryLogConfig.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logging/OpenTelemetryLogConfig.java new file mode 100644 index 0000000000000..84601b57c515d --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logging/OpenTelemetryLogConfig.java @@ -0,0 +1,14 @@ +package io.quarkus.opentelemetry.runtime.logging; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.RUN_TIME, name = "log.handler.open-telemetry") +public class OpenTelemetryLogConfig { + /** + * Determine whether to enable the OpenTelemetry logging handler + */ + @ConfigItem + public boolean enabled; +} diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logging/OpenTelemetryLogHandler.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logging/OpenTelemetryLogHandler.java new file mode 100644 index 0000000000000..ab36f36d09303 --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logging/OpenTelemetryLogHandler.java @@ -0,0 +1,55 @@ +package io.quarkus.opentelemetry.runtime.logging; + +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("quarkus-log-appender"); + } + + @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/logging/OpenTelemetryLogRecorder.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logging/OpenTelemetryLogRecorder.java new file mode 100644 index 0000000000000..f069fa4d5fe8d --- /dev/null +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logging/OpenTelemetryLogRecorder.java @@ -0,0 +1,21 @@ +package io.quarkus.opentelemetry.runtime.logging; + +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/integration-tests/opentelemetry-logging/pom.xml b/integration-tests/opentelemetry-logging/pom.xml new file mode 100644 index 0000000000000..47a85e2c21165 --- /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..1139e54d2a290 --- /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.exporter=otlp \ 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 e0b6a8a90dd0f..2d094d5ae83fd 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -427,6 +427,7 @@ mtls-certificates virtual-threads + opentelemetry-logging