Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
public final class io/sentry/opentelemetry/agent/AgentlessMarker {
public fun <init> ()V
}

public final class io/sentry/opentelemetry/agentless/BuildConfig {
public static final field SENTRY_OPENTELEMETRY_AGENTLESS_SDK_NAME Ljava/lang/String;
public static final field VERSION_NAME Ljava/lang/String;
}

Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
`java-library`
id("io.sentry.javadoc")
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.buildconfig)
id("io.sentry.animalsniffer")
}

tasks.named<KotlinCompile>("compileKotlin") {
compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
}

dependencies {
api(projects.sentry)
implementation(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap)
Expand All @@ -13,8 +20,17 @@ dependencies {
api(libs.otel.semconv)
api(libs.otel.semconv.incubating)
api(libs.otel.extension.autoconfigure)

testImplementation(projects.sentryAsyncProfiler)
testImplementation(projects.sentryTestSupport)
testImplementation(kotlin(Config.kotlinStdLib))
testImplementation(libs.kotlin.test.junit)
testImplementation(libs.google.truth)
}

// Sentry's OpenTelemetry customizer only initializes Sentry when the environment asks it to.
tasks.test { environment("SENTRY_AUTO_INIT", "true") }

buildConfig {
useJavaOutput()
packageName("io.sentry.opentelemetry.agentless")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package io.sentry.opentelemetry.agentless

import com.google.common.truth.Truth.assertThat
import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.sdk.OpenTelemetrySdk
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk
import io.sentry.Breadcrumb
import io.sentry.EventProcessor
import io.sentry.Hint
import io.sentry.Sentry
import io.sentry.SentryEvent
import io.sentry.SentryLevel
import io.sentry.protocol.Message
import io.sentry.test.MockSentryServer
import io.sentry.test.event
import io.sentry.test.hasFlag
import io.sentry.test.hasMetric
import io.sentry.test.hasMetricWithAttribute
import io.sentry.test.metrics
import io.sentry.test.profileChunk
import io.sentry.test.transaction
import java.util.concurrent.TimeUnit
import kotlin.test.AfterTest
import kotlin.test.Test

/**
* Sets Sentry up the way an app without the agent does: the OpenTelemetry SDK's autoconfiguration
* loads Sentry's customizer, which initializes Sentry and has it trace through OpenTelemetry.
*/
class AgentlessEndToEndTest {
private val server = MockSentryServer()
private var openTelemetry: OpenTelemetrySdk? = null

@AfterTest
fun teardown() {
openTelemetry?.close()
GlobalOpenTelemetry.resetForTest()
Sentry.close()
server.close()
SENTRY_PROPERTIES.forEach { System.clearProperty(it) }
}

@Test
fun `Sentry set up by OpenTelemetry reports events, a profiled transaction and metrics`() {
// The customizer's Sentry.init reads these as external configuration.
System.setProperty("sentry.dsn", server.dsn)
System.setProperty("sentry.traces-sample-rate", "1.0")
System.setProperty("sentry.profile-session-sample-rate", "1.0")
System.setProperty("sentry.profile-lifecycle", "TRACE")
// The profiler rejects a traces path with a dot anywhere in it, which the module's absolute
// path can have.
System.setProperty(
"sentry.profiling-traces-dir-path",
"${System.getProperty("java.io.tmpdir")}/sentry-agentless-end-to-end-test",
)
val openTelemetry =
AutoConfiguredOpenTelemetrySdk.builder()
.setResultAsGlobal()
.addPropertiesSupplier {
mapOf(
"otel.logs.exporter" to "none",
"otel.metrics.exporter" to "none",
"otel.traces.exporter" to "none",
)
}
.build()
.openTelemetrySdk
this.openTelemetry = openTelemetry

Sentry.configureScope {
it.addEventProcessor(
object : EventProcessor {
override fun process(event: SentryEvent, hint: Hint): SentryEvent = event.apply {
if ((level?.ordinal ?: 0) > SentryLevel.INFO.ordinal) {
addBreadcrumb(Breadcrumb("Processed by the event processor"))
}
}
}
)
}
Sentry.withScope {
it.level = SentryLevel.FATAL
Sentry.captureMessage("Fatal message!")
}
Sentry.captureMessage("Some warning!", SentryLevel.WARNING)
Sentry.addFeatureFlag("my-feature-flag", true)
Sentry.setAttribute("user.type", "admin")
Sentry.setAttribute("feature.version", 2)
Sentry.metrics().count("countMetric")
Sentry.metrics().gauge("gaugeMetric", 5.0)
Sentry.metrics().distribution("distributionMetric", 7.0)
Sentry.captureException(RuntimeException("Some error!"))
Sentry.captureEvent(
SentryEvent().apply {
message = Message().apply { message = "Detailed event" }
fingerprints = listOf("NewClientDebug")
level = SentryLevel.DEBUG
}
)
repeat(10) { i ->
Sentry.captureEvent(
SentryEvent().apply { message = Message().apply { message = "event $i" } }
)
}
val transaction = Sentry.startTransaction("transaction name", "op")
val span = transaction.startChild("child")
Thread.sleep(100)
span.finish()
transaction.finish()
Sentry.flush(5000)
// Sentry exports OpenTelemetry spans in batches every 5 seconds.
openTelemetry.sdkTracerProvider.forceFlush().join(5, TimeUnit.SECONDS)

val fatal =
server.awaitEnvelope { it.event()?.message?.formatted == "Fatal message!" }.event()!!
assertThat(fatal.level).isEqualTo(SentryLevel.FATAL)
assertThat(fatal.breadcrumbs!!.map { it.message }).contains("Processed by the event processor")

val warning =
server.awaitEnvelope { it.event()?.message?.formatted == "Some warning!" }.event()!!
assertThat(warning.level).isEqualTo(SentryLevel.WARNING)

val error =
server
.awaitEnvelope { it.event()?.exceptions?.any { e -> e.value == "Some error!" } == true }
.event()!!
assertThat(error.exceptions!!.map { it.type to it.value })
.contains("RuntimeException" to "Some error!")
assertThat(error.hasFlag("my-feature-flag", true)).isTrue()

val detailed =
server.awaitEnvelope { it.event()?.message?.message == "Detailed event" }.event()!!
assertThat(detailed.fingerprints).containsExactly("NewClientDebug")
assertThat(detailed.level).isEqualTo(SentryLevel.DEBUG)

repeat(10) { i -> server.awaitEnvelope { it.event()?.message?.message == "event $i" } }

val sent =
server.awaitEnvelope { it.transaction()?.transaction == "transaction name" }.transaction()!!
assertThat(sent.spans.map { it.op }).contains("child")
val profilerId = sent.contexts.profile?.profilerId
assertThat(profilerId).isNotNull()
server.awaitEnvelope(timeoutMillis = 30_000) { it.profileChunk()?.profilerId == profilerId }

val metrics = server.awaitEnvelope { it.metrics() != null }.metrics()!!
assertThat(metrics.hasMetric("countMetric", "counter", 1.0)).isTrue()
assertThat(metrics.hasMetric("gaugeMetric", "gauge", 5.0)).isTrue()
assertThat(metrics.hasMetric("distributionMetric", "distribution", 7.0)).isTrue()
assertThat(metrics.hasMetricWithAttribute("countMetric", "user.type", "admin")).isTrue()
assertThat(metrics.hasMetricWithAttribute("countMetric", "feature.version", 2)).isTrue()
}

private companion object {
val SENTRY_PROPERTIES =
listOf(
"sentry.dsn",
"sentry.traces-sample-rate",
"sentry.profile-session-sample-rate",
"sentry.profile-lifecycle",
"sentry.profiling-traces-dir-path",
)
}
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
java
application
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.gradle.versions)
alias(libs.plugins.shadow)
id("io.sentry.systemtest")
}

application { mainClass.set("io.sentry.samples.console.Main") }

tasks.withType<KotlinCompile>().configureEach {
kotlin {
compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict")
}
}

dependencies {
implementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentless)
implementation(projects.sentryAsyncProfiler)

testImplementation(kotlin(Config.kotlinStdLib))
testImplementation(projects.sentry)
testImplementation(projects.sentrySystemTestSupport)
testImplementation(libs.kotlin.test.junit)
testImplementation(libs.slf4j.api)
testImplementation(libs.slf4j.jdk14)
}

// Configure the Shadow JAR (executable JAR with all dependencies)
Expand All @@ -45,22 +28,3 @@ tasks.jar {

// Fix the startScripts task dependency
tasks.startScripts { dependsOn(tasks.shadowJar) }

tasks.register<Test>("systemTest").configure {
group = "verification"
description = "Runs the System tests"

val test = project.extensions.getByType<SourceSetContainer>()["test"]
testClassesDirs = test.output.classesDirs
classpath = test.runtimeClasspath

maxParallelForks = 1

filter { includeTestsMatching("io.sentry.systemtest*") }
}

tasks.named("test").configure {
require(this is Test)

filter { excludeTestsMatching("io.sentry.systemtest.*") }
}

This file was deleted.

This file was deleted.

Loading
Loading