From 301302aac09da03613475516a29b8990aeb5b837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hermann?= Date: Wed, 6 Nov 2024 11:54:21 -0500 Subject: [PATCH 1/3] sentry exporter --- docs/modules/ROOT/nav.adoc | 1 + ...quarkus-opentelemetry-exporter-sentry.adoc | 47 ++++++ pom.xml | 1 + .../deployment/pom.xml | 75 ++++++++++ .../sentry/deployment/SentryProcessor.java | 65 +++++++++ .../SentryExporterDisabledTest.java | 32 ++++ .../deployment/SentryExporterEnabledTest.java | 33 +++++ .../integration-tests/pom.xml | 122 ++++++++++++++++ .../exporter/it/SimpleResource.java | 70 +++++++++ .../opentelemetry/exporter/it/TraceData.java | 5 + .../exporter/it/TracedService.java | 13 ++ .../it/output/SpanDataModuleSerializer.java | 19 +++ .../it/output/SpanDataSerializer.java | 54 +++++++ .../src/main/resources/application.properties | 2 + .../opentelemetry/exporter/it/SentryTest.java | 138 ++++++++++++++++++ quarkus-opentelemetry-exporter-sentry/pom.xml | 48 ++++++ .../runtime/pom.xml | 78 ++++++++++ .../beans/SentrySpanProcessorProducer.java | 15 ++ .../exporter/sentry/config/SentryConfig.java | 58 ++++++++ .../exporter/sentry/filters/SentryFilter.java | 50 +++++++ .../sentry/otel/SentryPropagatorProvider.java | 18 +++ .../sentry/recorders/SentryRecorder.java | 56 +++++++ ...nfigure.spi.ConfigurablePropagatorProvider | 1 + .../src/main/resources/application.properties | 1 + 24 files changed, 1002 insertions(+) create mode 100644 docs/modules/ROOT/pages/quarkus-opentelemetry-exporter-sentry.adoc create mode 100644 quarkus-opentelemetry-exporter-sentry/deployment/pom.xml create mode 100644 quarkus-opentelemetry-exporter-sentry/deployment/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/deployment/SentryProcessor.java create mode 100644 quarkus-opentelemetry-exporter-sentry/deployment/src/test/java/io/quarkiverse/opentelemetry/exporter/sentry/deployment/SentryExporterDisabledTest.java create mode 100644 quarkus-opentelemetry-exporter-sentry/deployment/src/test/java/io/quarkiverse/opentelemetry/exporter/sentry/deployment/SentryExporterEnabledTest.java create mode 100644 quarkus-opentelemetry-exporter-sentry/integration-tests/pom.xml create mode 100644 quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/SimpleResource.java create mode 100644 quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/TraceData.java create mode 100644 quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/TracedService.java create mode 100644 quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/output/SpanDataModuleSerializer.java create mode 100644 quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/java/io/quarkiverse/opentelemetry/exporter/it/output/SpanDataSerializer.java create mode 100644 quarkus-opentelemetry-exporter-sentry/integration-tests/src/main/resources/application.properties create mode 100644 quarkus-opentelemetry-exporter-sentry/integration-tests/src/test/java/io/quarkiverse/opentelemetry/exporter/it/SentryTest.java create mode 100644 quarkus-opentelemetry-exporter-sentry/pom.xml create mode 100644 quarkus-opentelemetry-exporter-sentry/runtime/pom.xml create mode 100644 quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/beans/SentrySpanProcessorProducer.java create mode 100644 quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/config/SentryConfig.java create mode 100644 quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/filters/SentryFilter.java create mode 100644 quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/otel/SentryPropagatorProvider.java create mode 100644 quarkus-opentelemetry-exporter-sentry/runtime/src/main/java/io/quarkiverse/opentelemetry/exporter/sentry/recorders/SentryRecorder.java create mode 100644 quarkus-opentelemetry-exporter-sentry/runtime/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider create mode 100644 quarkus-opentelemetry-exporter-sentry/runtime/src/main/resources/application.properties 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..4f6873c --- /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/product/sentry-basics/dsn-explained[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/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 From 9662a02b6f75f630a3a5bbab821428e5929c98a2 Mon Sep 17 00:00:00 2001 From: brunobat Date: Mon, 11 Nov 2024 14:35:52 +0000 Subject: [PATCH 2/3] Fix docs --- docs/pom.xml | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) 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 + From 3db9f331a5312bbdb869e2cabf630a999bd082ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hermann?= Date: Mon, 11 Nov 2024 11:12:08 -0500 Subject: [PATCH 3/3] fix sentry dsn link --- .../ROOT/pages/quarkus-opentelemetry-exporter-sentry.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/quarkus-opentelemetry-exporter-sentry.adoc b/docs/modules/ROOT/pages/quarkus-opentelemetry-exporter-sentry.adoc index 4f6873c..cfc861a 100644 --- a/docs/modules/ROOT/pages/quarkus-opentelemetry-exporter-sentry.adoc +++ b/docs/modules/ROOT/pages/quarkus-opentelemetry-exporter-sentry.adoc @@ -19,7 +19,7 @@ For Maven: ---- -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/product/sentry-basics/dsn-explained[in the top right corner]. +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: