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