diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc
index 68a47e2..7fd3602 100644
--- a/docs/modules/ROOT/nav.adoc
+++ b/docs/modules/ROOT/nav.adoc
@@ -1,3 +1,4 @@
* xref:index.adoc[Introduction]
* xref:quarkus-opentelemetry-exporter-azure.adoc[Quarkus Opentelemetry Exporter for Microsoft Azure]
* xref:quarkus-opentelemetry-exporter-gcp.adoc[Quarkus Opentelemetry Exporter for Google Cloud Platform]
+* xref:quarkus-opentelemetry-exporter-sentry.adoc[Quarkus Opentelemetry Exporter for Sentry]
\ No newline at end of file
diff --git a/docs/modules/ROOT/pages/quarkus-opentelemetry-exporter-sentry.adoc b/docs/modules/ROOT/pages/quarkus-opentelemetry-exporter-sentry.adoc
new file mode 100644
index 0000000..cfc861a
--- /dev/null
+++ b/docs/modules/ROOT/pages/quarkus-opentelemetry-exporter-sentry.adoc
@@ -0,0 +1,47 @@
+= Quarkus Opentelemetry Exporter for Sentry
+
+include::./includes/attributes.adoc[]
+
+This exporter sends data to sentry.
+
+== General configuration
+
+Add the https://mvnrepository.com/artifact/io.quarkiverse.opentelemetry.exporter/quarkus-opentelemetry-exporter-sentry[Sentry exporter extension] to your build file.
+
+For Maven:
+
+[source,xml,subs=attributes+]
+----
+
+ io.quarkiverse.opentelemetry.exporter
+ quarkus-opentelemetry-exporter-sentry
+ {project-version}
+
+----
+
+You also need a sentry project to receive the telemetry data. Go to the sentry portal, search for your project or create a new one. On the overview page of your project, you will find a DSN https://docs.sentry.io/concepts/key-terms/dsn-explainer[in the top right corner].
+
+You can then set the dsn in your project configuration:
+
+* With the `application.properties` file
+
+[source]
+----
+quarkus.otel.sentry.dsn=your_dsn
+----
+
+* With the `QUARKUS_OTEL_SENTRY_DSN=your_dsn` environment variable
+
+
+Read https://quarkus.io/guides/opentelemetry#configuration-reference[this page] to learn more configuration options.
+
+== Enable more instrumentation
+
+* Read https://quarkus.io/guides/opentelemetry#jdbc[this documentation] to enable the JDBC instrumentation
+* Read https://quarkus.io/guides/opentelemetry#additional-instrumentation[this documentation] to enable additional instrumentations
+
+
+[[extension-configuration-reference]]
+== Extension Configuration Reference
+
+include::includes/quarkus-opentelemetry-tracer-exporter-sentry.adoc[leveloffset=+1, opts=optional]
\ No newline at end of file
diff --git a/docs/pom.xml b/docs/pom.xml
index 7926fc9..f9bdd26 100644
--- a/docs/pom.xml
+++ b/docs/pom.xml
@@ -40,6 +40,19 @@
+
+ io.quarkiverse.opentelemetry.exporter
+ quarkus-opentelemetry-exporter-sentry-deployment
+ ${project.version}
+ pom
+ test
+
+
+ *
+ *
+
+
+
@@ -56,6 +69,14 @@
+
+ io.quarkus
+ quarkus-config-doc-maven-plugin
+ true
+
+ ${project.basedir}/modules/ROOT/pages/includes/
+
+
it.ozimov
yaml-properties-maven-plugin
@@ -73,14 +94,6 @@
-
- io.quarkus
- quarkus-config-doc-maven-plugin
- true
-
- ${project.basedir}/modules/ROOT/pages/includes/
-
-
maven-resources-plugin
@@ -122,6 +135,9 @@
org.asciidoctor
asciidoctor-maven-plugin
+
+ true
+
diff --git a/pom.xml b/pom.xml
index bfa8c37..dc8fd9d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,6 +17,7 @@
quarkus-opentelemetry-exporter-common
quarkus-opentelemetry-exporter-azure
quarkus-opentelemetry-exporter-gcp
+ quarkus-opentelemetry-exporter-sentry
diff --git a/quarkus-opentelemetry-exporter-sentry/deployment/pom.xml b/quarkus-opentelemetry-exporter-sentry/deployment/pom.xml
new file mode 100644
index 0000000..087aef3
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/deployment/pom.xml
@@ -0,0 +1,75 @@
+
+
+ 4.0.0
+
+
+ io.quarkiverse.opentelemetry.exporter
+ quarkus-opentelemetry-exporter-sentry-parent
+ 999-SNAPSHOT
+
+
+ quarkus-opentelemetry-exporter-sentry-deployment
+ Quarkus Opentelemetry Exporter Sentry - Deployment
+
+
+
+ io.quarkiverse.opentelemetry.exporter
+ quarkus-opentelemetry-exporter-sentry
+ ${project.version}
+
+
+ io.quarkus
+ quarkus-core-deployment
+
+
+ io.quarkus
+ quarkus-arc-deployment
+
+
+ io.quarkus
+ quarkus-opentelemetry-deployment
+
+
+ io.sentry
+ sentry-opentelemetry-core
+
+
+ io.quarkus
+ quarkus-junit5-internal
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+ org.assertj
+ assertj-core
+ ${assertj-core.version}
+ test
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${quarkus.version}
+
+
+
+
+
+
+
diff --git a/quarkus-opentelemetry-exporter-sentry/deployment/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/deployment/SentryProcessor.java b/quarkus-opentelemetry-exporter-sentry/deployment/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/deployment/SentryProcessor.java
new file mode 100644
index 0000000..b6afe79
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/deployment/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/deployment/SentryProcessor.java
@@ -0,0 +1,65 @@
+package io.quarkiverse.opentelemetry.exporter.sentry.deployment;
+
+import static io.quarkus.deployment.Capability.REST;
+import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;
+
+import java.util.function.BooleanSupplier;
+
+import io.quarkiverse.opentelemetry.exporter.sentry.beans.SentrySpanProcessorProducer;
+import io.quarkiverse.opentelemetry.exporter.sentry.config.SentryConfig;
+import io.quarkiverse.opentelemetry.exporter.sentry.config.SentryConfig.SentryExporterRuntimeConfig;
+import io.quarkiverse.opentelemetry.exporter.sentry.filters.SentryFilter;
+import io.quarkiverse.opentelemetry.exporter.sentry.recorders.SentryRecorder;
+import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
+import io.quarkus.deployment.Capabilities;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.BuildSteps;
+import io.quarkus.deployment.annotations.Record;
+import io.quarkus.deployment.builditem.FeatureBuildItem;
+import io.quarkus.deployment.builditem.LogHandlerBuildItem;
+import io.quarkus.opentelemetry.deployment.exporter.otlp.ExternalOtelExporterBuildItem;
+
+@BuildSteps(onlyIf = SentryProcessor.SentryExporterEnabled.class)
+public final class SentryProcessor {
+
+ static class SentryExporterEnabled implements BooleanSupplier {
+ SentryConfig.SentryExporterBuildConfig sentryExporterConfig;
+
+ public boolean getAsBoolean() {
+ return sentryExporterConfig.enabled();
+ }
+ }
+
+ private static final String FEATURE = "sentry";
+
+ @BuildStep
+ FeatureBuildItem feature() {
+ return new FeatureBuildItem(FEATURE);
+ }
+
+ @BuildStep
+ void registerExternalExporter(BuildProducer buildProducer) {
+ buildProducer.produce(new ExternalOtelExporterBuildItem("sentry"));
+ }
+
+ @BuildStep
+ @Record(RUNTIME_INIT)
+ LogHandlerBuildItem addSentryHandler(final SentryExporterRuntimeConfig config, final SentryRecorder recorder) {
+ return new LogHandlerBuildItem(recorder.create(config));
+ }
+
+ @BuildStep
+ void additionalBeanSentryFilter(Capabilities capabilities, BuildProducer producer) {
+ if (!capabilities.isPresent(REST)) {
+ return;
+ }
+ producer.produce(AdditionalBeanBuildItem.builder().addBeanClass(SentryFilter.class).build());
+ }
+
+ @BuildStep
+ AdditionalBeanBuildItem additionalBeanProducers() {
+ return AdditionalBeanBuildItem.builder()
+ .addBeanClass(SentrySpanProcessorProducer.class).build();
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/deployment/src/test/java/io/quarkiverse/opentelemetry/exporter/sentry/deployment/SentryExporterDisabledTest.java b/quarkus-opentelemetry-exporter-sentry/deployment/src/test/java/io/quarkiverse/opentelemetry/exporter/sentry/deployment/SentryExporterDisabledTest.java
new file mode 100644
index 0000000..f3670f7
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/deployment/src/test/java/io/quarkiverse/opentelemetry/exporter/sentry/deployment/SentryExporterDisabledTest.java
@@ -0,0 +1,32 @@
+package io.quarkiverse.opentelemetry.exporter.sentry.deployment;
+
+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.test.QuarkusUnitTest;
+import io.sentry.opentelemetry.SentrySpanProcessor;
+
+public class SentryExporterDisabledTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .withEmptyApplication()
+ .overrideConfigKey("quarkus.otel.sentry.enabled", "false");
+
+ @Inject
+ OpenTelemetry openTelemetry;
+
+ @Inject
+ Instance sentrySpanProcessorInstance;
+
+ @Test
+ void testOpenTelemetryButNoSpanProcessor() {
+ Assertions.assertNotNull(openTelemetry);
+ Assertions.assertFalse(sentrySpanProcessorInstance.isResolvable());
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/deployment/src/test/java/io/quarkiverse/opentelemetry/exporter/sentry/deployment/SentryExporterEnabledTest.java b/quarkus-opentelemetry-exporter-sentry/deployment/src/test/java/io/quarkiverse/opentelemetry/exporter/sentry/deployment/SentryExporterEnabledTest.java
new file mode 100644
index 0000000..8c85701
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/deployment/src/test/java/io/quarkiverse/opentelemetry/exporter/sentry/deployment/SentryExporterEnabledTest.java
@@ -0,0 +1,33 @@
+package io.quarkiverse.opentelemetry.exporter.sentry.deployment;
+
+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.opentelemetry.sdk.trace.SpanProcessor;
+import io.quarkus.test.QuarkusUnitTest;
+
+public class SentryExporterEnabledTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .withEmptyApplication()
+ .overrideConfigKey("quarkus.otel.sentry.sentry.enabled", "true")
+ .overrideConfigKey("quarkus.otel.sentry.dsn", "https://1234@test/1234");
+
+ @Inject
+ OpenTelemetry openTelemetry;
+
+ @Inject
+ Instance sentrySpanProcessorInstance;
+
+ @Test
+ void testOpenTelemetryButNoSpanProcessor() {
+ Assertions.assertNotNull(openTelemetry);
+ Assertions.assertTrue(sentrySpanProcessorInstance.isResolvable());
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/integration-tests/pom.xml b/quarkus-opentelemetry-exporter-sentry/integration-tests/pom.xml
new file mode 100644
index 0000000..24942b2
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/integration-tests/pom.xml
@@ -0,0 +1,122 @@
+
+
+ 4.0.0
+
+ io.quarkiverse.opentelemetry.exporter
+ quarkus-opentelemetry-exporter-sentry-parent
+ 999-SNAPSHOT
+
+
+ quarkus-opentelemetry-exporter-sentry-integration-tests
+ Quarkus Opentelemetry Exporter Sentry - Integration Tests
+
+
+ true
+
+
+
+
+ io.quarkus
+ quarkus-resteasy-jackson
+
+
+ io.quarkiverse.opentelemetry.exporter
+ quarkus-opentelemetry-exporter-sentry
+ ${project.version}
+
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ ${testcontainers-junit-jupiter.version}
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ org.awaitility
+ awaitility
+ test
+
+
+ io.quarkiverse.wiremock
+ quarkus-wiremock-test
+ ${quarkus-wiremock-test.version}
+ test
+
+
+ org.assertj
+ assertj-core
+ ${assertj-core.version}
+ 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/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/SimpleResource.java b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/SimpleResource.java
new file mode 100644
index 0000000..abd75a0
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/SimpleResource.java
@@ -0,0 +1,70 @@
+package io.quarkiverse.opentelemetry.exporter.it;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+import io.quarkus.logging.Log;
+
+@Path("")
+@Produces(MediaType.APPLICATION_JSON)
+public class SimpleResource {
+
+ @Inject
+ TracedService tracedService;
+
+ @GET
+ public TraceData noPath() {
+ TraceData data = new TraceData();
+ data.message = "No path trace";
+ return data;
+ }
+
+ @GET
+ @Path("/direct")
+ public TraceData directTrace() {
+ TraceData data = new TraceData();
+ data.message = "Direct trace";
+
+ return data;
+ }
+
+ @GET
+ @Path("/chained")
+ public TraceData chainedTrace() {
+ TraceData data = new TraceData();
+ data.message = tracedService.call();
+
+ return data;
+ }
+
+ @GET
+ @Path("/logged")
+ public TraceData loggedTrace() {
+ TraceData data = new TraceData();
+ data.message = "Logged trace";
+ Log.error("This is a logged message");
+ return data;
+ }
+
+ @GET
+ @Path("/deep/path")
+ public TraceData deepUrlPathTrace() {
+ TraceData data = new TraceData();
+ data.message = "Deep url path";
+
+ return data;
+ }
+
+ @GET
+ @Path("/param/{paramId}")
+ public TraceData pathParameters(@PathParam("paramId") String paramId) {
+ TraceData data = new TraceData();
+ data.message = "ParameterId: " + paramId;
+
+ return data;
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/TraceData.java b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/TraceData.java
new file mode 100644
index 0000000..b59319c
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/TraceData.java
@@ -0,0 +1,5 @@
+package io.quarkiverse.opentelemetry.exporter.it;
+
+public class TraceData {
+ public String message;
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/TracedService.java b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/TracedService.java
new file mode 100644
index 0000000..30cc72a
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/TracedService.java
@@ -0,0 +1,13 @@
+package io.quarkiverse.opentelemetry.exporter.it;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import io.opentelemetry.instrumentation.annotations.WithSpan;
+
+@ApplicationScoped
+public class TracedService {
+ @WithSpan
+ public String call() {
+ return "Chained trace";
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/output/SpanDataModuleSerializer.java b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/output/SpanDataModuleSerializer.java
new file mode 100644
index 0000000..e7fede1
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/output/SpanDataModuleSerializer.java
@@ -0,0 +1,19 @@
+package io.quarkiverse.opentelemetry.exporter.it.output;
+
+import jakarta.inject.Singleton;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.quarkus.jackson.ObjectMapperCustomizer;
+
+@Singleton
+public class SpanDataModuleSerializer implements ObjectMapperCustomizer {
+ @Override
+ public void customize(ObjectMapper objectMapper) {
+ SimpleModule simpleModule = new SimpleModule();
+ simpleModule.addSerializer(SpanData.class, new SpanDataSerializer());
+ objectMapper.registerModule(simpleModule);
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/output/SpanDataSerializer.java b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/output/SpanDataSerializer.java
new file mode 100644
index 0000000..29fe780
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/output/SpanDataSerializer.java
@@ -0,0 +1,54 @@
+package io.quarkiverse.opentelemetry.exporter.it.output;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+import io.opentelemetry.sdk.trace.data.SpanData;
+
+public class SpanDataSerializer extends StdSerializer {
+ public SpanDataSerializer() {
+ this(null);
+ }
+
+ public SpanDataSerializer(Class type) {
+ super(type);
+ }
+
+ @Override
+ public void serialize(SpanData spanData, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
+ throws IOException {
+ jsonGenerator.writeStartObject();
+
+ jsonGenerator.writeStringField("spanId", spanData.getSpanId());
+ jsonGenerator.writeStringField("traceId", spanData.getTraceId());
+ jsonGenerator.writeStringField("name", spanData.getName());
+ jsonGenerator.writeStringField("kind", spanData.getKind().name());
+ jsonGenerator.writeBooleanField("ended", spanData.hasEnded());
+
+ jsonGenerator.writeStringField("parent_spanId", spanData.getParentSpanContext().getSpanId());
+ jsonGenerator.writeStringField("parent_traceId", spanData.getParentSpanContext().getTraceId());
+ jsonGenerator.writeBooleanField("parent_remote", spanData.getParentSpanContext().isRemote());
+ jsonGenerator.writeBooleanField("parent_valid", spanData.getParentSpanContext().isValid());
+
+ spanData.getAttributes().forEach((k, v) -> {
+ try {
+ jsonGenerator.writeStringField("attr_" + k.getKey(), v.toString());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+
+ spanData.getResource().getAttributes().forEach((k, v) -> {
+ try {
+ jsonGenerator.writeStringField("resource_" + k.getKey(), v.toString());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+
+ jsonGenerator.writeEndObject();
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/resources/application.properties b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/resources/application.properties
new file mode 100644
index 0000000..2c18707
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+quarkus.otel.sentry.dsn=https://b1cb3f24f3545258505c835c791a740dbd1469c7706322e595a84a09f4cf913c@localhost:53602/1234
+quarkus.otel.sentry.spotlight-connection-url=http://localhost:53602
\ No newline at end of file
diff --git a/quarkus-opentelemetry-exporter-sentry/integration-tests/src/test/java/io/quarkiverse/opentelemetry/exporter/it/SentryTest.java b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/test/java/io/quarkiverse/opentelemetry/exporter/it/SentryTest.java
new file mode 100644
index 0000000..c6d0040
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/integration-tests/src/test/java/io/quarkiverse/opentelemetry/exporter/it/SentryTest.java
@@ -0,0 +1,138 @@
+package io.quarkiverse.opentelemetry.exporter.it;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static io.restassured.RestAssured.given;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import com.github.tomakehurst.wiremock.verification.LoggedRequest;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+@QuarkusTest
+public class SentryTest {
+
+ public static final int HTTP_PORT_NUMBER = 53602; // See application.properties file
+ private static final ObjectMapper mapper = new ObjectMapper();
+ private WireMockServer wireMockServer;
+
+ @BeforeEach
+ public void startWireMock() {
+ WireMockConfiguration wireMockConfiguration = new WireMockConfiguration().port(HTTP_PORT_NUMBER);
+ wireMockServer = new WireMockServer(wireMockConfiguration);
+ wireMockServer.start();
+ }
+
+ @AfterEach
+ public void stopWireMock() {
+ wireMockServer.stop();
+ }
+
+ @Test
+ void traceTest() {
+
+ wireMockServer.stubFor(
+ any(urlMatching(".*"))
+ .withPort(HTTP_PORT_NUMBER)
+ .willReturn(aResponse().withStatus(200)));
+
+ given()
+ .contentType("application/json")
+ .when().get("/direct")
+ .then()
+ .statusCode(200)
+ .body("message", equalTo("Direct trace"));
+
+ await()
+ .atMost(Duration.ofSeconds(2))
+ .until(telemetryDataContainTheHttpCall(wireMockServer, 1));
+
+ List> requestBodies = getRequestBodies();
+
+ assertThat(requestBodies).hasSize(1)
+ .satisfiesOnlyOnce(group -> {
+ assertThat(group).hasSize(3);
+ assertThat(group.get(0).get("sdk").get("name").asText()).isEqualTo("sentry.java");
+ assertThat(group.get(1).get("type").asText()).isEqualTo("transaction");
+ assertThat(group.get(2).get("transaction").asText()).isEqualTo("GET /direct");
+ });
+
+ }
+
+ @Test
+ void loggedTest() {
+
+ wireMockServer.stubFor(
+ any(urlMatching(".*"))
+ .withPort(HTTP_PORT_NUMBER)
+ .willReturn(aResponse().withStatus(200)));
+
+ given()
+ .contentType("application/json")
+ .when().get("/logged")
+ .then()
+ .statusCode(200)
+ .body("message", equalTo("Logged trace"));
+
+ await()
+ .atMost(Duration.ofSeconds(2))
+ .until(telemetryDataContainTheHttpCall(wireMockServer, 2));
+
+ List> requestBodies = getRequestBodies();
+
+ assertThat(requestBodies).hasSize(2);
+
+ assertThat(requestBodies).satisfiesOnlyOnce(group -> {
+ assertThat(group).hasSize(3);
+ assertThat(group.get(0).get("sdk").get("name").asText()).isEqualTo("sentry.java");
+ assertThat(group.get(1).get("type").asText()).isEqualTo("event");
+ assertThat(group.get(2).get("message").get("message").asText()).isEqualTo("This is a logged message");
+ })
+ .satisfiesOnlyOnce(group -> {
+ assertThat(group).hasSize(3);
+ assertThat(group.get(0).get("sdk").get("name").asText()).isEqualTo("sentry.java");
+ assertThat(group.get(1).get("type").asText()).isEqualTo("transaction");
+ assertThat(group.get(2).get("transaction").asText()).isEqualTo("GET /logged");
+ });
+
+ }
+
+ private @NotNull List> getRequestBodies() {
+ List telemetryExport = wireMockServer.findAll(postRequestedFor(anyUrl()));
+ List> requestBodies = telemetryExport
+ .stream()
+ .map(request -> {
+ try {
+ List parsed = new ArrayList<>();
+ String[] messages = new String(request.getBody()).split("\n");
+ for (String message : messages) {
+ parsed.add(mapper.readTree(message));
+ }
+ return parsed;
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }).toList();
+ return requestBodies;
+ }
+
+ private static Callable telemetryDataContainTheHttpCall(WireMockServer wireMockServer, int size) {
+ return () -> wireMockServer.findAll(postRequestedFor(anyUrl())).size() == size;
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/pom.xml b/quarkus-opentelemetry-exporter-sentry/pom.xml
new file mode 100644
index 0000000..f78d83c
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/pom.xml
@@ -0,0 +1,48 @@
+
+
+ 4.0.0
+
+ io.quarkiverse.opentelemetry.exporter
+ quarkus-opentelemetry-exporter-parent
+ 999-SNAPSHOT
+
+
+ quarkus-opentelemetry-exporter-sentry-parent
+ pom
+ Quarkus Opentelemetry Exporter - Sentry
+
+
+ runtime
+ deployment
+
+
+
+ 7.16.0
+
+
+
+
+ io.sentry
+ sentry-bom
+ ${sentry.version}
+ pom
+ import
+
+
+
+
+
+ it
+
+
+ performRelease
+ !true
+
+
+
+ integration-tests
+
+
+
+
diff --git a/quarkus-opentelemetry-exporter-sentry/runtime/pom.xml b/quarkus-opentelemetry-exporter-sentry/runtime/pom.xml
new file mode 100644
index 0000000..fa986e7
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/runtime/pom.xml
@@ -0,0 +1,78 @@
+
+
+ 4.0.0
+
+ io.quarkiverse.opentelemetry.exporter
+ quarkus-opentelemetry-exporter-sentry-parent
+ 999-SNAPSHOT
+
+
+ quarkus-opentelemetry-exporter-sentry
+ Quarkus Opentelemetry Exporter Sentry - Runtime
+
+
+
+ io.quarkiverse.opentelemetry.exporter
+ quarkus-opentelemetry-exporter-common
+
+
+
+ io.quarkus
+ quarkus-core
+
+
+ io.quarkus
+ quarkus-arc
+
+
+
+ io.quarkus
+ quarkus-opentelemetry
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+
+
+ io.sentry
+ sentry-opentelemetry-core
+
+
+ io.sentry
+ sentry-jul
+
+
+
+
+
+
+ io.quarkus
+ quarkus-extension-maven-plugin
+ ${quarkus.version}
+
+
+ compile
+
+ extension-descriptor
+
+
+ ${project.groupId}:${project.artifactId}-deployment:${project.version}
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${quarkus.version}
+
+
+
+
+
+
+
diff --git a/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/beans/SentrySpanProcessorProducer.java b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/beans/SentrySpanProcessorProducer.java
new file mode 100644
index 0000000..37104fa
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/beans/SentrySpanProcessorProducer.java
@@ -0,0 +1,15 @@
+package io.quarkiverse.opentelemetry.exporter.sentry.beans;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Produces;
+
+import io.sentry.opentelemetry.SentrySpanProcessor;
+
+public final class SentrySpanProcessorProducer {
+ @SuppressWarnings("unused")
+ @ApplicationScoped
+ @Produces
+ public SentrySpanProcessor produceSentrySpanProcessor() {
+ return new SentrySpanProcessor();
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/config/SentryConfig.java b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/config/SentryConfig.java
new file mode 100644
index 0000000..909db8a
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/config/SentryConfig.java
@@ -0,0 +1,58 @@
+package io.quarkiverse.opentelemetry.exporter.sentry.config;
+
+import java.util.List;
+import java.util.Optional;
+
+import io.quarkus.runtime.annotations.ConfigPhase;
+import io.quarkus.runtime.annotations.ConfigRoot;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+public class SentryConfig {
+
+ @ConfigMapping(prefix = "quarkus.otel.sentry")
+ @ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
+ public interface SentryExporterBuildConfig {
+ /**
+ * Sentry's Tracing exporter support. Enabled by default.
+ */
+ @WithDefault(value = "true")
+ Boolean enabled();
+ }
+
+ @ConfigMapping(prefix = "quarkus.otel.sentry")
+ @ConfigRoot(phase = ConfigPhase.RUN_TIME)
+ public interface SentryExporterRuntimeConfig {
+
+ /**
+ * Sentry Data Source Name.
+ */
+ Optional dsn();
+
+ /**
+ * Environment the events are tagged with.
+ */
+ Optional environment();
+
+ /**
+ * Percentage of performance events sent to Sentry.
+ */
+ @WithDefault("1.0")
+ Optional tracesSampleRate();
+
+ /**
+ * Packages to flag as In App.
+ */
+ Optional> inAppPackages();
+
+ /**
+ * Sentry Spotlight connection URL.
+ */
+ Optional spotlightConnectionUrl();
+
+ /**
+ * Enable debug mode.
+ */
+ Optional debug();
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/filters/SentryFilter.java b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/filters/SentryFilter.java
new file mode 100644
index 0000000..4b5e459
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/filters/SentryFilter.java
@@ -0,0 +1,50 @@
+package io.quarkiverse.opentelemetry.exporter.sentry.filters;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import jakarta.ws.rs.container.ContainerRequestContext;
+import jakarta.ws.rs.container.ContainerRequestFilter;
+import jakarta.ws.rs.container.DynamicFeature;
+import jakarta.ws.rs.container.ResourceInfo;
+import jakarta.ws.rs.core.FeatureContext;
+import jakarta.ws.rs.ext.Provider;
+
+import io.sentry.Sentry;
+
+@SuppressWarnings("unused")
+@Provider
+public final class SentryFilter implements ContainerRequestFilter, DynamicFeature {
+ @Override
+ public void filter(final ContainerRequestContext context) {
+ final var method = context.getMethod();
+ final var uriInfo = context.getUriInfo();
+ final var sentryRequest = new io.sentry.protocol.Request();
+ sentryRequest.setApiTarget("rest");
+ sentryRequest.setMethod(method);
+ sentryRequest.setUrl(uriInfo.getRequestUri().toString());
+ sentryRequest.setQueryString(
+ uriInfo.getQueryParameters().entrySet().stream()
+ .map(entry -> entry.getKey() + '=' + String.join(",", entry.getValue()))
+ .collect(Collectors.joining("&")));
+ sentryRequest.setHeaders(
+ context.getHeaders().entrySet().stream()
+ .collect(
+ Collectors.toMap(Map.Entry::getKey, entry -> String.join(";", entry.getValue()))));
+ sentryRequest.setCookies(
+ context.getCookies().entrySet().stream()
+ .map(entry -> entry.getKey() + '=' + entry.getValue().getValue())
+ .collect(Collectors.joining(";")));
+
+ Sentry.configureScope(
+ scope -> {
+ scope.setTransaction(method + ' ' + uriInfo.getPath());
+ scope.setRequest(sentryRequest);
+ });
+ }
+
+ @Override
+ public void configure(final ResourceInfo resourceInfo, final FeatureContext context) {
+ context.register(this);
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/otel/SentryPropagatorProvider.java b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/otel/SentryPropagatorProvider.java
new file mode 100644
index 0000000..e32881e
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/otel/SentryPropagatorProvider.java
@@ -0,0 +1,18 @@
+package io.quarkiverse.opentelemetry.exporter.sentry.otel;
+
+import io.opentelemetry.context.propagation.TextMapPropagator;
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider;
+import io.sentry.opentelemetry.SentryPropagator;
+
+public class SentryPropagatorProvider implements ConfigurablePropagatorProvider {
+ @Override
+ public TextMapPropagator getPropagator(ConfigProperties configProperties) {
+ return new SentryPropagator();
+ }
+
+ @Override
+ public String getName() {
+ return "sentry";
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/recorders/SentryRecorder.java b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/recorders/SentryRecorder.java
new file mode 100644
index 0000000..a467027
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/recorders/SentryRecorder.java
@@ -0,0 +1,56 @@
+package io.quarkiverse.opentelemetry.exporter.sentry.recorders;
+
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+
+import org.eclipse.microprofile.config.ConfigProvider;
+
+import io.quarkiverse.opentelemetry.exporter.sentry.config.SentryConfig.SentryExporterRuntimeConfig;
+import io.quarkus.runtime.RuntimeValue;
+import io.quarkus.runtime.annotations.Recorder;
+import io.sentry.Instrumenter;
+import io.sentry.Sentry;
+import io.sentry.SentryOptions;
+import io.sentry.jul.SentryHandler;
+import io.sentry.opentelemetry.OpenTelemetryLinkErrorEventProcessor;
+
+@Recorder
+public class SentryRecorder {
+ public RuntimeValue> create(final SentryExporterRuntimeConfig sentryConfig) {
+ if (sentryConfig.dsn().isEmpty()) {
+ return new RuntimeValue<>(Optional.empty());
+ }
+
+ final var config = ConfigProvider.getConfig();
+ final var appName = config.getValue("quarkus.application.name", String.class);
+ final var appVersion = config.getValue("quarkus.application.version", String.class);
+ final var options = new AtomicReference();
+
+ Sentry.init(
+ it -> {
+
+ sentryConfig.dsn().ifPresent(it::setDsn);
+ sentryConfig.environment().ifPresent(it::setEnvironment);
+ sentryConfig.tracesSampleRate().ifPresent(it::setTracesSampleRate);
+ sentryConfig.inAppPackages().ifPresent(packages -> packages.forEach(it::addInAppInclude));
+ sentryConfig.debug().ifPresent(it::setDebug);
+ sentryConfig.spotlightConnectionUrl().ifPresent(url -> {
+ it.setSpotlightConnectionUrl(url);
+ it.setEnableSpotlight(true);
+ });
+ it.setRelease(appName + '@' + appVersion);
+ it.setInstrumenter(Instrumenter.OTEL);
+ it.addEventProcessor(new OpenTelemetryLinkErrorEventProcessor());
+ options.set(it);
+ });
+
+ final var handler = new SentryHandler(options.get());
+ handler.setPrintfStyle(true);
+ handler.setLevel(Level.WARNING);
+ handler.setMinimumEventLevel(Level.WARNING);
+ handler.setMinimumBreadcrumbLevel(Level.INFO);
+ return new RuntimeValue<>(Optional.of(handler));
+ }
+}
diff --git a/quarkus-opentelemetry-exporter-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider
new file mode 100644
index 0000000..8fa0fbb
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider
@@ -0,0 +1 @@
+io.quarkiverse.opentelemetry.exporter.sentry.otel.SentryPropagatorProvider
diff --git a/quarkus-opentelemetry-exporter-sentry/runtime/src/main/resources/application.properties b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/resources/application.properties
new file mode 100644
index 0000000..d9bf47b
--- /dev/null
+++ b/quarkus-opentelemetry-exporter-sentry/runtime/src/main/resources/application.properties
@@ -0,0 +1 @@
+quarkus.otel.propagators=tracecontext,baggage,sentry