From 81a1a6c19ce01552f9a7f0c8814398e573877c86 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 28 Nov 2022 18:04:26 +0100 Subject: [PATCH] Add support for Opentelemetry (#2344) Co-authored-by: Sentry Github Bot --- .craft.yml | 3 + .github/ISSUE_TEMPLATE/bug_report_java.yml | 2 + CHANGELOG.md | 12 +- README.md | 59 ++-- buildSrc/src/main/java/Config.kt | 6 + .../core/ActivityLifecycleIntegration.java | 6 +- .../android/okhttp/SentryOkHttpInterceptor.kt | 2 +- .../apollo3/SentryApollo3HttpInterceptor.kt | 2 +- .../sentry/apollo/SentryApolloInterceptor.kt | 23 +- .../sentry/jdbc/SentryJdbcEventListener.java | 2 +- .../sentry/openfeign/SentryFeignClient.java | 3 +- sentry-opentelemetry/README.md | 30 ++ .../sentry-opentelemetry-agent/README.md | 46 +++ .../build.gradle.kts | 160 ++++++++++ ...entry-opentelemetry-agentcustomization.api | 16 + .../build.gradle.kts | 78 +++++ ...ryAutoConfigurationCustomizerProvider.java | 100 ++++++ .../SentryBootstrapPackagesProvider.java | 21 ++ .../SentryPropagatorProvider.java | 17 ++ ...ling.bootstrap.BootstrapPackagesConfigurer | 1 + ...re.spi.AutoConfigurationCustomizerProvider | 1 + ...nfigure.spi.ConfigurablePropagatorProvider | 1 + .../api/sentry-opentelemetry-core.api | 49 +++ .../build.gradle.kts | 78 +++++ .../io/sentry/opentelemetry/OtelSpanInfo.java | 34 +++ .../sentry/opentelemetry/SentryOtelKeys.java | 16 + .../opentelemetry/SentryPropagator.java | 89 ++++++ .../opentelemetry/SentrySpanProcessor.java | 284 ++++++++++++++++++ .../opentelemetry/SentrySpanStorage.java | 41 +++ .../SpanDescriptionExtractor.java | 62 ++++ .../io/sentry/opentelemetry/TraceData.java | 50 +++ .../test/kotlin/SentrySpanProcessorTest.kt | 148 +++++++++ .../spring/boot/CustomEventProcessor.java | 35 --- .../jakarta/tracing/SentrySpanAdvice.java | 2 +- ...entrySpanClientHttpRequestInterceptor.java | 5 +- .../SentrySpanClientWebRequestFilter.java | 5 +- .../jakarta/tracing/SentryTracingFilter.java | 4 +- .../spring/tracing/SentrySpanAdvice.java | 2 +- ...entrySpanClientHttpRequestInterceptor.java | 8 +- .../SentrySpanClientWebRequestFilter.java | 9 +- .../spring/tracing/SentryTracingFilter.java | 3 +- sentry/api/sentry.api | 46 ++- sentry/src/main/java/io/sentry/Baggage.java | 2 +- sentry/src/main/java/io/sentry/DateUtils.java | 11 + sentry/src/main/java/io/sentry/DsnUtil.java | 36 +++ sentry/src/main/java/io/sentry/Hub.java | 11 +- sentry/src/main/java/io/sentry/ISpan.java | 22 +- .../src/main/java/io/sentry/ITransaction.java | 8 + .../src/main/java/io/sentry/Instrumenter.java | 12 + sentry/src/main/java/io/sentry/NoOpSpan.java | 19 +- .../main/java/io/sentry/NoOpTransaction.java | 29 +- .../main/java/io/sentry/SentryOptions.java | 25 ++ .../src/main/java/io/sentry/SentryTracer.java | 68 ++++- sentry/src/main/java/io/sentry/Span.java | 21 +- .../java/io/sentry/TransactionContext.java | 21 +- .../config/ClasspathPropertiesLoader.java | 9 +- .../modules/ResourcesModulesLoader.java | 10 +- .../io/sentry/protocol/SentryTransaction.java | 3 + .../src/test/java/io/sentry/DateUtilsTest.kt | 22 ++ sentry/src/test/java/io/sentry/DsnUtilTest.kt | 48 +++ sentry/src/test/java/io/sentry/HubTest.kt | 35 +++ .../test/java/io/sentry/InstrumenterTest.kt | 19 ++ .../test/java/io/sentry/SentryTracerTest.kt | 36 +++ sentry/src/test/java/io/sentry/SpanTest.kt | 26 +- .../java/io/sentry/TransactionContextTest.kt | 8 +- settings.gradle.kts | 3 + 66 files changed, 1923 insertions(+), 142 deletions(-) create mode 100644 sentry-opentelemetry/README.md create mode 100644 sentry-opentelemetry/sentry-opentelemetry-agent/README.md create mode 100644 sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts create mode 100644 sentry-opentelemetry/sentry-opentelemetry-agentcustomization/api/sentry-opentelemetry-agentcustomization.api create mode 100644 sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts create mode 100644 sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java create mode 100644 sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryBootstrapPackagesProvider.java create mode 100644 sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryPropagatorProvider.java create mode 100644 sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/resources/META-INF/services/io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesConfigurer create mode 100644 sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider create mode 100644 sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider create mode 100644 sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api create mode 100644 sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts create mode 100644 sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanInfo.java create mode 100644 sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryOtelKeys.java create mode 100644 sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryPropagator.java create mode 100644 sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java create mode 100644 sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanStorage.java create mode 100644 sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanDescriptionExtractor.java create mode 100644 sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/TraceData.java create mode 100644 sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentrySpanProcessorTest.kt delete mode 100644 sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CustomEventProcessor.java create mode 100644 sentry/src/main/java/io/sentry/DsnUtil.java create mode 100644 sentry/src/main/java/io/sentry/Instrumenter.java create mode 100644 sentry/src/test/java/io/sentry/DsnUtilTest.kt create mode 100644 sentry/src/test/java/io/sentry/InstrumenterTest.kt diff --git a/.craft.yml b/.craft.yml index 6ca89b13ca..ee94d4d7dd 100644 --- a/.craft.yml +++ b/.craft.yml @@ -40,6 +40,9 @@ targets: maven:io.sentry:sentry-android-fragment: maven:io.sentry:sentry-bom: maven:io.sentry:sentry-openfeign: + maven:io.sentry:sentry-opentelemetry-agent: + maven:io.sentry:sentry-opentelemetry-agentcustomization: + maven:io.sentry:sentry-opentelemetry-core: maven:io.sentry:sentry-apollo: maven:io.sentry:sentry-jdbc: maven:io.sentry:sentry-graphql: diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index 34dab19b01..ef030cbf43 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -14,6 +14,8 @@ body: - sentry-apollo - sentry-apollo-3 - sentry-kotlin-extensions + - sentry-opentelemetry-agent + - sentry-opentelemetry-core - sentry-servlet - sentry-servlet-jakarta - sentry-spring-boot-starter diff --git a/CHANGELOG.md b/CHANGELOG.md index d70bab93a7..dbff415fe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,18 @@ ## Unreleased -### Features - -- Add beforeSendTransaction which allows users to filter and change transactions ([#2388](https://github.com/getsentry/sentry-java/pull/2388)) - ### Fixes - Use `canonicalName` in Fragment Integration for better de-obfuscation ([#2379](https://github.com/getsentry/sentry-java/pull/2379)) - Fix Timber and Fragment integrations auto-installation for obfuscated builds ([#2379](https://github.com/getsentry/sentry-java/pull/2379)) - Don't attach screenshots to events from Hybrid SDKs ([#2360](https://github.com/getsentry/sentry-java/pull/2360)) - Ensure Hints do not cause memory leaks ([#2387](https://github.com/getsentry/sentry-java/pull/2387)) +- Do not attach empty `sentry-trace` and `baggage` headers ([#2385](https://github.com/getsentry/sentry-java/pull/2385)) + +### Features + +- Add beforeSendTransaction which allows users to filter and change transactions ([#2388](https://github.com/getsentry/sentry-java/pull/2388)) +- Add experimental support for OpenTelemetry ([README](sentry-opentelemetry/README.md))([#2344](https://github.com/getsentry/sentry-java/pull/2344)) ### Dependencies @@ -40,8 +42,6 @@ ### Features -- Don't set device name on Android if `sendDefaultPii` is disabled ([#2354](https://github.com/getsentry/sentry-java/pull/2354)) -- Fix corrupted UUID on Motorola devices ([#2363](https://github.com/getsentry/sentry-java/pull/2363)) - Update Spring Boot Jakarta to Spring Boot 3.0.0-RC2 ([#2347](https://github.com/getsentry/sentry-java/pull/2347)) ## 6.7.0 diff --git a/README.md b/README.md index 802a6189b0..52d005225e 100644 --- a/README.md +++ b/README.md @@ -16,33 +16,38 @@ Sentry SDK for Java and Android [![codecov](https://codecov.io/gh/getsentry/sentry-java/branch/main/graph/badge.svg)](https://codecov.io/gh/getsentry/sentry-java) [![Discord Chat](https://img.shields.io/discord/621778831602221064?logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/PXa5Apfe7K) -| Packages | Maven Central | Android API | -| ---------------------- | ------- | ------- | -| sentry-android | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android) | 16 | -| sentry-android-core | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-core) | 14 | -| sentry-android-ndk | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-ndk/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-ndk) | 16 | -| sentry-android-okhttp | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-okhttp/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-okhttp) | 21 | -| sentry-android-timber | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-timber/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-timber) | 14 | -| sentry-android-fragment | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-fragment/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-fragment) | 14 | -| sentry-android-navigation | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-navigation/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-navigation) | 14 | -| sentry-compose-android | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-android/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-android) | 21 | -| sentry-compose-desktop | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-desktop/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-desktop) | -| sentry-compose | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose) | -| sentry-apache-http-client-5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apache-http-client-5/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apache-http-client-5) | -| sentry | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry) | 14 | -| sentry-jul | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jul/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jul) | -| sentry-jdbc | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jdbc/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jdbc) | -| sentry-apollo | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo) | 14 | -| sentry-kotlin-extensions | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-kotlin-extensions/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-kotlin-extensions) | 14 | -| sentry-servlet | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet) | | -| sentry-servlet-jakarta | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet-jakarta/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet-jakarta) | | -| sentry-spring-boot-starter | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-starter/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-starter) | -| sentry-spring | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring) | -| sentry-logback | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-logback/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-logback) | -| sentry-log4j2 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2) | -| sentry-bom | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom) | -| sentry-graphql | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql) | -| sentry-openfeign | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign) | +| Packages | Maven Central | Android API | +|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------- | +| sentry-android | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android) | 16 | +| sentry-android-core | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-core) | 14 | +| sentry-android-ndk | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-ndk/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-ndk) | 16 | +| sentry-android-okhttp | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-okhttp/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-okhttp) | 21 | +| sentry-android-timber | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-timber/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-timber) | 14 | +| sentry-android-fragment | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-fragment/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-fragment) | 14 | +| sentry-android-navigation | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-navigation/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-navigation) | 14 | +| sentry-compose-android | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-android/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-android) | 21 | +| sentry-compose-desktop | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-desktop/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-desktop) | +| sentry-compose | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose) | +| sentry-apache-http-client-5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apache-http-client-5/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apache-http-client-5) | +| sentry | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry) | 14 | +| sentry-jul | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jul/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jul) | +| sentry-jdbc | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jdbc/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jdbc) | +| sentry-apollo | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo) | 14 | +| sentry-kotlin-extensions | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-kotlin-extensions/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-kotlin-extensions) | 14 | +| sentry-servlet | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet) | | +| sentry-servlet-jakarta | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet-jakarta/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet-jakarta) | | +| sentry-spring-boot-starter | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-starter/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-starter) | +| sentry-spring-boot-starter-jakarta | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-starter-jakarta/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-starter-jakarta) | +| sentry-spring | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring) | +| sentry-spring-jakarta | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-jakarta/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-jakarta) | +| sentry-logback | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-logback/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-logback) | +| sentry-log4j2 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2) | +| sentry-bom | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom) | +| sentry-graphql | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql) | +| sentry-openfeign | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign) | +| sentry-opentelemetry-agent | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agent) | +| sentry-opentelemetry-agentcustomization | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agentcustomization/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agentcustomization) | +| sentry-opentelemetry-core | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-core) | diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index ac8e8de10c..11c226acdc 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -131,6 +131,11 @@ object Config { val composeMaterial = "androidx.compose.material3:material3:1.0.0-alpha13" val apolloKotlin = "com.apollographql.apollo3:apollo-runtime:3.3.0" + + val otelVersion = "1.19.0" + val otelAlphaVersion = "1.19.0-alpha" + val otelJavaagentVersion = "1.19.2" + val otelJavaagentAlphaVersion = "1.19.2-alpha" } object AnnotationProcessors { @@ -193,6 +198,7 @@ object Config { val SENTRY_SPRING_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring" val SENTRY_SPRING_BOOT_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring-boot" val SENTRY_SPRING_BOOT_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring-boot.jakarta" + val SENTRY_OPENTELEMETRY_AGENT_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.opentelemetry.agent" val group = "io.sentry" val description = "SDK for sentry.io" val versionNameProp = "versionName" diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index a307b01679..62be5a39e0 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -15,6 +15,7 @@ import io.sentry.IHub; import io.sentry.ISpan; import io.sentry.ITransaction; +import io.sentry.Instrumenter; import io.sentry.Integration; import io.sentry.Scope; import io.sentry.SentryLevel; @@ -197,7 +198,10 @@ private void startTracing(final @NotNull Activity activity) { // start specific span for app start appStartSpan = transaction.startChild( - getAppStartOp(coldStart), getAppStartDesc(coldStart), appStartTime); + getAppStartOp(coldStart), + getAppStartDesc(coldStart), + appStartTime, + Instrumenter.SENTRY); } // lets bind to the scope so other integrations can pick it up diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt index 13fbe99dff..5cb9da2183 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt @@ -64,7 +64,7 @@ class SentryOkHttpInterceptor( var code: Int? = null try { val requestBuilder = request.newBuilder() - if (span != null && + if (span != null && !span.isNoOp && PropagationTargetsUtils.contain(hub.options.tracePropagationTargets, request.url.toString()) ) { span.toSentryTrace().let { diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt index e04d1a4071..a3615d3289 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -33,7 +33,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor(private val hub: IH var cleanedHeaders = removeSentryInternalHeaders(request.headers).toMutableList() - if (PropagationTargetsUtils.contain(hub.options.tracePropagationTargets, request.url)) { + if (!span.isNoOp && PropagationTargetsUtils.contain(hub.options.tracePropagationTargets, request.url)) { val sentryTraceHeader = span.toSentryTrace() val baggageHeader = span.toBaggageHeader(request.headers.filter { it.name == BaggageHeader.BAGGAGE_HEADER }.map { it.value }) cleanedHeaders.add(HttpHeader(sentryTraceHeader.name, sentryTraceHeader.value)) diff --git a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt index cd61d5367b..1ccb55ba5d 100644 --- a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt +++ b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt @@ -37,16 +37,23 @@ class SentryApolloInterceptor( chain.proceedAsync(request, dispatcher, callBack) } else { val span = startChild(request, activeSpan) - val sentryTraceHeader = span.toSentryTrace() - // we have no access to URI, no way to verify tracing origins - val requestHeaderBuilder = request.requestHeaders.toBuilder() - requestHeaderBuilder.addHeader(sentryTraceHeader.name, sentryTraceHeader.value) - span.toBaggageHeader(listOf(request.requestHeaders.headerValue(BaggageHeader.BAGGAGE_HEADER)))?.let { - requestHeaderBuilder.addHeader(it.name, it.value) + val requestWithHeader = if (span.isNoOp) { + request + } else { + val sentryTraceHeader = span.toSentryTrace() + + // we have no access to URI, no way to verify tracing origins + val requestHeaderBuilder = request.requestHeaders.toBuilder() + requestHeaderBuilder.addHeader(sentryTraceHeader.name, sentryTraceHeader.value) + span.toBaggageHeader(listOf(request.requestHeaders.headerValue(BaggageHeader.BAGGAGE_HEADER))) + ?.let { + requestHeaderBuilder.addHeader(it.name, it.value) + } + val headers = requestHeaderBuilder.build() + request.toBuilder().requestHeaders(headers).build() } - val headers = requestHeaderBuilder.build() - val requestWithHeader = request.toBuilder().requestHeaders(headers).build() + span.setData("operationId", requestWithHeader.operation.operationId()) span.setData("variables", requestWithHeader.operation.variables().valueMap().toString()) diff --git a/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java b/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java index b85af52774..f8062a097e 100644 --- a/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java +++ b/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java @@ -30,7 +30,7 @@ public SentryJdbcEventListener() { @Override public void onBeforeAnyExecute(final @NotNull StatementInformation statementInformation) { final ISpan parent = hub.getSpan(); - if (parent != null) { + if (parent != null && !parent.isNoOp()) { final ISpan span = parent.startChild("db.query", statementInformation.getSql()); CURRENT_SPAN.set(span); } diff --git a/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java b/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java index 01c43c739b..c25feb57fa 100644 --- a/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java +++ b/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java @@ -55,7 +55,8 @@ public Response execute(final @NotNull Request request, final @NotNull Request.O final RequestWrapper requestWrapper = new RequestWrapper(request); - if (PropagationTargetsUtils.contain(hub.getOptions().getTracePropagationTargets(), url)) { + if (!span.isNoOp() + && PropagationTargetsUtils.contain(hub.getOptions().getTracePropagationTargets(), url)) { final SentryTraceHeader sentryTraceHeader = span.toSentryTrace(); final @Nullable Collection requestBaggageHeader = request.headers().get(BaggageHeader.BAGGAGE_HEADER); diff --git a/sentry-opentelemetry/README.md b/sentry-opentelemetry/README.md new file mode 100644 index 0000000000..76a118dbb2 --- /dev/null +++ b/sentry-opentelemetry/README.md @@ -0,0 +1,30 @@ +# sentry-opentelemetry + +*NOTE: Our OpenTelemetry modules are still experimental. Any feedback is welcome.* + +## OpenTelemetry + +More information on OpenTelemetry can be found on their [website](https://opentelemetry.io/) as well +as their docs and GitHub repos: +- https://opentelemetry.io/docs/instrumentation/java/getting-started/ +- https://github.com/open-telemetry/opentelemetry-java +- https://github.com/open-telemetry/opentelemetry-java-instrumentation + +## Modules + +### [`sentry-opentelemetry-agent`](sentry-opentelemetry-agent/README.md) + +Produces the Sentry OpenTelemetry Java Agent `JAR` that can be used to auto instrument an +application. Please see the module [README](sentry-opentelemetry-agent/README.md) for more details on how to use it. + +### `sentry-opentelemetry-agentcustomization` + +This contains customizations to the OpenTelemetry Java Agent such as registering the +`SentrySpanProcessor` and `SentryPropagator` as well as providing default properties that +enable the `sentry` propagator and disable exporters so our agent doesn't trigger lots of log +warnings due to OTLP server not being there. + +### `sentry-opentelemetry-core` + +Contains `SentrySpanProcessor` and `SentryPropagator` which are used by our Java Agent but can also +be used when manually instrumenting using OpenTelemetry. diff --git a/sentry-opentelemetry/sentry-opentelemetry-agent/README.md b/sentry-opentelemetry/sentry-opentelemetry-agent/README.md new file mode 100644 index 0000000000..3ed8b775c0 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agent/README.md @@ -0,0 +1,46 @@ +# sentry-opentelemetry-agent + +*NOTE: Our OpenTelemetry modules are still experimental. Any feedback is welcome.* + +## How to use it + +Download the latest `sentry-opentelemetry-agent.jar` and use it when launching your Java +application by adding the following parameters to your `java` command: + +`$ SENTRY_PROPERTIES_FILE=sentry.properties java -javaagent:sentry-opentelemetry-agent.jar -jar your-application.jar` + +Your `sentry.properties` could look like this: + +```properties +# NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard +dsn=https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563 +traces-sample-rate=1.0 +``` + +For more details on configuring Sentry via `sentry.properties` please see the +[docs page](https://docs.sentry.io/platforms/java/configuration/). + +As an alternative to the `SENTRY_PROPERTIES_FILE` environment variable you can provide individual +settings as environment variables (e.g. `SENTRY_DSN=...`) or you may initialize `Sentry` inside +your target application. If you do so, please make sure to set the `instrumenter` to `otel`, e.g. +like this: + +``` +Sentry.init( + options -> { + options.setDsn("..."); + ... + options.setInstrumenter(Instrumenter.OTEL); + } +) +``` + +Using the `otel` instrumenter will ensure `Sentry` instrumentation will be done via OpenTelemetry +and integrations as well as direct interactions with transactions and spans have no effect. + +## Debugging + +To enable debug logging for Sentry, please provide `SENTRY_DEBUG=true` as environment variable or +add `debug=true` to your `sentry.properties`. + +To also show debug output for OpenTelemetry please add `-Dotel.javaagent.debug=true` to the command. diff --git a/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts new file mode 100644 index 0000000000..495e200f24 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts @@ -0,0 +1,160 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + `java-library` + id("com.github.johnrengelman.shadow") version "7.1.2" +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +fun relocatePackages(shadowJar: ShadowJar) { + // rewrite dependencies calling Logger.getLogger + shadowJar.relocate("java.util.logging.Logger", "io.opentelemetry.javaagent.bootstrap.PatchLogger") + + // rewrite library instrumentation dependencies + shadowJar.relocate("io.opentelemetry.instrumentation", "io.opentelemetry.javaagent.shaded.instrumentation") + + // relocate OpenTelemetry API usage + shadowJar.relocate("io.opentelemetry.api", "io.opentelemetry.javaagent.shaded.io.opentelemetry.api") + shadowJar.relocate("io.opentelemetry.semconv", "io.opentelemetry.javaagent.shaded.io.opentelemetry.semconv") + shadowJar.relocate("io.opentelemetry.context", "io.opentelemetry.javaagent.shaded.io.opentelemetry.context") + + // relocate the OpenTelemetry extensions that are used by instrumentation modules + // these extensions live in the AgentClassLoader, and are injected into the user's class loader + // by the instrumentation modules that use them + shadowJar.relocate("io.opentelemetry.extension.aws", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.aws") + shadowJar.relocate("io.opentelemetry.extension.kotlin", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.kotlin") +} + +// this configuration collects libs that will be placed in the bootstrap classloader +val bootstrapLibs = configurations.create("bootstrapLibs") { + isCanBeResolved = true + isCanBeConsumed = false +} + +// this configuration collects libs that will be placed in the agent classloader, isolated from the instrumented application code +val javaagentLibs = configurations.create("javaagentLibs") { + isCanBeResolved = true + isCanBeConsumed = false +} + +// this configuration stores the upstream agent dep that's extended by this project +val upstreamAgent = configurations.create("upstreamAgent") { + isCanBeResolved = true + isCanBeConsumed = false +} + +dependencies { + bootstrapLibs(projects.sentry) + javaagentLibs(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization) + upstreamAgent("io.opentelemetry.javaagent:opentelemetry-javaagent:${Config.Libs.otelJavaagentVersion}") +} + +fun isolateClasses(jars: Iterable): CopySpec { + return copySpec { + jars.forEach { + from(zipTree(it)) { + into("inst") + rename("^(.*)\\.class\$", "\$1.classdata") + // Rename LICENSE file since it clashes with license dir on non-case sensitive FSs (i.e. Mac) + rename("^LICENSE\$", "LICENSE.renamed") + exclude("META-INF/INDEX.LIST") + exclude("META-INF/*.DSA") + exclude("META-INF/*.SF") + } + } + } +} + +// Don't publish non-shadowed jar (shadowJar is in shadowRuntimeElements) +with(components["java"] as AdhocComponentWithVariants) { + configurations.forEach { + withVariantsFromConfiguration(configurations["apiElements"]) { + skip() + } + withVariantsFromConfiguration(configurations["runtimeElements"]) { + skip() + } + } +} + +tasks { + jar { + archiveClassifier.set("dontuse") + } + + // building the final javaagent jar is done in 3 steps: + + // 1. all distro specific javaagent libs are relocated + create("relocateJavaagentLibs", ShadowJar::class.java) { + configurations = listOf(javaagentLibs) + + duplicatesStrategy = DuplicatesStrategy.FAIL + + archiveFileName.set("javaagentLibs-relocated.jar") + + mergeServiceFiles() + exclude("**/module-info.class") + relocatePackages(this) + + // exclude known bootstrap dependencies - they can't appear in the inst/ directory + dependencies { + exclude("org.slf4j:slf4j-api") + exclude("io.opentelemetry:opentelemetry-api") + exclude("io.opentelemetry:opentelemetry-api-logs") + exclude("io.opentelemetry:opentelemetry-context") + exclude("io.opentelemetry:opentelemetry-semconv") + } + } + + // 2. the distro javaagent libs are then isolated - moved to the inst/ directory + // having a separate task for isolating javaagent libs is required to avoid duplicates with the upstream javaagent + // duplicatesStrategy in shadowJar won't be applied when adding files with with(CopySpec) because each CopySpec has + // its own duplicatesStrategy + create("isolateJavaagentLibs", Copy::class.java) { + dependsOn(findByName("relocateJavaagentLibs")) + with(isolateClasses(findByName("relocateJavaagentLibs")!!.outputs.files)) + + into("$buildDir/isolated/javaagentLibs") + } + + // 3. the relocated and isolated javaagent libs are merged together with the bootstrap libs (which undergo relocation + // in this task) and the upstream javaagent jar; duplicates are removed + shadowJar { + configurations = listOf(bootstrapLibs, upstreamAgent) + + dependsOn(findByName("isolateJavaagentLibs")) + from(findByName("isolateJavaagentLibs")!!.outputs) + + archiveClassifier.set("") + + duplicatesStrategy = DuplicatesStrategy.FAIL + + mergeServiceFiles { + include("inst/META-INF/services/*") + } + exclude("**/module-info.class") + relocatePackages(this) + + manifest { + attributes.put("Main-Class", "io.opentelemetry.javaagent.OpenTelemetryAgent") + attributes.put("Agent-Class", "io.opentelemetry.javaagent.OpenTelemetryAgent") + attributes.put("Premain-Class", "io.opentelemetry.javaagent.OpenTelemetryAgent") + attributes.put("Can-Redefine-Classes", "true") + attributes.put("Can-Retransform-Classes", "true") + attributes.put("Implementation-Vendor", "Sentry") + attributes.put("Implementation-Version", "sentry-${project.version}-otel-${Config.Libs.otelJavaagentVersion}") + attributes.put("Sentry-Version-Name", project.version) + attributes.put("Sentry-Opentelemetry-SDK-Name", Config.Sentry.SENTRY_OPENTELEMETRY_AGENT_SDK_NAME) + attributes.put("Sentry-Opentelemetry-Version-Name", Config.Libs.otelVersion) + attributes.put("Sentry-Opentelemetry-Javaagent-Version-Name", Config.Libs.otelJavaagentVersion) + } + } + + assemble { + dependsOn(shadowJar) + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/api/sentry-opentelemetry-agentcustomization.api b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/api/sentry-opentelemetry-agentcustomization.api new file mode 100644 index 0000000000..342f71b5bb --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/api/sentry-opentelemetry-agentcustomization.api @@ -0,0 +1,16 @@ +public final class io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider : io/opentelemetry/sdk/autoconfigure/spi/AutoConfigurationCustomizerProvider { + public fun ()V + public fun customize (Lio/opentelemetry/sdk/autoconfigure/spi/AutoConfigurationCustomizer;)V +} + +public final class io/sentry/opentelemetry/SentryBootstrapPackagesProvider : io/opentelemetry/javaagent/tooling/bootstrap/BootstrapPackagesConfigurer { + public fun ()V + public fun configure (Lio/opentelemetry/javaagent/tooling/bootstrap/BootstrapPackagesBuilder;Lio/opentelemetry/sdk/autoconfigure/spi/ConfigProperties;)V +} + +public final class io/sentry/opentelemetry/SentryPropagatorProvider : io/opentelemetry/sdk/autoconfigure/spi/ConfigurablePropagatorProvider { + public fun ()V + public fun getName ()Ljava/lang/String; + public fun getPropagator (Lio/opentelemetry/sdk/autoconfigure/spi/ConfigProperties;)Lio/opentelemetry/context/propagation/TextMapPropagator; +} + diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts new file mode 100644 index 0000000000..0b0dc08ca1 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts @@ -0,0 +1,78 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + kotlin("jvm") + jacoco + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +dependencies { + compileOnly(projects.sentry) + implementation(projects.sentryOpentelemetry.sentryOpentelemetryCore) + + compileOnly("io.opentelemetry:opentelemetry-sdk:${Config.Libs.otelVersion}") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:${Config.Libs.otelVersion}") + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api:${Config.Libs.otelJavaagentAlphaVersion}") + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-tooling:${Config.Libs.otelJavaagentAlphaVersion}") + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + errorprone(Config.CompileOnly.errorProneNullAway) + + // tests + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.awaitility) +} + +configure { + test { + java.srcDir("src/test/java") + } +} + +jacoco { + toolVersion = Config.QualityPlugins.Jacoco.version +} + +tasks.jacocoTestReport { + reports { + xml.isEnabled = true + html.isEnabled = false + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { + rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } + } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java new file mode 100644 index 0000000000..9058ee7855 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryAutoConfigurationCustomizerProvider.java @@ -0,0 +1,100 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.sentry.Instrumenter; +import io.sentry.Sentry; +import io.sentry.SentryOptions; +import io.sentry.protocol.SdkVersion; +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SentryAutoConfigurationCustomizerProvider + implements AutoConfigurationCustomizerProvider { + + @Override + public void customize(AutoConfigurationCustomizer autoConfiguration) { + final @Nullable String sentryPropertiesFile = System.getenv("SENTRY_PROPERTIES_FILE"); + final @Nullable String sentryDsn = System.getenv("SENTRY_DSN"); + + if (sentryPropertiesFile != null || sentryDsn != null) { + Sentry.init( + options -> { + options.setEnableExternalConfiguration(true); + options.setInstrumenter(Instrumenter.OTEL); + final @Nullable SdkVersion sdkVersion = createSdkVersion(options); + if (sdkVersion != null) { + options.setSdkVersion(sdkVersion); + } + }); + } + + autoConfiguration + .addTracerProviderCustomizer(this::configureSdkTracerProvider) + .addPropertiesSupplier(this::getDefaultProperties); + } + + private @Nullable SdkVersion createSdkVersion(final @NotNull SentryOptions sentryOptions) { + SdkVersion sdkVersion = sentryOptions.getSdkVersion(); + + try { + final @NotNull Enumeration resources = + ClassLoader.getSystemClassLoader().getResources("META-INF/MANIFEST.MF"); + while (resources.hasMoreElements()) { + try { + final @NotNull Manifest manifest = new Manifest(resources.nextElement().openStream()); + final @Nullable Attributes mainAttributes = manifest.getMainAttributes(); + if (mainAttributes != null) { + final @Nullable String name = mainAttributes.getValue("Sentry-Opentelemetry-SDK-Name"); + final @Nullable String version = mainAttributes.getValue("Sentry-Version-Name"); + + if (name != null && version != null) { + sdkVersion = SdkVersion.updateSdkVersion(sdkVersion, name, version); + sdkVersion.addPackage("maven:io.sentry:sentry-opentelemetry-agent", version); + final @Nullable String otelVersion = + mainAttributes.getValue("Sentry-Opentelemetry-Version-Name"); + if (otelVersion != null) { + sdkVersion.addPackage("maven:io.opentelemetry:opentelemetry-sdk", otelVersion); + } + final @Nullable String otelJavaagentVersion = + mainAttributes.getValue("Sentry-Opentelemetry-Javaagent-Version-Name"); + if (otelJavaagentVersion != null) { + sdkVersion.addPackage( + "maven:io.opentelemetry.javaagent:opentelemetry-javaagent", + otelJavaagentVersion); + } + } + } + } catch (Exception e) { + // ignore + } + } + } catch (IOException e) { + // ignore + } + + return sdkVersion; + } + + private SdkTracerProviderBuilder configureSdkTracerProvider( + SdkTracerProviderBuilder tracerProvider, ConfigProperties config) { + return tracerProvider.addSpanProcessor(new SentrySpanProcessor()); + } + + private Map getDefaultProperties() { + Map properties = new HashMap<>(); + properties.put("otel.traces.exporter", "none"); + properties.put("otel.metrics.exporter", "none"); + properties.put("otel.propagators", "sentry"); + return properties; + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryBootstrapPackagesProvider.java b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryBootstrapPackagesProvider.java new file mode 100644 index 0000000000..231a0af847 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryBootstrapPackagesProvider.java @@ -0,0 +1,21 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesBuilder; +import io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesConfigurer; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.sentry.Sentry; + +/** + * To ensure that the classes we add to bootstrap class loader are available in class loaders that + * don't delegate all class loading requests to bootstrap class loader e.g. OSGi we need to tell the + * agent which packages we have added. + * + * @see BootstrapPackagesConfigurer + */ +public final class SentryBootstrapPackagesProvider implements BootstrapPackagesConfigurer { + + @Override + public void configure(BootstrapPackagesBuilder builder, ConfigProperties config) { + builder.add(Sentry.class.getPackage().getName()); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryPropagatorProvider.java b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryPropagatorProvider.java new file mode 100644 index 0000000000..49acd725fb --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/java/io/sentry/opentelemetry/SentryPropagatorProvider.java @@ -0,0 +1,17 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider; + +public final class SentryPropagatorProvider implements ConfigurablePropagatorProvider { + @Override + public TextMapPropagator getPropagator(ConfigProperties config) { + return new SentryPropagator(); + } + + @Override + public String getName() { + return "sentry"; + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/resources/META-INF/services/io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesConfigurer b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/resources/META-INF/services/io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesConfigurer new file mode 100644 index 0000000000..4733b4591e --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/resources/META-INF/services/io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesConfigurer @@ -0,0 +1 @@ +io.sentry.opentelemetry.SentryBootstrapPackagesProvider diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider new file mode 100644 index 0000000000..ba7c0a3190 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider @@ -0,0 +1 @@ +io.sentry.opentelemetry.SentryAutoConfigurationCustomizerProvider diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider new file mode 100644 index 0000000000..d29e01096b --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider @@ -0,0 +1 @@ +io.sentry.opentelemetry.SentryPropagatorProvider diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api b/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api new file mode 100644 index 0000000000..2347a99db7 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api @@ -0,0 +1,49 @@ +public final class io/sentry/opentelemetry/OtelSpanInfo { + public fun (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V + public fun getDescription ()Ljava/lang/String; + public fun getOp ()Ljava/lang/String; + public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; +} + +public final class io/sentry/opentelemetry/SentryOtelKeys { + public static final field SENTRY_BAGGAGE_KEY Lio/opentelemetry/context/ContextKey; + public static final field SENTRY_TRACE_KEY Lio/opentelemetry/context/ContextKey; + public fun ()V +} + +public final class io/sentry/opentelemetry/SentryPropagator : io/opentelemetry/context/propagation/TextMapPropagator { + public fun ()V + public fun extract (Lio/opentelemetry/context/Context;Ljava/lang/Object;Lio/opentelemetry/context/propagation/TextMapGetter;)Lio/opentelemetry/context/Context; + public fun fields ()Ljava/util/Collection; + public fun inject (Lio/opentelemetry/context/Context;Ljava/lang/Object;Lio/opentelemetry/context/propagation/TextMapSetter;)V +} + +public final class io/sentry/opentelemetry/SentrySpanProcessor : io/opentelemetry/sdk/trace/SpanProcessor { + public fun ()V + public fun isEndRequired ()Z + public fun isStartRequired ()Z + public fun onEnd (Lio/opentelemetry/sdk/trace/ReadableSpan;)V + public fun onStart (Lio/opentelemetry/context/Context;Lio/opentelemetry/sdk/trace/ReadWriteSpan;)V +} + +public final class io/sentry/opentelemetry/SentrySpanStorage { + public fun get (Ljava/lang/String;)Lio/sentry/ISpan; + public static fun getInstance ()Lio/sentry/opentelemetry/SentrySpanStorage; + public fun removeAndGet (Ljava/lang/String;)Lio/sentry/ISpan; + public fun store (Ljava/lang/String;Lio/sentry/ISpan;)V +} + +public final class io/sentry/opentelemetry/SpanDescriptionExtractor { + public fun ()V + public fun extractSpanDescription (Lio/opentelemetry/sdk/trace/ReadableSpan;)Lio/sentry/opentelemetry/OtelSpanInfo; +} + +public final class io/sentry/opentelemetry/TraceData { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryTraceHeader;Lio/sentry/Baggage;)V + public fun getBaggage ()Lio/sentry/Baggage; + public fun getParentSpanId ()Ljava/lang/String; + public fun getSentryTraceHeader ()Lio/sentry/SentryTraceHeader; + public fun getSpanId ()Ljava/lang/String; + public fun getTraceId ()Ljava/lang/String; +} + diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts new file mode 100644 index 0000000000..c9c248ee01 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts @@ -0,0 +1,78 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + kotlin("jvm") + jacoco + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +dependencies { + compileOnly(projects.sentry) + + compileOnly("io.opentelemetry:opentelemetry-sdk:${Config.Libs.otelVersion}") + compileOnly("io.opentelemetry:opentelemetry-semconv:${Config.Libs.otelAlphaVersion}") + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + errorprone(Config.CompileOnly.errorProneNullAway) + + // tests + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.awaitility) + + testImplementation("io.opentelemetry:opentelemetry-sdk:${Config.Libs.otelVersion}") + testImplementation("io.opentelemetry:opentelemetry-semconv:${Config.Libs.otelAlphaVersion}") +} + +configure { + test { + java.srcDir("src/test/java") + } +} + +jacoco { + toolVersion = Config.QualityPlugins.Jacoco.version +} + +tasks.jacocoTestReport { + reports { + xml.isEnabled = true + html.isEnabled = false + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { + rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } + } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanInfo.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanInfo.java new file mode 100644 index 0000000000..6ad39d3793 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanInfo.java @@ -0,0 +1,34 @@ +package io.sentry.opentelemetry; + +import io.sentry.protocol.TransactionNameSource; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class OtelSpanInfo { + + private final @NotNull String op; + private final @NotNull String description; + private final @NotNull TransactionNameSource transactionNameSource; + + public OtelSpanInfo( + final @NotNull String op, + final @NotNull String description, + final @NotNull TransactionNameSource transactionNameSource) { + this.op = op; + this.description = description; + this.transactionNameSource = transactionNameSource; + } + + public @NotNull String getOp() { + return op; + } + + public @NotNull String getDescription() { + return description; + } + + public @NotNull TransactionNameSource getTransactionNameSource() { + return transactionNameSource; + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryOtelKeys.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryOtelKeys.java new file mode 100644 index 0000000000..51ead00c6f --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryOtelKeys.java @@ -0,0 +1,16 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.context.ContextKey; +import io.sentry.Baggage; +import io.sentry.SentryTraceHeader; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class SentryOtelKeys { + + public static final @NotNull ContextKey SENTRY_TRACE_KEY = + ContextKey.named("sentry.trace"); + public static final @NotNull ContextKey SENTRY_BAGGAGE_KEY = + ContextKey.named("sentry.baggage"); +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryPropagator.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryPropagator.java new file mode 100644 index 0000000000..85ba4b3b30 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryPropagator.java @@ -0,0 +1,89 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.context.propagation.TextMapSetter; +import io.sentry.Baggage; +import io.sentry.BaggageHeader; +import io.sentry.ISpan; +import io.sentry.SentryTraceHeader; +import io.sentry.exception.InvalidSentryTraceHeaderException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SentryPropagator implements TextMapPropagator { + + private static final @NotNull List FIELDS = + Arrays.asList(SentryTraceHeader.SENTRY_TRACE_HEADER, BaggageHeader.BAGGAGE_HEADER); + private final @NotNull SentrySpanStorage spanStorage = SentrySpanStorage.getInstance(); + + @Override + public Collection fields() { + return FIELDS; + } + + @Override + public void inject(final Context context, final C carrier, final TextMapSetter setter) { + final @NotNull Span otelSpan = Span.fromContext(context); + final @NotNull SpanContext otelSpanContext = otelSpan.getSpanContext(); + if (!otelSpanContext.isValid()) { + return; + } + final @Nullable ISpan sentrySpan = spanStorage.get(otelSpanContext.getSpanId()); + if (sentrySpan == null || sentrySpan.isNoOp()) { + return; + } + + final @NotNull SentryTraceHeader sentryTraceHeader = sentrySpan.toSentryTrace(); + setter.set(carrier, sentryTraceHeader.getName(), sentryTraceHeader.getValue()); + final @Nullable BaggageHeader baggageHeader = + sentrySpan.toBaggageHeader(Collections.emptyList()); + if (baggageHeader != null) { + setter.set(carrier, baggageHeader.getName(), baggageHeader.getValue()); + } + } + + @Override + public Context extract( + final Context context, final C carrier, final TextMapGetter getter) { + final @Nullable String sentryTraceString = + getter.get(carrier, SentryTraceHeader.SENTRY_TRACE_HEADER); + if (sentryTraceString == null) { + return context; + } + + try { + SentryTraceHeader sentryTraceHeader = new SentryTraceHeader(sentryTraceString); + + SpanContext otelSpanContext = + SpanContext.createFromRemoteParent( + sentryTraceHeader.getTraceId().toString(), + sentryTraceHeader.getSpanId().toString(), + TraceFlags.getSampled(), + TraceState.getDefault()); + + @NotNull + Context modifiedContext = context.with(SentryOtelKeys.SENTRY_TRACE_KEY, sentryTraceHeader); + + final @Nullable String baggageString = getter.get(carrier, BaggageHeader.BAGGAGE_HEADER); + Baggage baggage = Baggage.fromHeader(baggageString); + modifiedContext = modifiedContext.with(SentryOtelKeys.SENTRY_BAGGAGE_KEY, baggage); + + Span wrappedSpan = Span.wrap(otelSpanContext); + modifiedContext = modifiedContext.with(wrappedSpan); + + return modifiedContext; + } catch (InvalidSentryTraceHeaderException e) { + return context; + } + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java new file mode 100644 index 0000000000..0daa6cab64 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java @@ -0,0 +1,284 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.sentry.Baggage; +import io.sentry.DateUtils; +import io.sentry.DsnUtil; +import io.sentry.HubAdapter; +import io.sentry.IHub; +import io.sentry.ISpan; +import io.sentry.ITransaction; +import io.sentry.Instrumenter; +import io.sentry.SentryTraceHeader; +import io.sentry.SpanId; +import io.sentry.SpanStatus; +import io.sentry.TransactionContext; +import io.sentry.TransactionOptions; +import io.sentry.protocol.SentryId; +import io.sentry.protocol.TransactionNameSource; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SentrySpanProcessor implements SpanProcessor { + + private final @NotNull List spanKindsConsideredForSentryRequests = + Arrays.asList(SpanKind.CLIENT, SpanKind.INTERNAL); + private final @NotNull SpanDescriptionExtractor spanDescriptionExtractor = + new SpanDescriptionExtractor(); + private final @NotNull SentrySpanStorage spanStorage = SentrySpanStorage.getInstance(); + private final @NotNull IHub hub; + + public SentrySpanProcessor() { + this(HubAdapter.getInstance()); + } + + SentrySpanProcessor(@NotNull IHub hub) { + this.hub = hub; + } + + @Override + public void onStart(final @NotNull Context parentContext, final @NotNull ReadWriteSpan otelSpan) { + if (!ensurePrerequisites(otelSpan)) { + return; + } + + if (isSentryRequest(otelSpan)) { + return; + } + + final @NotNull TraceData traceData = getTraceData(otelSpan, parentContext); + final @Nullable ISpan sentryParentSpan = + traceData.getParentSpanId() == null ? null : spanStorage.get(traceData.getParentSpanId()); + + if (sentryParentSpan != null) { + final @NotNull Date startDate = + DateUtils.nanosToDate(otelSpan.toSpanData().getStartEpochNanos()); + final @NotNull ISpan sentryChildSpan = + sentryParentSpan.startChild( + otelSpan.getName(), otelSpan.getName(), startDate, Instrumenter.OTEL); + spanStorage.store(traceData.getSpanId(), sentryChildSpan); + } else { + final @NotNull String transactionName = otelSpan.getName(); + final @NotNull TransactionNameSource transactionNameSource = TransactionNameSource.CUSTOM; + final @Nullable String op = otelSpan.getName(); + final SpanId spanId = new SpanId(traceData.getSpanId()); + + final @NotNull TransactionContext transactionContext = + traceData.getSentryTraceHeader() == null + ? new TransactionContext( + transactionName, + op, + new SentryId(traceData.getTraceId()), + spanId, + transactionNameSource, + null, + null, + null) + : TransactionContext.fromSentryTrace( + transactionName, + transactionNameSource, + op, + traceData.getSentryTraceHeader(), + traceData.getBaggage(), + spanId); + ; + transactionContext.setInstrumenter(Instrumenter.OTEL); + + TransactionOptions transactionOptions = new TransactionOptions(); + transactionOptions.setStartTimestamp( + DateUtils.nanosToDate(otelSpan.toSpanData().getStartEpochNanos())); + + ISpan sentryTransaction = hub.startTransaction(transactionContext, transactionOptions); + spanStorage.store(traceData.getSpanId(), sentryTransaction); + } + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(final @NotNull ReadableSpan otelSpan) { + if (!ensurePrerequisites(otelSpan)) { + return; + } + + final @NotNull TraceData traceData = getTraceData(otelSpan, null); + final @Nullable ISpan sentrySpan = spanStorage.removeAndGet(traceData.getSpanId()); + + if (sentrySpan == null) { + return; + } + + if (isSentryRequest(otelSpan)) { + return; + } + + if (sentrySpan instanceof ITransaction) { + ITransaction sentryTransaction = (ITransaction) sentrySpan; + updateTransactionWithOtelData(sentryTransaction, otelSpan); + } else { + updateSpanWithOtelData(sentrySpan, otelSpan); + } + + final @NotNull SpanStatus sentryStatus = mapOtelStatus(otelSpan); + final @NotNull Date endTimestamp = + DateUtils.nanosToDate(otelSpan.toSpanData().getEndEpochNanos()); + sentrySpan.finish(sentryStatus, endTimestamp); + } + + @Override + public boolean isEndRequired() { + return true; + } + + private boolean ensurePrerequisites(final @NotNull ReadableSpan otelSpan) { + if (!hasSentryBeenInitialized()) { + return false; + } + + if (!Instrumenter.OTEL.equals(hub.getOptions().getInstrumenter())) { + return false; + } + + final @NotNull SpanContext otelSpanContext = otelSpan.getSpanContext(); + if (!otelSpanContext.isValid()) { + return false; + } + + return true; + } + + private boolean isSentryRequest(final @NotNull ReadableSpan otelSpan) { + final @NotNull SpanKind kind = otelSpan.getKind(); + if (!spanKindsConsideredForSentryRequests.contains(kind)) { + return false; + } + + final @Nullable String httpUrl = otelSpan.getAttribute(SemanticAttributes.HTTP_URL); + return DsnUtil.urlContainsDsnHost(hub.getOptions(), httpUrl); + } + + private @NotNull TraceData getTraceData( + final @NotNull ReadableSpan otelSpan, final @Nullable Context parentContext) { + final @NotNull SpanContext otelSpanContext = otelSpan.getSpanContext(); + final @NotNull String otelSpanId = otelSpanContext.getSpanId(); + final @NotNull String otelParentSpanIdMaybeInvalid = + otelSpan.getParentSpanContext().getSpanId(); + final @NotNull String otelTraceId = otelSpanContext.getTraceId(); + final @Nullable String otelParentSpanId = + io.opentelemetry.api.trace.SpanId.isValid(otelParentSpanIdMaybeInvalid) + ? otelParentSpanIdMaybeInvalid + : null; + + @Nullable SentryTraceHeader sentryTraceHeader = null; + @Nullable Baggage baggage = null; + + if (parentContext != null) { + sentryTraceHeader = parentContext.get(SentryOtelKeys.SENTRY_TRACE_KEY); + if (sentryTraceHeader != null) { + baggage = parentContext.get(SentryOtelKeys.SENTRY_BAGGAGE_KEY); + } + } + + return new TraceData(otelTraceId, otelSpanId, otelParentSpanId, sentryTraceHeader, baggage); + } + + private void updateTransactionWithOtelData( + final @NotNull ITransaction sentryTransaction, final @NotNull ReadableSpan otelSpan) { + final @NotNull OtelSpanInfo otelSpanInfo = + spanDescriptionExtractor.extractSpanDescription(otelSpan); + sentryTransaction.setOperation(otelSpanInfo.getOp()); + sentryTransaction.setName( + otelSpanInfo.getDescription(), otelSpanInfo.getTransactionNameSource()); + + final @NotNull Map otelContext = toOtelContext(otelSpan); + sentryTransaction.setContext("otel", otelContext); + } + + private @NotNull Map toOtelContext(final @NotNull ReadableSpan otelSpan) { + final @NotNull SpanData spanData = otelSpan.toSpanData(); + final @NotNull Map context = new HashMap<>(); + + context.put("attributes", toMapWithStringKeys(spanData.getAttributes())); + context.put("resource", toMapWithStringKeys(spanData.getResource().getAttributes())); + + return context; + } + + private void updateSpanWithOtelData( + final @NotNull ISpan sentrySpan, final @NotNull ReadableSpan otelSpan) { + final @NotNull SpanData spanData = otelSpan.toSpanData(); + + sentrySpan.setData("otel.kind", otelSpan.getKind()); + + spanData + .getAttributes() + .forEach( + (attributeKey, value) -> { + if (value != null) { + sentrySpan.setData(attributeKey.getKey(), value); + } + }); + + final @NotNull OtelSpanInfo otelSpanInfo = + spanDescriptionExtractor.extractSpanDescription(otelSpan); + sentrySpan.setOperation(otelSpanInfo.getOp()); + sentrySpan.setDescription(otelSpanInfo.getDescription()); + } + + private SpanStatus mapOtelStatus(final @NotNull ReadableSpan otelSpan) { + final @NotNull SpanData otelSpanData = otelSpan.toSpanData(); + final @NotNull StatusData otelStatus = otelSpanData.getStatus(); + final @NotNull StatusCode otelStatusCode = otelStatus.getStatusCode(); + + if (StatusCode.OK.equals(otelStatusCode) || StatusCode.UNSET.equals(otelStatusCode)) { + return SpanStatus.OK; + } + + final @Nullable Long httpStatus = otelSpan.getAttribute(SemanticAttributes.HTTP_STATUS_CODE); + if (httpStatus != null) { + final @Nullable SpanStatus spanStatus = SpanStatus.fromHttpStatusCode(httpStatus.intValue()); + if (spanStatus != null) { + return spanStatus; + } + } + + return SpanStatus.UNKNOWN_ERROR; + } + + private boolean hasSentryBeenInitialized() { + return hub.isEnabled(); + } + + private @NotNull Map toMapWithStringKeys(final @Nullable Attributes attributes) { + final @NotNull Map mapWithStringKeys = new HashMap<>(); + + if (attributes != null) { + attributes.forEach( + (key, value) -> { + if (key != null) { + mapWithStringKeys.put(key.getKey(), value); + } + }); + } + + return mapWithStringKeys; + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanStorage.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanStorage.java new file mode 100644 index 0000000000..9898fc5c4c --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanStorage.java @@ -0,0 +1,41 @@ +package io.sentry.opentelemetry; + +import io.sentry.ISpan; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class SentrySpanStorage { + private static volatile @Nullable SentrySpanStorage INSTANCE; + + public static @NotNull SentrySpanStorage getInstance() { + if (INSTANCE == null) { + synchronized (SentrySpanStorage.class) { + if (INSTANCE == null) { + INSTANCE = new SentrySpanStorage(); + } + } + } + + return INSTANCE; + } + + private final @NotNull Map spans = new ConcurrentHashMap<>(); + + private SentrySpanStorage() {} + + public void store(final @NotNull String spanId, final @NotNull ISpan span) { + spans.put(spanId, span); + } + + public @Nullable ISpan get(final @Nullable String spanId) { + return spans.get(spanId); + } + + public @Nullable ISpan removeAndGet(final @Nullable String spanId) { + return spans.remove(spanId); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanDescriptionExtractor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanDescriptionExtractor.java new file mode 100644 index 0000000000..06fa945402 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SpanDescriptionExtractor.java @@ -0,0 +1,62 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes; +import io.sentry.protocol.TransactionNameSource; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class SpanDescriptionExtractor { + + public @NotNull OtelSpanInfo extractSpanDescription(final @NotNull ReadableSpan otelSpan) { + final @NotNull String name = otelSpan.getName(); + + final @Nullable String httpMethod = otelSpan.getAttribute(SemanticAttributes.HTTP_METHOD); + if (httpMethod != null) { + return descriptionForHttpMethod(otelSpan, httpMethod); + } + + final @Nullable String dbSystem = otelSpan.getAttribute(SemanticAttributes.DB_SYSTEM); + if (dbSystem != null) { + return descriptionForDbSystem(otelSpan); + } + + return new OtelSpanInfo(name, name, TransactionNameSource.CUSTOM); + } + + private OtelSpanInfo descriptionForHttpMethod( + final @NotNull ReadableSpan otelSpan, final @NotNull String httpMethod) { + final @NotNull String name = otelSpan.getName(); + final @NotNull SpanKind kind = otelSpan.getKind(); + final @NotNull StringBuilder opBuilder = new StringBuilder("http"); + + if (SpanKind.CLIENT.equals(kind)) { + opBuilder.append(".client"); + } else if (SpanKind.SERVER.equals(kind)) { + opBuilder.append(".server"); + } + final @Nullable String httpTarget = otelSpan.getAttribute(SemanticAttributes.HTTP_TARGET); + final @Nullable String httpRoute = otelSpan.getAttribute(SemanticAttributes.HTTP_ROUTE); + final @Nullable String httpPath = httpRoute != null ? httpRoute : httpTarget; + final @NotNull String op = opBuilder.toString(); + + if (httpPath == null) { + return new OtelSpanInfo(op, name, TransactionNameSource.CUSTOM); + } + + final @NotNull String description = httpMethod + " " + httpPath; + final @NotNull TransactionNameSource transactionNameSource = + httpRoute != null ? TransactionNameSource.ROUTE : TransactionNameSource.URL; + + return new OtelSpanInfo(op, description, transactionNameSource); + } + + private OtelSpanInfo descriptionForDbSystem(final @NotNull ReadableSpan otelSpan) { + @Nullable String dbStatement = otelSpan.getAttribute(SemanticAttributes.DB_STATEMENT); + @NotNull String description = dbStatement != null ? dbStatement : otelSpan.getName(); + return new OtelSpanInfo("db", description, TransactionNameSource.TASK); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/TraceData.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/TraceData.java new file mode 100644 index 0000000000..08751b5609 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/TraceData.java @@ -0,0 +1,50 @@ +package io.sentry.opentelemetry; + +import io.sentry.Baggage; +import io.sentry.SentryTraceHeader; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class TraceData { + + private final @NotNull String traceId; + private final @NotNull String spanId; + private final @Nullable String parentSpanId; + private final @Nullable SentryTraceHeader sentryTraceHeader; + private final @Nullable Baggage baggage; + + public TraceData( + @NotNull final String traceId, + @NotNull final String spanId, + @Nullable final String parentSpanId, + @Nullable final SentryTraceHeader sentryTraceHeader, + @Nullable final Baggage baggage) { + this.traceId = traceId; + this.spanId = spanId; + this.parentSpanId = parentSpanId; + this.sentryTraceHeader = sentryTraceHeader; + this.baggage = baggage; + } + + public @NotNull String getTraceId() { + return traceId; + } + + public @NotNull String getSpanId() { + return spanId; + } + + public @Nullable String getParentSpanId() { + return parentSpanId; + } + + public @Nullable SentryTraceHeader getSentryTraceHeader() { + return sentryTraceHeader; + } + + public @Nullable Baggage getBaggage() { + return baggage; + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentrySpanProcessorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentrySpanProcessorTest.kt new file mode 100644 index 0000000000..3cdb487f29 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentrySpanProcessorTest.kt @@ -0,0 +1,148 @@ +package io.sentry.opentelemetry + +import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.api.trace.Tracer +import io.opentelemetry.context.Context +import io.opentelemetry.context.propagation.ContextPropagators +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.trace.SdkTracerProvider +import io.opentelemetry.semconv.trace.attributes.SemanticAttributes +import io.sentry.IHub +import io.sentry.ISpan +import io.sentry.ITransaction +import io.sentry.Instrumenter +import io.sentry.SentryOptions +import io.sentry.SpanStatus +import io.sentry.TransactionContext +import io.sentry.TransactionOptions +import io.sentry.protocol.TransactionNameSource +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.check +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.util.Date +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class SentrySpanProcessorTest { + + private class Fixture { + + lateinit var options: SentryOptions + lateinit var hub: IHub + lateinit var transaction: ITransaction + lateinit var span: ISpan + + fun getSut(): Tracer { + options = SentryOptions().also { + it.dsn = "https://key@sentry.io/proj" + it.instrumenter = Instrumenter.OTEL + } + hub = mock() + transaction = mock() + span = mock() + + whenever(hub.isEnabled).thenReturn(true) + whenever(hub.options).thenReturn(options) + whenever(hub.startTransaction(any(), any())).thenReturn(transaction) + whenever(transaction.startChild(any(), anyOrNull(), anyOrNull(), eq(Instrumenter.OTEL))).thenReturn(span) + + val sdkTracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SentrySpanProcessor(hub)) + .build() + + val openTelemetry: OpenTelemetry = OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + .setPropagators(ContextPropagators.create(SentryPropagator())) + .build() + + return openTelemetry.getTracer("sentry-test") + } + } + + private val fixture = Fixture() + + @Test + fun `requires start`() { + val processor = SentrySpanProcessor() + assertTrue(processor.isStartRequired) + } + + @Test + fun `requires end`() { + val processor = SentrySpanProcessor() + assertTrue(processor.isEndRequired) + } + + @Test + fun `ignores sentry client request`() { + val tracer = fixture.getSut() + tracer.spanBuilder("testspan") + .setSpanKind(SpanKind.CLIENT) + .setAttribute(SemanticAttributes.HTTP_URL, "https://key@sentry.io/proj/some-api") + .startSpan() + + verify(fixture.hub, never()).startTransaction(any(), any()) + } + + @Test + fun `ignores sentry internal request`() { + val tracer = fixture.getSut() + tracer.spanBuilder("testspan") + .setSpanKind(SpanKind.CLIENT) + .setAttribute(SemanticAttributes.HTTP_URL, "https://key@sentry.io/proj/some-api") + .startSpan() + + verify(fixture.hub, never()).startTransaction(any(), any()) + } + + @Test + fun `creates transaction for first otel span and span for second`() { + val tracer = fixture.getSut() + val otelSpan = tracer.spanBuilder("testspan") + .setSpanKind(SpanKind.SERVER) + .startSpan() + + verify(fixture.hub).startTransaction( + check { + assertEquals("testspan", it.name) + assertEquals(TransactionNameSource.CUSTOM, it.transactionNameSource) + assertEquals("testspan", it.operation) + assertEquals(otelSpan.spanContext.spanId, it.spanId.toString()) + assertEquals(otelSpan.spanContext.traceId, it.traceId.toString()) + assertNull(it.parentSpanId) + assertNull(it.parentSamplingDecision) + assertNull(it.baggage) + }, + check { + assertNotNull(it.startTimestamp) + assertFalse(it.isBindToScope) + } + ) + + val otelChildSpan = tracer.spanBuilder("childspan") + .setSpanKind(SpanKind.CLIENT) + .setParent(Context.current().with(otelSpan)) + .startSpan() + + verify(fixture.transaction).startChild(eq("childspan"), eq("childspan"), any(), eq(Instrumenter.OTEL)) + + otelSpan.end() + + verify(fixture.transaction).setContext(eq("otel"), any()) + verify(fixture.transaction).finish(eq(SpanStatus.OK), any()) + + otelChildSpan.end() + + verify(fixture.span).finish(eq(SpanStatus.OK), any()) + } +} diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CustomEventProcessor.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CustomEventProcessor.java deleted file mode 100644 index b74310a676..0000000000 --- a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CustomEventProcessor.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.sentry.samples.spring.boot; - -import io.sentry.EventProcessor; -import io.sentry.Hint; -import io.sentry.SentryEvent; -import io.sentry.protocol.SentryRuntime; -import org.jetbrains.annotations.NotNull; -import org.springframework.boot.SpringBootVersion; -import org.springframework.stereotype.Component; - -/** - * Custom {@link EventProcessor} implementation lets modifying {@link SentryEvent}s before they are - * sent to Sentry. - */ -@Component -public class CustomEventProcessor implements EventProcessor { - private final String springBootVersion; - - public CustomEventProcessor(String springBootVersion) { - this.springBootVersion = springBootVersion; - } - - public CustomEventProcessor() { - this(SpringBootVersion.getVersion()); - } - - @Override - public @NotNull SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { - final SentryRuntime runtime = new SentryRuntime(); - runtime.setVersion(springBootVersion); - runtime.setName("Spring Boot"); - event.getContexts().setRuntime(runtime); - return event; - } -} diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanAdvice.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanAdvice.java index cec8e9afd8..a181f5f538 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanAdvice.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanAdvice.java @@ -31,7 +31,7 @@ public SentrySpanAdvice(final @NotNull IHub hub) { public Object invoke(final @NotNull MethodInvocation invocation) throws Throwable { final ISpan activeSpan = hub.getSpan(); - if (activeSpan == null) { + if (activeSpan == null || activeSpan.isNoOp()) { // there is no active transaction, we do not start new span return invocation.proceed(); } else { diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java index 37b04e2b7d..b3cf87bf2e 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java @@ -49,10 +49,9 @@ public SentrySpanClientHttpRequestInterceptor(final @NotNull IHub hub) { request.getMethod() != null ? request.getMethod().name() : "unknown"; span.setDescription(methodName + " " + request.getURI()); - final SentryTraceHeader sentryTraceHeader = span.toSentryTrace(); - - if (PropagationTargetsUtils.contain( + if (!span.isNoOp() && PropagationTargetsUtils.contain( hub.getOptions().getTracePropagationTargets(), request.getURI())) { + final SentryTraceHeader sentryTraceHeader = span.toSentryTrace(); request.getHeaders().add(sentryTraceHeader.getName(), sentryTraceHeader.getValue()); @Nullable BaggageHeader baggage = diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java index c5843701c9..0d44f45839 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java @@ -41,12 +41,11 @@ public SentrySpanClientWebRequestFilter(final @NotNull IHub hub) { final ISpan span = activeSpan.startChild("http.client"); span.setDescription(request.method().name() + " " + request.url()); - final SentryTraceHeader sentryTraceHeader = span.toSentryTrace(); - final ClientRequest.Builder requestBuilder = ClientRequest.from(request); - if (PropagationTargetsUtils.contain( + if (!span.isNoOp() && PropagationTargetsUtils.contain( hub.getOptions().getTracePropagationTargets(), request.url())) { + final SentryTraceHeader sentryTraceHeader = span.toSentryTrace(); requestBuilder.header(sentryTraceHeader.getName(), sentryTraceHeader.getValue()); final @Nullable BaggageHeader baggageHeader = diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingFilter.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingFilter.java index d8956fa0ac..88bc989ce3 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingFilter.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingFilter.java @@ -131,7 +131,9 @@ private ITransaction startTransaction( TransactionNameSource.URL, "http.server", new SentryTraceHeader(sentryTraceHeader), - baggage); + baggage, + null + ); final TransactionOptions transactionOptions = new TransactionOptions(); transactionOptions.setCustomSamplingContext(customSamplingContext); diff --git a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanAdvice.java b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanAdvice.java index a29108a21e..51a7e1fca0 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanAdvice.java +++ b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanAdvice.java @@ -31,7 +31,7 @@ public SentrySpanAdvice(final @NotNull IHub hub) { public Object invoke(final @NotNull MethodInvocation invocation) throws Throwable { final ISpan activeSpan = hub.getSpan(); - if (activeSpan == null) { + if (activeSpan == null || activeSpan.isNoOp()) { // there is no active transaction, we do not start new span return invocation.proceed(); } else { diff --git a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java index 4456ea8afc..9a445fb7e3 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java +++ b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java @@ -49,10 +49,10 @@ public SentrySpanClientHttpRequestInterceptor(final @NotNull IHub hub) { request.getMethod() != null ? request.getMethod().name() : "unknown"; span.setDescription(methodName + " " + request.getURI()); - final SentryTraceHeader sentryTraceHeader = span.toSentryTrace(); - - if (PropagationTargetsUtils.contain( - hub.getOptions().getTracePropagationTargets(), request.getURI())) { + if (!span.isNoOp() + && PropagationTargetsUtils.contain( + hub.getOptions().getTracePropagationTargets(), request.getURI())) { + final SentryTraceHeader sentryTraceHeader = span.toSentryTrace(); request.getHeaders().add(sentryTraceHeader.getName(), sentryTraceHeader.getValue()); @Nullable BaggageHeader baggage = diff --git a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientWebRequestFilter.java b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientWebRequestFilter.java index 329fb1d28a..399e7044c9 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientWebRequestFilter.java +++ b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientWebRequestFilter.java @@ -41,12 +41,13 @@ public SentrySpanClientWebRequestFilter(final @NotNull IHub hub) { final ISpan span = activeSpan.startChild("http.client"); span.setDescription(request.method().name() + " " + request.url()); - final SentryTraceHeader sentryTraceHeader = span.toSentryTrace(); - final ClientRequest.Builder requestBuilder = ClientRequest.from(request); - if (PropagationTargetsUtils.contain( - hub.getOptions().getTracePropagationTargets(), request.url())) { + if (!span.isNoOp() + && PropagationTargetsUtils.contain( + hub.getOptions().getTracePropagationTargets(), request.url())) { + + final SentryTraceHeader sentryTraceHeader = span.toSentryTrace(); requestBuilder.header(sentryTraceHeader.getName(), sentryTraceHeader.getValue()); final @Nullable BaggageHeader baggageHeader = diff --git a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentryTracingFilter.java b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentryTracingFilter.java index 238c73a891..fd5cd5f0f7 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentryTracingFilter.java +++ b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentryTracingFilter.java @@ -131,7 +131,8 @@ private ITransaction startTransaction( TransactionNameSource.URL, "http.server", new SentryTraceHeader(sentryTraceHeader), - baggage); + baggage, + null); final TransactionOptions transactionOptions = new TransactionOptions(); transactionOptions.setCustomSamplingContext(customSamplingContext); diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 5d29a877fb..e33162b1ea 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -161,6 +161,7 @@ public final class io/sentry/DateUtils { public static fun getDateTimeWithMillisPrecision (Ljava/lang/String;)Ljava/util/Date; public static fun getTimestamp (Ljava/util/Date;)Ljava/lang/String; public static fun millisToSeconds (D)D + public static fun nanosToDate (J)Ljava/util/Date; public static fun nanosToMillis (D)D } @@ -173,6 +174,11 @@ public final class io/sentry/DiagnosticLogger : io/sentry/ILogger { public fun log (Lio/sentry/SentryLevel;Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V } +public final class io/sentry/DsnUtil { + public fun ()V + public static fun urlContainsDsnHost (Lio/sentry/SentryOptions;Ljava/lang/String;)Z +} + public final class io/sentry/DuplicateEventDetectionEventProcessor : io/sentry/EventProcessor { public fun (Lio/sentry/SentryOptions;)V public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; @@ -485,6 +491,7 @@ public abstract interface class io/sentry/ISerializer { public abstract interface class io/sentry/ISpan { public abstract fun finish ()V public abstract fun finish (Lio/sentry/SpanStatus;)V + public abstract fun finish (Lio/sentry/SpanStatus;Ljava/util/Date;)V public abstract fun getData (Ljava/lang/String;)Ljava/lang/Object; public abstract fun getDescription ()Ljava/lang/String; public abstract fun getOperation ()Ljava/lang/String; @@ -493,6 +500,7 @@ public abstract interface class io/sentry/ISpan { public abstract fun getTag (Ljava/lang/String;)Ljava/lang/String; public abstract fun getThrowable ()Ljava/lang/Throwable; public abstract fun isFinished ()Z + public abstract fun isNoOp ()Z public abstract fun setData (Ljava/lang/String;Ljava/lang/Object;)V public abstract fun setDescription (Ljava/lang/String;)V public abstract fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;)V @@ -503,13 +511,14 @@ public abstract interface class io/sentry/ISpan { public abstract fun setThrowable (Ljava/lang/Throwable;)V public abstract fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; - public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;)Lio/sentry/ISpan; + public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; public abstract fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public abstract fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public abstract fun traceContext ()Lio/sentry/TraceContext; } public abstract interface class io/sentry/ITransaction : io/sentry/ISpan { + public abstract fun getContexts ()Lio/sentry/protocol/Contexts; public abstract fun getEventId ()Lio/sentry/protocol/SentryId; public abstract fun getLatestActiveSpan ()Lio/sentry/Span; public abstract fun getName ()Ljava/lang/String; @@ -519,6 +528,7 @@ public abstract interface class io/sentry/ITransaction : io/sentry/ISpan { public abstract fun isProfileSampled ()Ljava/lang/Boolean; public abstract fun isSampled ()Ljava/lang/Boolean; public abstract fun scheduleFinish ()V + public abstract fun setContext (Ljava/lang/String;Ljava/lang/Object;)V public abstract fun setName (Ljava/lang/String;)V public abstract fun setName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V } @@ -532,6 +542,13 @@ public abstract interface class io/sentry/ITransportFactory { public abstract fun create (Lio/sentry/SentryOptions;Lio/sentry/RequestDetails;)Lio/sentry/transport/ITransport; } +public final class io/sentry/Instrumenter : java/lang/Enum { + public static final field OTEL Lio/sentry/Instrumenter; + public static final field SENTRY Lio/sentry/Instrumenter; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/Instrumenter; + public static fun values ()[Lio/sentry/Instrumenter; +} + public abstract interface class io/sentry/Integration { public abstract fun register (Lio/sentry/IHub;Lio/sentry/SentryOptions;)V } @@ -722,6 +739,7 @@ public final class io/sentry/NoOpLogger : io/sentry/ILogger { public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V + public fun finish (Lio/sentry/SpanStatus;Ljava/util/Date;)V public fun getData (Ljava/lang/String;)Ljava/lang/Object; public fun getDescription ()Ljava/lang/String; public static fun getInstance ()Lio/sentry/NoOpSpan; @@ -731,6 +749,7 @@ public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun getTag (Ljava/lang/String;)Ljava/lang/String; public fun getThrowable ()Ljava/lang/Throwable; public fun isFinished ()Z + public fun isNoOp ()Z public fun setData (Ljava/lang/String;Ljava/lang/Object;)V public fun setDescription (Ljava/lang/String;)V public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;)V @@ -741,7 +760,7 @@ public final class io/sentry/NoOpSpan : io/sentry/ISpan { public fun setThrowable (Ljava/lang/Throwable;)V public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; - public fun startChild (Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public fun traceContext ()Lio/sentry/TraceContext; @@ -750,6 +769,8 @@ public final class io/sentry/NoOpSpan : io/sentry/ISpan { public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V + public fun finish (Lio/sentry/SpanStatus;Ljava/util/Date;)V + public fun getContexts ()Lio/sentry/protocol/Contexts; public fun getData (Ljava/lang/String;)Ljava/lang/Object; public fun getDescription ()Ljava/lang/String; public fun getEventId ()Lio/sentry/protocol/SentryId; @@ -765,9 +786,11 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun getThrowable ()Ljava/lang/Throwable; public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; public fun isFinished ()Z + public fun isNoOp ()Z public fun isProfileSampled ()Ljava/lang/Boolean; public fun isSampled ()Ljava/lang/Boolean; public fun scheduleFinish ()V + public fun setContext (Ljava/lang/String;Ljava/lang/Object;)V public fun setData (Ljava/lang/String;Ljava/lang/Object;)V public fun setDescription (Ljava/lang/String;)V public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;)V @@ -780,7 +803,7 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun setThrowable (Ljava/lang/Throwable;)V public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; - public fun startChild (Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public fun traceContext ()Lio/sentry/TraceContext; @@ -1375,6 +1398,7 @@ public class io/sentry/SentryOptions { public fun getIgnoredExceptionsForType ()Ljava/util/Set; public fun getInAppExcludes ()Ljava/util/List; public fun getInAppIncludes ()Ljava/util/List; + public fun getInstrumenter ()Lio/sentry/Instrumenter; public fun getIntegrations ()Ljava/util/List; public fun getLogger ()Lio/sentry/ILogger; public fun getMaxAttachmentSize ()J @@ -1456,6 +1480,7 @@ public class io/sentry/SentryOptions { public fun setFlushTimeoutMillis (J)V public fun setHostnameVerifier (Ljavax/net/ssl/HostnameVerifier;)V public fun setIdleTimeout (Ljava/lang/Long;)V + public fun setInstrumenter (Lio/sentry/Instrumenter;)V public fun setLogger (Lio/sentry/ILogger;)V public fun setMaxAttachmentSize (J)V public fun setMaxBreadcrumbs (I)V @@ -1555,7 +1580,9 @@ public final class io/sentry/SentryTracer : io/sentry/ITransaction { public fun (Lio/sentry/TransactionContext;Lio/sentry/IHub;ZLio/sentry/TransactionFinishedCallback;)V public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V + public fun finish (Lio/sentry/SpanStatus;Ljava/util/Date;)V public fun getChildren ()Ljava/util/List; + public fun getContexts ()Lio/sentry/protocol/Contexts; public fun getData ()Ljava/util/Map; public fun getData (Ljava/lang/String;)Ljava/lang/Object; public fun getDescription ()Ljava/lang/String; @@ -1574,9 +1601,11 @@ public final class io/sentry/SentryTracer : io/sentry/ITransaction { public fun getTimestamp ()Ljava/lang/Double; public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; public fun isFinished ()Z + public fun isNoOp ()Z public fun isProfileSampled ()Ljava/lang/Boolean; public fun isSampled ()Ljava/lang/Boolean; public fun scheduleFinish ()V + public fun setContext (Ljava/lang/String;Ljava/lang/Object;)V public fun setData (Ljava/lang/String;Ljava/lang/Object;)V public fun setDescription (Ljava/lang/String;)V public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;)V @@ -1589,7 +1618,7 @@ public final class io/sentry/SentryTracer : io/sentry/ITransaction { public fun setThrowable (Ljava/lang/Throwable;)V public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; - public fun startChild (Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public fun traceContext ()Lio/sentry/TraceContext; @@ -1666,6 +1695,7 @@ public final class io/sentry/Span : io/sentry/ISpan { public fun (Lio/sentry/TransactionContext;Lio/sentry/SentryTracer;Lio/sentry/IHub;Ljava/util/Date;)V public fun finish ()V public fun finish (Lio/sentry/SpanStatus;)V + public fun finish (Lio/sentry/SpanStatus;Ljava/util/Date;)V public fun getData ()Ljava/util/Map; public fun getData (Ljava/lang/String;)Ljava/lang/Object; public fun getDescription ()Ljava/lang/String; @@ -1683,6 +1713,7 @@ public final class io/sentry/Span : io/sentry/ISpan { public fun getTimestamp ()Ljava/lang/Double; public fun getTraceId ()Lio/sentry/protocol/SentryId; public fun isFinished ()Z + public fun isNoOp ()Z public fun isProfileSampled ()Ljava/lang/Boolean; public fun isSampled ()Ljava/lang/Boolean; public fun setData (Ljava/lang/String;Ljava/lang/Object;)V @@ -1695,7 +1726,7 @@ public final class io/sentry/Span : io/sentry/ISpan { public fun setThrowable (Ljava/lang/Throwable;)V public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; - public fun startChild (Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; public fun traceContext ()Lio/sentry/TraceContext; @@ -1855,14 +1886,17 @@ public final class io/sentry/TransactionContext : io/sentry/SpanContext { public fun (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;Ljava/lang/String;Lio/sentry/TracesSamplingDecision;)V public fun (Ljava/lang/String;Ljava/lang/String;)V public fun (Ljava/lang/String;Ljava/lang/String;Lio/sentry/TracesSamplingDecision;)V + public fun (Ljava/lang/String;Ljava/lang/String;Lio/sentry/protocol/SentryId;Lio/sentry/SpanId;Lio/sentry/protocol/TransactionNameSource;Lio/sentry/SpanId;Lio/sentry/TracesSamplingDecision;Lio/sentry/Baggage;)V public static fun fromSentryTrace (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;Ljava/lang/String;Lio/sentry/SentryTraceHeader;)Lio/sentry/TransactionContext; - public static fun fromSentryTrace (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;Ljava/lang/String;Lio/sentry/SentryTraceHeader;Lio/sentry/Baggage;)Lio/sentry/TransactionContext; + public static fun fromSentryTrace (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;Ljava/lang/String;Lio/sentry/SentryTraceHeader;Lio/sentry/Baggage;Lio/sentry/SpanId;)Lio/sentry/TransactionContext; public static fun fromSentryTrace (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryTraceHeader;)Lio/sentry/TransactionContext; public fun getBaggage ()Lio/sentry/Baggage; + public fun getInstrumenter ()Lio/sentry/Instrumenter; public fun getName ()Ljava/lang/String; public fun getParentSampled ()Ljava/lang/Boolean; public fun getParentSamplingDecision ()Lio/sentry/TracesSamplingDecision; public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; + public fun setInstrumenter (Lio/sentry/Instrumenter;)V public fun setParentSampled (Ljava/lang/Boolean;)V public fun setParentSampled (Ljava/lang/Boolean;Ljava/lang/Boolean;)V } diff --git a/sentry/src/main/java/io/sentry/Baggage.java b/sentry/src/main/java/io/sentry/Baggage.java index aca3fdf0fd..283274d4de 100644 --- a/sentry/src/main/java/io/sentry/Baggage.java +++ b/sentry/src/main/java/io/sentry/Baggage.java @@ -35,7 +35,7 @@ public final class Baggage { final @NotNull ILogger logger; @NotNull - public static Baggage fromHeader(final String headerValue) { + public static Baggage fromHeader(final @Nullable String headerValue) { return Baggage.fromHeader( headerValue, false, HubAdapter.getInstance().getOptions().getLogger()); } diff --git a/sentry/src/main/java/io/sentry/DateUtils.java b/sentry/src/main/java/io/sentry/DateUtils.java index e99d6b9f68..22448d79f7 100644 --- a/sentry/src/main/java/io/sentry/DateUtils.java +++ b/sentry/src/main/java/io/sentry/DateUtils.java @@ -103,6 +103,17 @@ public static double nanosToMillis(final double nanos) { return nanos / 1000000; } + /** + * Converts nanoseconds to {{@link java.util.Date}} rounded down to milliseconds + * + * @param nanos - nanoseconds + * @return date rounded down to milliseconds + */ + public static Date nanosToDate(final long nanos) { + Double millis = nanosToMillis(Double.valueOf(nanos)); + return getDateTime(millis.longValue()); + } + /** * Convert {@link Date} to epoch time in seconds represented as {@link Double}. * diff --git a/sentry/src/main/java/io/sentry/DsnUtil.java b/sentry/src/main/java/io/sentry/DsnUtil.java new file mode 100644 index 0000000000..3c48e4a43e --- /dev/null +++ b/sentry/src/main/java/io/sentry/DsnUtil.java @@ -0,0 +1,36 @@ +package io.sentry; + +import java.net.URI; +import java.util.Locale; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class DsnUtil { + + public static boolean urlContainsDsnHost(@Nullable SentryOptions options, @Nullable String url) { + if (options == null) { + return false; + } + + if (url == null) { + return false; + } + + final @Nullable String dsnString = options.getDsn(); + if (dsnString == null) { + return false; + } + + final @NotNull Dsn dsn = new Dsn(dsnString); + final @NotNull URI sentryUri = dsn.getSentryUri(); + final @Nullable String dsnHost = sentryUri.getHost(); + + if (dsnHost == null) { + return false; + } + + return url.toLowerCase(Locale.ROOT).contains(dsnHost.toLowerCase(Locale.ROOT)); + } +} diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index c573b45c12..172b81dcad 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -699,6 +699,15 @@ public void flush(long timeoutMillis) { SentryLevel.WARNING, "Instance is disabled and this 'startTransaction' returns a no-op."); transaction = NoOpTransaction.getInstance(); + } else if (!options.getInstrumenter().equals(transactionContext.getInstrumenter())) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Returning no-op for instrumenter %s as the SDK has been configured to use instrumenter %s", + transactionContext.getInstrumenter(), + options.getInstrumenter()); + transaction = NoOpTransaction.getInstance(); } else if (!options.isTracingEnabled()) { options .getLogger() @@ -744,7 +753,7 @@ public void flush(long timeoutMillis) { SentryLevel.WARNING, "Instance is disabled and this 'traceHeaders' call is a no-op."); } else { final ISpan span = stack.peek().getScope().getSpan(); - if (span != null) { + if (span != null && !span.isNoOp()) { traceHeader = span.toSentryTrace(); } } diff --git a/sentry/src/main/java/io/sentry/ISpan.java b/sentry/src/main/java/io/sentry/ISpan.java index ab4bfdffe5..e0d744e434 100644 --- a/sentry/src/main/java/io/sentry/ISpan.java +++ b/sentry/src/main/java/io/sentry/ISpan.java @@ -20,7 +20,10 @@ public interface ISpan { @ApiStatus.Internal @NotNull ISpan startChild( - @NotNull String operation, @Nullable String description, @Nullable Date timestamp); + @NotNull String operation, + @Nullable String description, + @Nullable Date timestamp, + @NotNull Instrumenter instrumenter); /** * Starts a child Span. @@ -68,6 +71,15 @@ ISpan startChild( */ void finish(@Nullable SpanStatus status); + /** + * Sets span timestamp marking this span as finished. + * + * @param status - the status + * @param timestamp - the end timestamp + */ + @ApiStatus.Internal + void finish(@Nullable SpanStatus status, @Nullable Date timestamp); + /** * Sets span operation. * @@ -194,4 +206,12 @@ ISpan startChild( * @param unit the unit the value is measured in */ void setMeasurement(@NotNull String name, @NotNull Number value, @NotNull MeasurementUnit unit); + + /** + * Whether this span instance is a NOOP that doesn't collect information + * + * @return true if NOOP + */ + @ApiStatus.Internal + boolean isNoOp(); } diff --git a/sentry/src/main/java/io/sentry/ITransaction.java b/sentry/src/main/java/io/sentry/ITransaction.java index bed79fec6b..6e1c525fe8 100644 --- a/sentry/src/main/java/io/sentry/ITransaction.java +++ b/sentry/src/main/java/io/sentry/ITransaction.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.Contexts; import io.sentry.protocol.SentryId; import io.sentry.protocol.TransactionNameSource; import java.util.List; @@ -77,4 +78,11 @@ public interface ITransaction extends ISpan { /** Schedules when transaction should be automatically finished. */ void scheduleFinish(); + + @ApiStatus.Internal + void setContext(@NotNull String key, @NotNull Object context); + + @ApiStatus.Internal + @NotNull + Contexts getContexts(); } diff --git a/sentry/src/main/java/io/sentry/Instrumenter.java b/sentry/src/main/java/io/sentry/Instrumenter.java new file mode 100644 index 0000000000..f72535bde8 --- /dev/null +++ b/sentry/src/main/java/io/sentry/Instrumenter.java @@ -0,0 +1,12 @@ +package io.sentry; + +/** + * Which framework is responsible for instrumenting. This includes starting and stopping of + * transactions and spans. + */ +public enum Instrumenter { + SENTRY, + + /** OpenTelemetry */ + OTEL +} diff --git a/sentry/src/main/java/io/sentry/NoOpSpan.java b/sentry/src/main/java/io/sentry/NoOpSpan.java index 08388f8368..a9fd426c70 100644 --- a/sentry/src/main/java/io/sentry/NoOpSpan.java +++ b/sentry/src/main/java/io/sentry/NoOpSpan.java @@ -3,6 +3,7 @@ import io.sentry.protocol.SentryId; import java.util.Date; import java.util.List; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -23,7 +24,10 @@ public static NoOpSpan getInstance() { @Override public @NotNull ISpan startChild( - @NotNull String operation, @Nullable String description, @Nullable Date timestamp) { + @NotNull String operation, + @Nullable String description, + @Nullable Date timestamp, + @NotNull Instrumenter instrumenter) { return NoOpSpan.getInstance(); } @@ -44,8 +48,8 @@ public static NoOpSpan getInstance() { } @Override - public @NotNull BaggageHeader toBaggageHeader(@Nullable List thirdPartyBaggageHeaders) { - return new BaggageHeader(""); + public @Nullable BaggageHeader toBaggageHeader(@Nullable List thirdPartyBaggageHeaders) { + return null; } @Override @@ -54,6 +58,10 @@ public void finish() {} @Override public void finish(@Nullable SpanStatus status) {} + @Override + @ApiStatus.Internal + public void finish(@Nullable SpanStatus status, @Nullable Date timestamp) {} + @Override public void setOperation(@NotNull String operation) {} @@ -118,4 +126,9 @@ public void setMeasurement(@NotNull String name, @NotNull Number value) {} @Override public void setMeasurement( @NotNull String name, @NotNull Number value, @NotNull MeasurementUnit unit) {} + + @Override + public boolean isNoOp() { + return true; + } } diff --git a/sentry/src/main/java/io/sentry/NoOpTransaction.java b/sentry/src/main/java/io/sentry/NoOpTransaction.java index a3ca631a3b..46404a366f 100644 --- a/sentry/src/main/java/io/sentry/NoOpTransaction.java +++ b/sentry/src/main/java/io/sentry/NoOpTransaction.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.Contexts; import io.sentry.protocol.SentryId; import io.sentry.protocol.TransactionNameSource; import java.util.Collections; @@ -43,7 +44,10 @@ public void setName(@NotNull String name, @NotNull TransactionNameSource transac @Override public @NotNull ISpan startChild( - @NotNull String operation, @Nullable String description, @Nullable Date timestamp) { + @NotNull String operation, + @Nullable String description, + @Nullable Date timestamp, + @NotNull Instrumenter instrumenter) { return NoOpSpan.getInstance(); } @@ -92,8 +96,8 @@ public boolean isFinished() { } @Override - public @NotNull BaggageHeader toBaggageHeader(@Nullable List thirdPartyBaggageHeaders) { - return new BaggageHeader(""); + public @Nullable BaggageHeader toBaggageHeader(@Nullable List thirdPartyBaggageHeaders) { + return null; } @Override @@ -102,6 +106,10 @@ public void finish() {} @Override public void finish(@Nullable SpanStatus status) {} + @Override + @ApiStatus.Internal + public void finish(@Nullable SpanStatus status, @Nullable Date timestamp) {} + @Override public void setOperation(@NotNull String operation) {} @@ -171,4 +179,19 @@ public void setMeasurement(@NotNull String name, @NotNull Number value) {} @Override public void setMeasurement( @NotNull String name, @NotNull Number value, @NotNull MeasurementUnit unit) {} + + @ApiStatus.Internal + @Override + public void setContext(@NotNull String key, @NotNull Object context) {} + + @ApiStatus.Internal + @Override + public @NotNull Contexts getContexts() { + return new Contexts(); + } + + @Override + public boolean isNoOp() { + return true; + } } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 63172383aa..4b5543d601 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -368,6 +368,9 @@ public class SentryOptions { /** Modules (dependencies, packages) that will be send along with each event. */ private @NotNull IModulesLoader modulesLoader = NoOpModulesLoader.getInstance(); + /** Which framework is responsible for instrumenting. */ + private @NotNull Instrumenter instrumenter = Instrumenter.SENTRY; + /** * Adds an event processor * @@ -1767,6 +1770,28 @@ public void setSendClientReports(boolean sendClientReports) { } } + /** + * Sets the instrumenter used for performance instrumentation. + * + *

If you set this to something other than {{@link Instrumenter#SENTRY}} Sentry will not create + * any transactions automatically nor will it create transactions if you call + * startTransaction(...), nor will it create child spans if you call startChild(...) + * + * @param instrumenter - the instrumenter to use + */ + public void setInstrumenter(final @NotNull Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } + + /** + * Returns the instrumenter used for performance instrumentation + * + * @return the configured instrumenter + */ + public @NotNull Instrumenter getInstrumenter() { + return instrumenter; + } + /** * Returns a ClientReportRecorder or a NoOp if sending of client reports has been disabled. * diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 6d5c069992..451dd5a5a1 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.Contexts; import io.sentry.protocol.MeasurementValue; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -77,6 +78,8 @@ public final class SentryTracer implements ITransaction { private final @NotNull Baggage baggage; private @NotNull TransactionNameSource transactionNameSource; private final @NotNull Map measurements; + private final @NotNull Instrumenter instrumenter; + private final @NotNull Contexts contexts = new Contexts(); public SentryTracer(final @NotNull TransactionContext context, final @NotNull IHub hub) { this(context, hub, null); @@ -110,6 +113,7 @@ public SentryTracer( this.measurements = new ConcurrentHashMap<>(); this.root = new Span(context, this, hub, startTimestamp); this.name = context.getName(); + this.instrumenter = context.getInstrumenter(); this.hub = hub; this.waitForChildren = waitForChildren; this.idleTimeout = idleTimeout; @@ -195,8 +199,9 @@ ISpan startChild( final @NotNull SpanId parentSpanId, final @NotNull String operation, final @Nullable String description, - final @Nullable Date timestamp) { - return createChild(parentSpanId, operation, description, timestamp); + final @Nullable Date timestamp, + final @NotNull Instrumenter instrumenter) { + return createChild(parentSpanId, operation, description, timestamp, instrumenter); } /** @@ -207,7 +212,7 @@ ISpan startChild( */ @NotNull private ISpan createChild(final @NotNull SpanId parentSpanId, final @NotNull String operation) { - return createChild(parentSpanId, operation, null, null); + return createChild(parentSpanId, operation, null, null, Instrumenter.SENTRY); } @NotNull @@ -215,11 +220,16 @@ private ISpan createChild( final @NotNull SpanId parentSpanId, final @NotNull String operation, final @Nullable String description, - @Nullable Date timestamp) { + @Nullable Date timestamp, + final @NotNull Instrumenter instrumenter) { if (root.isFinished()) { return NoOpSpan.getInstance(); } + if (!this.instrumenter.equals(instrumenter)) { + return NoOpSpan.getInstance(); + } + Objects.requireNonNull(parentSpanId, "parentSpanId is required"); Objects.requireNonNull(operation, "operation is required"); cancelTimer(); @@ -256,26 +266,34 @@ private ISpan createChild( @Override public @NotNull ISpan startChild( - final @NotNull String operation, @Nullable String description, @Nullable Date timestamp) { - return createChild(operation, description, timestamp); + final @NotNull String operation, + @Nullable String description, + @Nullable Date timestamp, + @NotNull Instrumenter instrumenter) { + return createChild(operation, description, timestamp, instrumenter); } @Override public @NotNull ISpan startChild( final @NotNull String operation, final @Nullable String description) { - return createChild(operation, description, null); + return createChild(operation, description, null, Instrumenter.SENTRY); } private @NotNull ISpan createChild( final @NotNull String operation, final @Nullable String description, - @Nullable Date timestamp) { + @Nullable Date timestamp, + final @NotNull Instrumenter instrumenter) { if (root.isFinished()) { return NoOpSpan.getInstance(); } + if (!this.instrumenter.equals(instrumenter)) { + return NoOpSpan.getInstance(); + } + if (children.size() < hub.getOptions().getMaxSpans()) { - return root.startChild(operation, description, timestamp); + return root.startChild(operation, description, timestamp, instrumenter); } else { hub.getOptions() .getLogger() @@ -298,9 +316,15 @@ public void finish() { this.finish(this.getStatus()); } - @SuppressWarnings({"JdkObsolete", "JavaUtilDate"}) @Override public void finish(@Nullable SpanStatus status) { + this.finish(status, null); + } + + @SuppressWarnings({"JdkObsolete", "JavaUtilDate"}) + @Override + @ApiStatus.Internal + public void finish(@Nullable SpanStatus status, @Nullable Date finishDate) { this.finishStatus = FinishStatus.finishing(status); if (!root.isFinished() && (!waitForChildren || hasAllChildrenFinished())) { if (Boolean.TRUE.equals(isSampled()) && Boolean.TRUE.equals(isProfileSampled())) { @@ -310,6 +334,13 @@ public void finish(@Nullable SpanStatus status) { // try to get the high precision timestamp from the root span Long endTime = System.nanoTime(); Double finishTimestamp = root.getHighPrecisionTimestamp(endTime); + + // if a finishDate was passed in, use that instead + if (finishDate != null) { + finishTimestamp = DateUtils.dateToSeconds(finishDate); + endTime = null; + } + // if it's not set -> fallback to the current time if (finishTimestamp == null) { finishTimestamp = DateUtils.dateToSeconds(DateUtils.getCurrentDateTime()); @@ -633,6 +664,23 @@ Map getMeasurements() { return measurements; } + @ApiStatus.Internal + @Override + public void setContext(final @NotNull String key, final @NotNull Object context) { + contexts.put(key, context); + } + + @ApiStatus.Internal + @Override + public @NotNull Contexts getContexts() { + return contexts; + } + + @Override + public boolean isNoOp() { + return false; + } + private static final class FinishStatus { static final FinishStatus NOT_FINISHED = FinishStatus.notFinished(); diff --git a/sentry/src/main/java/io/sentry/Span.java b/sentry/src/main/java/io/sentry/Span.java index b149fac379..1d5376276b 100644 --- a/sentry/src/main/java/io/sentry/Span.java +++ b/sentry/src/main/java/io/sentry/Span.java @@ -117,12 +117,14 @@ public Span( public @NotNull ISpan startChild( final @NotNull String operation, final @Nullable String description, - final @Nullable Date timestamp) { + final @Nullable Date timestamp, + final @NotNull Instrumenter instrumenter) { if (finished.get()) { return NoOpSpan.getInstance(); } - return transaction.startChild(context.getSpanId(), operation, description, timestamp); + return transaction.startChild( + context.getSpanId(), operation, description, timestamp, instrumenter); } @Override @@ -160,6 +162,16 @@ public void finish(@Nullable SpanStatus status) { finish(status, DateUtils.dateToSeconds(DateUtils.getCurrentDateTime()), null); } + @Override + @ApiStatus.Internal + public void finish(@Nullable SpanStatus status, @Nullable Date timestamp) { + if (timestamp == null) { + finish(status); + } else { + finish(status, DateUtils.dateToSeconds(timestamp), null); + } + } + /** * Used to finish unfinished spans by {@link SentryTracer}. * @@ -329,6 +341,11 @@ Long getEndNanos() { return endNanos; } + @Override + public boolean isNoOp() { + return false; + } + void setSpanFinishedCallback(final @Nullable SpanFinishedCallback callback) { this.spanFinishedCallback = callback; } diff --git a/sentry/src/main/java/io/sentry/TransactionContext.java b/sentry/src/main/java/io/sentry/TransactionContext.java index e941621112..4463f9ae4f 100644 --- a/sentry/src/main/java/io/sentry/TransactionContext.java +++ b/sentry/src/main/java/io/sentry/TransactionContext.java @@ -12,6 +12,7 @@ public final class TransactionContext extends SpanContext { private final @NotNull TransactionNameSource transactionNameSource; private @Nullable TracesSamplingDecision parentSamplingDecision; private @Nullable Baggage baggage; + private @NotNull Instrumenter instrumenter = Instrumenter.SENTRY; /** * Creates {@link TransactionContext} from sentry-trace header. @@ -25,7 +26,7 @@ public final class TransactionContext extends SpanContext { final @NotNull String name, final @NotNull String operation, final @NotNull SentryTraceHeader sentryTrace) { - return fromSentryTrace(name, TransactionNameSource.CUSTOM, operation, sentryTrace, null); + return fromSentryTrace(name, TransactionNameSource.CUSTOM, operation, sentryTrace, null, null); } /** @@ -71,7 +72,8 @@ public final class TransactionContext extends SpanContext { final @NotNull TransactionNameSource transactionNameSource, final @NotNull String operation, final @NotNull SentryTraceHeader sentryTrace, - final @Nullable Baggage baggage) { + final @Nullable Baggage baggage, + final @Nullable SpanId spanId) { @Nullable Boolean parentSampled = sentryTrace.isSampled(); TracesSamplingDecision samplingDecision = parentSampled == null ? null : new TracesSamplingDecision(parentSampled); @@ -88,11 +90,13 @@ public final class TransactionContext extends SpanContext { } } + final @NotNull SpanId spanIdToUse = spanId == null ? new SpanId() : spanId; + return new TransactionContext( name, operation, sentryTrace.getTraceId(), - new SpanId(), + spanIdToUse, transactionNameSource, sentryTrace.getSpanId(), samplingDecision, @@ -144,7 +148,8 @@ public TransactionContext( this.setSamplingDecision(samplingDecision); } - private TransactionContext( + @ApiStatus.Internal + public TransactionContext( final @NotNull String name, final @NotNull String operation, final @NotNull SentryId traceId, @@ -203,4 +208,12 @@ public void setParentSampled( public @NotNull TransactionNameSource getTransactionNameSource() { return transactionNameSource; } + + public @NotNull Instrumenter getInstrumenter() { + return instrumenter; + } + + public void setInstrumenter(final @NotNull Instrumenter instrumenter) { + this.instrumenter = instrumenter; + } } diff --git a/sentry/src/main/java/io/sentry/config/ClasspathPropertiesLoader.java b/sentry/src/main/java/io/sentry/config/ClasspathPropertiesLoader.java index b458b808be..1a730e8f16 100644 --- a/sentry/src/main/java/io/sentry/config/ClasspathPropertiesLoader.java +++ b/sentry/src/main/java/io/sentry/config/ClasspathPropertiesLoader.java @@ -16,9 +16,14 @@ final class ClasspathPropertiesLoader implements PropertiesLoader { private final @NotNull ILogger logger; public ClasspathPropertiesLoader( - @NotNull String fileName, @NotNull ClassLoader classLoader, @NotNull ILogger logger) { + @NotNull String fileName, @Nullable ClassLoader classLoader, @NotNull ILogger logger) { this.fileName = fileName; - this.classLoader = classLoader; + // bootstrap classloader is represented as null, so using system classloader instead + if (classLoader == null) { + this.classLoader = ClassLoader.getSystemClassLoader(); + } else { + this.classLoader = classLoader; + } this.logger = logger; } diff --git a/sentry/src/main/java/io/sentry/internal/modules/ResourcesModulesLoader.java b/sentry/src/main/java/io/sentry/internal/modules/ResourcesModulesLoader.java index 491678118a..2b26a284d4 100644 --- a/sentry/src/main/java/io/sentry/internal/modules/ResourcesModulesLoader.java +++ b/sentry/src/main/java/io/sentry/internal/modules/ResourcesModulesLoader.java @@ -7,6 +7,7 @@ import java.util.TreeMap; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; @ApiStatus.Internal public final class ResourcesModulesLoader extends ModulesLoader { @@ -17,9 +18,14 @@ public ResourcesModulesLoader(final @NotNull ILogger logger) { this(logger, ResourcesModulesLoader.class.getClassLoader()); } - ResourcesModulesLoader(final @NotNull ILogger logger, final @NotNull ClassLoader classLoader) { + ResourcesModulesLoader(final @NotNull ILogger logger, final @Nullable ClassLoader classLoader) { super(logger); - this.classLoader = classLoader; + // bootstrap classloader is represented as null, so using system classloader instead + if (classLoader == null) { + this.classLoader = ClassLoader.getSystemClassLoader(); + } else { + this.classLoader = classLoader; + } } @Override diff --git a/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java b/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java index a1e96fa360..fab4b2ae42 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java +++ b/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java @@ -67,6 +67,9 @@ public SentryTransaction(final @NotNull SentryTracer sentryTracer) { } } final Contexts contexts = this.getContexts(); + + contexts.putAll(sentryTracer.getContexts()); + final SpanContext tracerContext = sentryTracer.getSpanContext(); // tags must be placed on the root of the transaction instead of contexts.trace.tags contexts.setTrace( diff --git a/sentry/src/test/java/io/sentry/DateUtilsTest.kt b/sentry/src/test/java/io/sentry/DateUtilsTest.kt index 06d59616f3..3176a313a2 100644 --- a/sentry/src/test/java/io/sentry/DateUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/DateUtilsTest.kt @@ -88,6 +88,28 @@ class DateUtilsTest { assertEquals("2020-06-07T12:38:12.631Z", timestamp) } + @Test + fun `nanos can be converted to Date losing nano precision`() { + val millis = 1591533492L * 1000L + 631L + val nanos = (millis * 1000L * 1000L) + (427L * 1000L) + val date = DateUtils.nanosToDate(nanos) + assertEquals(millis, date.time) + } + + @Test + fun `nanos can be converted to Date but rounds down to next ms`() { + val millis = 1591533492L * 1000L + 631L + val nanos = (millis * 1000L * 1000L) + (999L * 1000L) + val date = DateUtils.nanosToDate(nanos) + assertEquals(millis, date.time) + } + + @Test + fun `nanos can be 0`() { + val date = DateUtils.nanosToDate(0) + assertEquals(0, date.time) + } + private fun convertDate(date: Date): LocalDateTime { return Instant.ofEpochMilli(date.time) .atZone(utcTimeZone) diff --git a/sentry/src/test/java/io/sentry/DsnUtilTest.kt b/sentry/src/test/java/io/sentry/DsnUtilTest.kt new file mode 100644 index 0000000000..aa0f1c8e4b --- /dev/null +++ b/sentry/src/test/java/io/sentry/DsnUtilTest.kt @@ -0,0 +1,48 @@ +package io.sentry + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DsnUtilTest { + + companion object { + val DSN = "https://publicKey:secretKey@host/path/id?sample.rate=0.1" + } + + @Test + fun `returns false for null options`() { + assertFalse(DsnUtil.urlContainsDsnHost(null, "sentry.io")) + } + + @Test + fun `returns false for options with null dsn`() { + assertFalse(DsnUtil.urlContainsDsnHost(optionsWithDsn(null), "sentry.io")) + } + + @Test + fun `returns true for matching host`() { + assertTrue(DsnUtil.urlContainsDsnHost(optionsWithDsn(DSN), "host")) + } + + @Test + fun `returns true for matching host with different case`() { + assertTrue(DsnUtil.urlContainsDsnHost(optionsWithDsn(DSN), "HOST")) + } + + @Test + fun `returns true for matching host with different case 2`() { + assertTrue(DsnUtil.urlContainsDsnHost(optionsWithDsn(DSN.uppercase()), "host")) + } + + @Test + fun `returns false for null url`() { + assertFalse(DsnUtil.urlContainsDsnHost(optionsWithDsn(DSN), null)) + } + + private fun optionsWithDsn(dsn: String?): SentryOptions { + return SentryOptions().also { + it.dsn = dsn + } + } +} diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt index f5fb838d9f..dfeccb12c0 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/HubTest.kt @@ -906,6 +906,41 @@ class HubTest { assertEquals("test", scope?.transactionName) } + + @Test + fun `when startTransaction is called with different instrumenter, no-op is returned`() { + val hub = generateHub() + + val transactionContext = TransactionContext("test", "op").also { it.instrumenter = Instrumenter.OTEL } + val transactionOptions = TransactionOptions() + val tx = hub.startTransaction(transactionContext, transactionOptions) + + assertTrue(tx is NoOpTransaction) + } + + @Test + fun `when startTransaction is called with different instrumenter, no-op is returned 2`() { + val hub = generateHub() { + it.instrumenter = Instrumenter.OTEL + } + + val tx = hub.startTransaction("test", "op") + + assertTrue(tx is NoOpTransaction) + } + + @Test + fun `when startTransaction is called with configured instrumenter, it works`() { + val hub = generateHub() { + it.instrumenter = Instrumenter.OTEL + } + + val transactionContext = TransactionContext("test", "op").also { it.instrumenter = Instrumenter.OTEL } + val transactionOptions = TransactionOptions() + val tx = hub.startTransaction(transactionContext, transactionOptions) + + assertFalse(tx is NoOpTransaction) + } //endregion //region setUser tests diff --git a/sentry/src/test/java/io/sentry/InstrumenterTest.kt b/sentry/src/test/java/io/sentry/InstrumenterTest.kt new file mode 100644 index 0000000000..529f637c4e --- /dev/null +++ b/sentry/src/test/java/io/sentry/InstrumenterTest.kt @@ -0,0 +1,19 @@ +package io.sentry + +import kotlin.test.Test +import kotlin.test.assertEquals + +class InstrumenterTest { + + @Test + fun `can create from otel string`() { + val instrumenter = Instrumenter.valueOf("OTEL") + assertEquals(instrumenter, Instrumenter.OTEL) + } + + @Test + fun `can create from sentry string`() { + val instrumenter = Instrumenter.valueOf("SENTRY") + assertEquals(instrumenter, Instrumenter.SENTRY) + } +} diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index 304fe75300..fed5bcadc6 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -10,6 +10,8 @@ import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.verify +import java.time.LocalDateTime +import java.time.ZoneOffset import java.util.Date import kotlin.test.Test import kotlin.test.assertEquals @@ -119,6 +121,15 @@ class SentryTracerTest { assertEquals(SpanStatus.ABORTED, tracer.status) } + @Test + fun `when transaction is finished with status and timestamp, timestamp and status are set`() { + val tracer = fixture.getSut() + val date = Date.from(LocalDateTime.of(2022, 12, 24, 23, 59, 58, 0).toInstant(ZoneOffset.UTC)) + tracer.finish(SpanStatus.ABORTED, date) + assertEquals(tracer.timestamp, DateUtils.dateToSeconds(date)) + assertEquals(SpanStatus.ABORTED, tracer.status) + } + @Test fun `when transaction is finished, transaction is captured`() { val tracer = fixture.getSut() @@ -207,6 +218,31 @@ class SentryTracerTest { ) } + @Test + fun `when transaction is finished, context is set`() { + val tracer = fixture.getSut() + val otelContext = mapOf( + "attributes" to mapOf( + "db.connection_string" to "hsqldb:mem:", + "db.statement" to "CREATE TABLE person ( id INTEGER IDENTITY PRIMARY KEY, firstName VARCHAR(?) NOT NULL, lastName VARCHAR(?) NOT NULL )" + ), + "resource" to mapOf( + "process.runtime.version" to "17.0.4.1+1", + "telemetry.auto.version" to "sentry-6.7.0-otel-1.19.2" + ) + ) + tracer.setContext("otel", otelContext) + tracer.finish() + + verify(fixture.hub).captureTransaction( + check { + assertEquals(otelContext, it.contexts["otel"]) + }, + anyOrNull(), + anyOrNull() + ) + } + @Test fun `returns sentry-trace header`() { val tracer = fixture.getSut() diff --git a/sentry/src/test/java/io/sentry/SpanTest.kt b/sentry/src/test/java/io/sentry/SpanTest.kt index 9f21645ffe..f1823b1877 100644 --- a/sentry/src/test/java/io/sentry/SpanTest.kt +++ b/sentry/src/test/java/io/sentry/SpanTest.kt @@ -63,7 +63,7 @@ class SpanTest { @Test fun `when span is created with a start timestamp, finish timestamp is equals to high precision timestamp`() { - val span = fixture.getSut().startChild("op", "desc", Date()) as Span + val span = fixture.getSut().startChild("op", "desc", Date(), Instrumenter.SENTRY) as Span span.finish() assertNotNull(span.timestamp) @@ -140,6 +140,24 @@ class SpanTest { assertEquals(2, transaction.spans.size) } + @Test + fun `starting a child with different instrumenter no-ops`() { + val transaction = getTransaction(TransactionContext("name", "op").also { it.instrumenter = Instrumenter.OTEL }) + val span = transaction.startChild("operation", "description") + + span.startChild("op") + assertEquals(0, transaction.spans.size) + } + + @Test + fun `starting a child with same instrumenter adds span to transaction`() { + val transaction = getTransaction(TransactionContext("name", "op").also { it.instrumenter = Instrumenter.OTEL }) + val span = transaction.startChild("operation", "description", null, Instrumenter.OTEL) + + span.startChild("op", "desc", null, Instrumenter.OTEL) + assertEquals(2, transaction.spans.size) + } + @Test fun `when span was not finished, isFinished returns false`() { val span = startChildFromSpan() @@ -200,7 +218,7 @@ class SpanTest { span.finish(SpanStatus.OK) assertTrue(span.isFinished) - assertEquals(NoOpSpan.getInstance(), span.startChild("op", "desc", null)) + assertEquals(NoOpSpan.getInstance(), span.startChild("op", "desc", null, Instrumenter.SENTRY)) assertEquals(NoOpSpan.getInstance(), span.startChild("op", "desc")) span.finish(SpanStatus.UNKNOWN_ERROR) @@ -248,8 +266,8 @@ class SpanTest { } } - private fun getTransaction(): SentryTracer { - return SentryTracer(TransactionContext("name", "op"), fixture.hub) + private fun getTransaction(transactionContext: TransactionContext = TransactionContext("name", "op")): SentryTracer { + return SentryTracer(transactionContext, fixture.hub) } private fun startChildFromSpan(): Span { diff --git a/sentry/src/test/java/io/sentry/TransactionContextTest.kt b/sentry/src/test/java/io/sentry/TransactionContextTest.kt index 7eef9ce881..4c40a65272 100644 --- a/sentry/src/test/java/io/sentry/TransactionContextTest.kt +++ b/sentry/src/test/java/io/sentry/TransactionContextTest.kt @@ -33,7 +33,7 @@ class TransactionContextTest { fun `when context is created from trace header and baggage header, parent sampling decision of false is set from trace header`() { val traceHeader = SentryTraceHeader(SentryId(), SpanId(), false) val baggageHeader = Baggage.fromHeader("sentry-trace_id=a,sentry-transaction=sentryTransaction,sentry-sample_rate=0.3") - val context = TransactionContext.fromSentryTrace("name", TransactionNameSource.CUSTOM, "op", traceHeader, baggageHeader) + val context = TransactionContext.fromSentryTrace("name", TransactionNameSource.CUSTOM, "op", traceHeader, baggageHeader, null) assertNull(context.sampled) assertNull(context.profileSampled) assertFalse(context.parentSampled!!) @@ -44,7 +44,7 @@ class TransactionContextTest { fun `when context is created from trace header and baggage header, parent sampling decision of false is set from trace header if no sample rate is available`() { val traceHeader = SentryTraceHeader(SentryId(), SpanId(), false) val baggageHeader = Baggage.fromHeader("sentry-trace_id=a,sentry-transaction=sentryTransaction") - val context = TransactionContext.fromSentryTrace("name", TransactionNameSource.CUSTOM, "op", traceHeader, baggageHeader) + val context = TransactionContext.fromSentryTrace("name", TransactionNameSource.CUSTOM, "op", traceHeader, baggageHeader, null) assertNull(context.sampled) assertNull(context.profileSampled) assertFalse(context.parentSampled!!) @@ -55,7 +55,7 @@ class TransactionContextTest { fun `when context is created from trace header and baggage header, parent sampling decision of true is set from trace header`() { val traceHeader = SentryTraceHeader(SentryId(), SpanId(), true) val baggageHeader = Baggage.fromHeader("sentry-trace_id=a,sentry-transaction=sentryTransaction,sentry-sample_rate=0.3") - val context = TransactionContext.fromSentryTrace("name", TransactionNameSource.CUSTOM, "op", traceHeader, baggageHeader) + val context = TransactionContext.fromSentryTrace("name", TransactionNameSource.CUSTOM, "op", traceHeader, baggageHeader, null) assertNull(context.sampled) assertNull(context.profileSampled) assertTrue(context.parentSampled!!) @@ -66,7 +66,7 @@ class TransactionContextTest { fun `when context is created from trace header and baggage header, parent sampling decision of true is set from trace header if no sample rate is available`() { val traceHeader = SentryTraceHeader(SentryId(), SpanId(), true) val baggageHeader = Baggage.fromHeader("sentry-trace_id=a,sentry-transaction=sentryTransaction") - val context = TransactionContext.fromSentryTrace("name", TransactionNameSource.CUSTOM, "op", traceHeader, baggageHeader) + val context = TransactionContext.fromSentryTrace("name", TransactionNameSource.CUSTOM, "op", traceHeader, baggageHeader, null) assertNull(context.sampled) assertNull(context.profileSampled) assertTrue(context.parentSampled!!) diff --git a/settings.gradle.kts b/settings.gradle.kts index 4c49a75a84..eb534489d4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,6 +38,9 @@ include( "sentry-openfeign", "sentry-graphql", "sentry-jdbc", + "sentry-opentelemetry:sentry-opentelemetry-core", + "sentry-opentelemetry:sentry-opentelemetry-agentcustomization", + "sentry-opentelemetry:sentry-opentelemetry-agent", "sentry-samples:sentry-samples-android", "sentry-samples:sentry-samples-console", "sentry-samples:sentry-samples-jul",