diff --git a/bom/application/pom.xml b/bom/application/pom.xml index b533b37e0e7e0..ee50babfdd909 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -6279,6 +6279,16 @@ ${project.version} + + io.quarkus + quarkus-logging-opentelemetry + ${project.version} + + + io.quarkus + quarkus-logging-opentelemetry-deployment + ${project.version} + diff --git a/extensions/logging-opentelemetry/deployment/pom.xml b/extensions/logging-opentelemetry/deployment/pom.xml new file mode 100644 index 0000000000000..59f52582f89d2 --- /dev/null +++ b/extensions/logging-opentelemetry/deployment/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + + io.quarkus + quarkus-logging-opentelemetry-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-logging-opentelemetry-deployment + Quarkus - Logging - OpenTelemetry - Deployment + + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-logging-opentelemetry + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/logging-opentelemetry/deployment/src/main/java/io/quarkus/logging/opentelemetry/deployment/OpenTelemetryLogHandlerProcessor.java b/extensions/logging-opentelemetry/deployment/src/main/java/io/quarkus/logging/opentelemetry/deployment/OpenTelemetryLogHandlerProcessor.java new file mode 100644 index 0000000000000..e0f335e21a2d0 --- /dev/null +++ b/extensions/logging-opentelemetry/deployment/src/main/java/io/quarkus/logging/opentelemetry/deployment/OpenTelemetryLogHandlerProcessor.java @@ -0,0 +1,25 @@ +package io.quarkus.logging.opentelemetry.deployment; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.LogHandlerBuildItem; +import io.quarkus.logging.opentelemetry.runtime.OpenTelemetryLogConfig; +import io.quarkus.logging.opentelemetry.runtime.OpenTelemetryLogRecorder; + +class OpenTelemetryLogHandlerProcessor { + + private static final String FEATURE = "logging-opentelemetry"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + @Record(ExecutionTime.RUNTIME_INIT) + LogHandlerBuildItem build(OpenTelemetryLogRecorder recorder, OpenTelemetryLogConfig config) { + return new LogHandlerBuildItem(recorder.initializeHandler(config)); + } +} diff --git a/extensions/logging-opentelemetry/deployment/src/test/java/io/quarkus/logging/opentelemetry/test/LoggingOpentelemetryDevModeTest.java b/extensions/logging-opentelemetry/deployment/src/test/java/io/quarkus/logging/opentelemetry/test/LoggingOpentelemetryDevModeTest.java new file mode 100644 index 0000000000000..8b7315bf51e5f --- /dev/null +++ b/extensions/logging-opentelemetry/deployment/src/test/java/io/quarkus/logging/opentelemetry/test/LoggingOpentelemetryDevModeTest.java @@ -0,0 +1,23 @@ +package io.quarkus.logging.opentelemetry.test; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; + +public class LoggingOpentelemetryDevModeTest { + + // Start hot reload (DevMode) test with your extension loaded + @RegisterExtension + static final QuarkusDevModeTest devModeTest = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Test + public void writeYourOwnDevModeTest() { + // Write your dev mode tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-hot-reload for more information + Assertions.assertTrue(true, "Add dev mode assertions to " + getClass().getName()); + } +} diff --git a/extensions/logging-opentelemetry/deployment/src/test/java/io/quarkus/logging/opentelemetry/test/LoggingOpentelemetryTest.java b/extensions/logging-opentelemetry/deployment/src/test/java/io/quarkus/logging/opentelemetry/test/LoggingOpentelemetryTest.java new file mode 100644 index 0000000000000..0102b7277842b --- /dev/null +++ b/extensions/logging-opentelemetry/deployment/src/test/java/io/quarkus/logging/opentelemetry/test/LoggingOpentelemetryTest.java @@ -0,0 +1,23 @@ +package io.quarkus.logging.opentelemetry.test; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class LoggingOpentelemetryTest { + + // Start unit test with your extension loaded + @RegisterExtension + static final QuarkusUnitTest unitTest = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + + @Test + public void writeYourOwnUnitTest() { + // Write your unit tests here - see the testing extension guide https://quarkus.io/guides/writing-extensions#testing-extensions for more information + Assertions.assertTrue(true, "Add some assertions to " + getClass().getName()); + } +} diff --git a/extensions/logging-opentelemetry/pom.xml b/extensions/logging-opentelemetry/pom.xml new file mode 100644 index 0000000000000..47cc7c281a82e --- /dev/null +++ b/extensions/logging-opentelemetry/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + io.quarkus + quarkus-extensions-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-logging-opentelemetry-parent + pom + Quarkus - Logging - OpenTelemetry + + + deployment + runtime + + diff --git a/extensions/logging-opentelemetry/runtime/pom.xml b/extensions/logging-opentelemetry/runtime/pom.xml new file mode 100644 index 0000000000000..f7a5e738f3b7e --- /dev/null +++ b/extensions/logging-opentelemetry/runtime/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + io.quarkus + quarkus-logging-opentelemetry-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-logging-opentelemetry + Quarkus - Logging - OpenTelemetry - Runtime + + + + io.quarkus + quarkus-arc + + + io.opentelemetry + opentelemetry-sdk + + + io.opentelemetry + opentelemetry-exporter-otlp + + + + + + + io.quarkus + quarkus-extension-maven-plugin + ${project.version} + + + compile + + extension-descriptor + + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/logging-opentelemetry/runtime/src/main/java/io/quarkus/logging/opentelemetry/runtime/OpenTelemetryLogConfig.java b/extensions/logging-opentelemetry/runtime/src/main/java/io/quarkus/logging/opentelemetry/runtime/OpenTelemetryLogConfig.java new file mode 100644 index 0000000000000..16642548302b3 --- /dev/null +++ b/extensions/logging-opentelemetry/runtime/src/main/java/io/quarkus/logging/opentelemetry/runtime/OpenTelemetryLogConfig.java @@ -0,0 +1,14 @@ +package io.quarkus.logging.opentelemetry.runtime; + +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/logging-opentelemetry/runtime/src/main/java/io/quarkus/logging/opentelemetry/runtime/OpenTelemetryLogHandler.java b/extensions/logging-opentelemetry/runtime/src/main/java/io/quarkus/logging/opentelemetry/runtime/OpenTelemetryLogHandler.java new file mode 100644 index 0000000000000..42ced472860b3 --- /dev/null +++ b/extensions/logging-opentelemetry/runtime/src/main/java/io/quarkus/logging/opentelemetry/runtime/OpenTelemetryLogHandler.java @@ -0,0 +1,57 @@ +package io.quarkus.logging.opentelemetry.runtime; + +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/logging-opentelemetry/runtime/src/main/java/io/quarkus/logging/opentelemetry/runtime/OpenTelemetryLogRecorder.java b/extensions/logging-opentelemetry/runtime/src/main/java/io/quarkus/logging/opentelemetry/runtime/OpenTelemetryLogRecorder.java new file mode 100644 index 0000000000000..47ad135f18765 --- /dev/null +++ b/extensions/logging-opentelemetry/runtime/src/main/java/io/quarkus/logging/opentelemetry/runtime/OpenTelemetryLogRecorder.java @@ -0,0 +1,21 @@ +package io.quarkus.logging.opentelemetry.runtime; + +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/logging-opentelemetry/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/logging-opentelemetry/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..e368439b7df6b --- /dev/null +++ b/extensions/logging-opentelemetry/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,9 @@ +name: Logging Opentelemetry +#description: Do something useful. +metadata: +# keywords: +# - logging-opentelemetry +# guide: ... # To create and publish this guide, see https://github.com/quarkiverse/quarkiverse/wiki#documenting-your-extension +# categories: +# - "miscellaneous" +# status: "preview" diff --git a/extensions/pom.xml b/extensions/pom.xml index f405528ad1782..97f509135bb98 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -193,6 +193,7 @@ logging-json logging-gelf + logging-opentelemetry qute diff --git a/integration-tests/logging-opentelemetry/pom.xml b/integration-tests/logging-opentelemetry/pom.xml new file mode 100644 index 0000000000000..66bacd249f2d2 --- /dev/null +++ b/integration-tests/logging-opentelemetry/pom.xml @@ -0,0 +1,135 @@ + + + 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-logging-opentelemetry + + + io.quarkus + quarkus-opentelemetry + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-logging-opentelemetry-deployment + ${project.version} + pom + 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/logging-opentelemetry/src/main/java/io/quarkus/logging/opentelemetry/it/LoggingOpentelemetryResource.java b/integration-tests/logging-opentelemetry/src/main/java/io/quarkus/logging/opentelemetry/it/LoggingOpentelemetryResource.java new file mode 100644 index 0000000000000..24c16c92465b6 --- /dev/null +++ b/integration-tests/logging-opentelemetry/src/main/java/io/quarkus/logging/opentelemetry/it/LoggingOpentelemetryResource.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 LoggingOpentelemetryResource { + private static final Logger LOG = LoggerFactory.getLogger(LoggingOpentelemetryResource.class); + + @GET + public String hello() { + LOG.info("Hello World"); + return "Hello logging-opentelemetry"; + } +} diff --git a/integration-tests/logging-opentelemetry/src/main/resources/application.properties b/integration-tests/logging-opentelemetry/src/main/resources/application.properties new file mode 100644 index 0000000000000..1139e54d2a290 --- /dev/null +++ b/integration-tests/logging-opentelemetry/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/logging-opentelemetry/src/test/java/io/quarkus/logging/opentelemetry/it/LoggingOpentelemetryResourceIT.java b/integration-tests/logging-opentelemetry/src/test/java/io/quarkus/logging/opentelemetry/it/LoggingOpentelemetryResourceIT.java new file mode 100644 index 0000000000000..32fc2570a1e32 --- /dev/null +++ b/integration-tests/logging-opentelemetry/src/test/java/io/quarkus/logging/opentelemetry/it/LoggingOpentelemetryResourceIT.java @@ -0,0 +1,7 @@ +package io.quarkus.logging.opentelemetry.it; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class LoggingOpentelemetryResourceIT extends LoggingOpentelemetryResourceTest { +} diff --git a/integration-tests/logging-opentelemetry/src/test/java/io/quarkus/logging/opentelemetry/it/LoggingOpentelemetryResourceTest.java b/integration-tests/logging-opentelemetry/src/test/java/io/quarkus/logging/opentelemetry/it/LoggingOpentelemetryResourceTest.java new file mode 100644 index 0000000000000..baf4003bc391e --- /dev/null +++ b/integration-tests/logging-opentelemetry/src/test/java/io/quarkus/logging/opentelemetry/it/LoggingOpentelemetryResourceTest.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 LoggingOpentelemetryResourceTest { + + @Test + public void testHelloEndpoint() { + given() + .when().get("/logging-opentelemetry") + .then() + .statusCode(200) + .body(is("Hello logging-opentelemetry")); + } +} diff --git a/integration-tests/logging-opentelemetry/src/test/resources/docker-compose.yml b/integration-tests/logging-opentelemetry/src/test/resources/docker-compose.yml new file mode 100644 index 0000000000000..29246fc7d173f --- /dev/null +++ b/integration-tests/logging-opentelemetry/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/logging-opentelemetry/src/test/resources/otel-collector-config.yaml b/integration-tests/logging-opentelemetry/src/test/resources/otel-collector-config.yaml new file mode 100644 index 0000000000000..b581b1b4c5485 --- /dev/null +++ b/integration-tests/logging-opentelemetry/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 c03b3703e342b..8c6a8c54e3454 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -423,6 +423,7 @@ mtls-certificates virtual-threads + logging-opentelemetry