Skip to content

Commit a2c3d69

Browse files
authored
Add system tests for distributed tracing (#4233)
* Also use port when checking if a request is made to Sentry DSN * changelog * Add a param to control whether the test script should rebuild before running the tested server * Add system tests for distributed tracing
1 parent 083eb83 commit a2c3d69

File tree

9 files changed

+336
-30
lines changed

9 files changed

+336
-30
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.sentry.samples.spring.boot.jakarta;
2+
3+
import io.opentelemetry.instrumentation.annotations.WithSpan;
4+
import java.nio.charset.Charset;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
import org.springframework.http.HttpHeaders;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
import org.springframework.web.bind.annotation.PathVariable;
10+
import org.springframework.web.bind.annotation.PostMapping;
11+
import org.springframework.web.bind.annotation.RequestBody;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.RestController;
14+
import org.springframework.web.client.RestClient;
15+
16+
@RestController
17+
@RequestMapping("/tracing/")
18+
public class DistributedTracingController {
19+
private static final Logger LOGGER = LoggerFactory.getLogger(DistributedTracingController.class);
20+
private final RestClient restClient;
21+
22+
public DistributedTracingController(RestClient restClient) {
23+
this.restClient = restClient;
24+
}
25+
26+
@GetMapping("{id}")
27+
@WithSpan("tracingSpanThroughOtelAnnotation")
28+
Person person(@PathVariable Long id) {
29+
return restClient
30+
.get()
31+
.uri("http://localhost:8080/person/{id}", id)
32+
.header(
33+
HttpHeaders.AUTHORIZATION,
34+
"Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset()))
35+
.retrieve()
36+
.body(Person.class);
37+
}
38+
39+
@PostMapping
40+
Person create(@RequestBody Person person) {
41+
return restClient
42+
.post()
43+
.uri("http://localhost:8080/person/")
44+
.body(person)
45+
.header(
46+
HttpHeaders.AUTHORIZATION,
47+
"Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset()))
48+
.retrieve()
49+
.body(Person.class);
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package io.sentry.systemtest
2+
3+
import io.sentry.protocol.SentryId
4+
import io.sentry.samples.spring.boot.jakarta.Person
5+
import io.sentry.systemtest.util.TestHelper
6+
import org.junit.Before
7+
import org.springframework.http.HttpStatus
8+
import kotlin.test.Test
9+
import kotlin.test.assertEquals
10+
import kotlin.test.assertNotEquals
11+
12+
class DistributedTracingSystemTest {
13+
14+
lateinit var testHelper: TestHelper
15+
16+
@Before
17+
fun setup() {
18+
testHelper = TestHelper("http://localhost:8080")
19+
testHelper.reset()
20+
}
21+
22+
@Test
23+
fun `get person distributed tracing`() {
24+
val traceId = SentryId()
25+
val restClient = testHelper.restClient
26+
restClient.getPersonDistributedTracing(
27+
1L,
28+
"$traceId-424cffc8f94feeee-1",
29+
"sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET"
30+
)
31+
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, restClient.lastKnownStatusCode)
32+
33+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
34+
transaction.transaction == "GET /tracing/{id}" &&
35+
testHelper.doesTransactionHaveTraceId(transaction, traceId.toString())
36+
}
37+
38+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
39+
transaction.transaction == "GET /person/{id}" &&
40+
testHelper.doesTransactionHaveTraceId(transaction, traceId.toString())
41+
}
42+
}
43+
44+
@Test
45+
fun `get person distributed tracing with sampled false`() {
46+
val traceId = SentryId()
47+
val restClient = testHelper.restClient
48+
restClient.getPersonDistributedTracing(
49+
1L,
50+
"$traceId-424cffc8f94feeee-0",
51+
"sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=false,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET"
52+
)
53+
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, restClient.lastKnownStatusCode)
54+
55+
testHelper.ensureNoTransactionReceived { transaction, envelopeHeader ->
56+
transaction.transaction == "GET /tracing/{id}"
57+
}
58+
59+
testHelper.ensureNoTransactionReceived { transaction, envelopeHeader ->
60+
transaction.transaction == "GET /person/{id}"
61+
}
62+
}
63+
64+
@Test
65+
fun `get person distributed tracing without sample_rand`() {
66+
val traceId = SentryId()
67+
val restClient = testHelper.restClient
68+
restClient.getPersonDistributedTracing(
69+
1L,
70+
"$traceId-424cffc8f94feeee-1",
71+
"sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET"
72+
)
73+
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, restClient.lastKnownStatusCode)
74+
75+
var sampleRand1: String? = null
76+
var sampleRand2: String? = null
77+
78+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
79+
80+
val matches = transaction.transaction == "GET /tracing/{id}" &&
81+
envelopeHeader.traceContext!!.traceId == traceId &&
82+
testHelper.doesTransactionHaveTraceId(transaction, traceId.toString())
83+
84+
if (matches) {
85+
testHelper.logObject(envelopeHeader)
86+
testHelper.logObject(transaction)
87+
sampleRand1 = envelopeHeader.traceContext?.sampleRand
88+
}
89+
90+
matches
91+
}
92+
93+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
94+
val matches = transaction.transaction == "GET /person/{id}" &&
95+
envelopeHeader.traceContext!!.traceId == traceId &&
96+
testHelper.doesTransactionHaveTraceId(transaction, traceId.toString())
97+
98+
if (matches) {
99+
testHelper.logObject(envelopeHeader)
100+
testHelper.logObject(transaction)
101+
sampleRand2 = envelopeHeader.traceContext?.sampleRand
102+
}
103+
104+
matches
105+
}
106+
107+
assertEquals(sampleRand1, sampleRand2)
108+
}
109+
110+
@Test
111+
fun `get person distributed tracing updates sample_rate on deferred decision`() {
112+
val traceId = SentryId()
113+
val restClient = testHelper.restClient
114+
restClient.getPersonDistributedTracing(
115+
1L,
116+
"$traceId-424cffc8f94feeee",
117+
"sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=0.5,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET"
118+
)
119+
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, restClient.lastKnownStatusCode)
120+
121+
var sampleRate1: String? = null
122+
var sampleRate2: String? = null
123+
124+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
125+
126+
val matches = transaction.transaction == "GET /tracing/{id}" &&
127+
envelopeHeader.traceContext!!.traceId == traceId &&
128+
testHelper.doesTransactionHaveTraceId(transaction, traceId.toString())
129+
130+
if (matches) {
131+
testHelper.logObject(envelopeHeader)
132+
testHelper.logObject(transaction)
133+
sampleRate1 = envelopeHeader.traceContext?.sampleRate
134+
}
135+
136+
matches
137+
}
138+
139+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
140+
val matches = transaction.transaction == "GET /person/{id}" &&
141+
envelopeHeader.traceContext!!.traceId == traceId &&
142+
testHelper.doesTransactionHaveTraceId(transaction, traceId.toString())
143+
144+
if (matches) {
145+
testHelper.logObject(envelopeHeader)
146+
testHelper.logObject(transaction)
147+
sampleRate2 = envelopeHeader.traceContext?.sampleRate
148+
}
149+
150+
matches
151+
}
152+
153+
assertEquals(sampleRate1, sampleRate2)
154+
assertNotEquals(sampleRate1, "0.5")
155+
}
156+
157+
@Test
158+
fun `create person distributed tracing`() {
159+
val traceId = SentryId()
160+
val restClient = testHelper.restClient
161+
val person = Person("firstA", "lastB")
162+
val returnedPerson = restClient.createPersonDistributedTracing(
163+
person,
164+
"$traceId-424cffc8f94feeee-1",
165+
"sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET"
166+
)
167+
assertEquals(HttpStatus.OK, restClient.lastKnownStatusCode)
168+
169+
assertEquals(person.firstName, returnedPerson!!.firstName)
170+
assertEquals(person.lastName, returnedPerson!!.lastName)
171+
172+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
173+
transaction.transaction == "POST /tracing/" &&
174+
testHelper.doesTransactionHaveTraceId(transaction, traceId.toString())
175+
}
176+
177+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
178+
transaction.transaction == "POST /person/" &&
179+
testHelper.doesTransactionHaveTraceId(transaction, traceId.toString())
180+
}
181+
}
182+
}

sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class GraphqlGreetingSystemTest {
1919
val response = testHelper.graphqlClient.greet("world")
2020

2121
testHelper.ensureNoErrors(response)
22-
testHelper.ensureTransactionReceived { transaction ->
22+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
2323
testHelper.doesTransactionContainSpanWithDescription(transaction, "Query.greeting")
2424
}
2525
}
@@ -32,7 +32,7 @@ class GraphqlGreetingSystemTest {
3232
testHelper.ensureErrorReceived { error ->
3333
error.message?.message?.startsWith("Unresolved RuntimeException for executionId ") ?: false
3434
}
35-
testHelper.ensureTransactionReceived { transaction ->
35+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
3636
testHelper.doesTransactionContainSpanWithDescription(transaction, "Query.greeting")
3737
}
3838
}

sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class GraphqlProjectSystemTest {
2323

2424
testHelper.ensureNoErrors(response)
2525
assertEquals("proj-slug", response?.data?.project?.slug)
26-
testHelper.ensureTransactionReceived { transaction ->
26+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
2727
testHelper.doesTransactionContainSpanWithDescription(transaction, "Query.project")
2828
}
2929
}
@@ -34,7 +34,7 @@ class GraphqlProjectSystemTest {
3434

3535
testHelper.ensureNoErrors(response)
3636
assertNotNull(response?.data?.addProject)
37-
testHelper.ensureTransactionReceived { transaction ->
37+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
3838
testHelper.doesTransactionContainSpanWithDescription(transaction, "Mutation.addProject")
3939
}
4040
}
@@ -48,7 +48,7 @@ class GraphqlProjectSystemTest {
4848
testHelper.ensureErrorReceived { error ->
4949
error.message?.message?.startsWith("Unresolved RuntimeException for executionId ") ?: false
5050
}
51-
testHelper.ensureTransactionReceived { transaction ->
51+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
5252
testHelper.doesTransactionContainSpanWithDescription(transaction, "Mutation.addProject")
5353
}
5454
}

sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class GraphqlTaskSystemTest {
3030
assertEquals("C3", firstTask.creatorId)
3131
assertEquals("C3", firstTask.creator?.id)
3232

33-
testHelper.ensureTransactionReceived { transaction ->
33+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
3434
testHelper.doesTransactionContainSpanWithDescription(transaction, "Query.tasks")
3535
}
3636
}

sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class PersonSystemTest {
2323
restClient.getPerson(1L)
2424
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, restClient.lastKnownStatusCode)
2525

26-
testHelper.ensureTransactionReceived { transaction ->
26+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
2727
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") &&
2828
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
2929
}
@@ -39,7 +39,7 @@ class PersonSystemTest {
3939
assertEquals(person.firstName, returnedPerson!!.firstName)
4040
assertEquals(person.lastName, returnedPerson!!.lastName)
4141

42-
testHelper.ensureTransactionReceived { transaction ->
42+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
4343
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") &&
4444
testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi")
4545
}

sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class TodoSystemTest {
2222
restClient.getTodo(1L)
2323
assertEquals(HttpStatus.OK, restClient.lastKnownStatusCode)
2424

25-
testHelper.ensureTransactionReceived { transaction ->
25+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
2626
testHelper.doesTransactionContainSpanWithOp(transaction, "todoSpanOtelApi") &&
2727
testHelper.doesTransactionContainSpanWithOp(transaction, "todoSpanSentryApi") &&
2828
testHelper.doesTransactionContainSpanWithOp(transaction, "http.client")
@@ -35,7 +35,7 @@ class TodoSystemTest {
3535
restClient.getTodoWebclient(1L)
3636
assertEquals(HttpStatus.OK, restClient.lastKnownStatusCode)
3737

38-
testHelper.ensureTransactionReceived { transaction ->
38+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
3939
testHelper.doesTransactionContainSpanWithOp(transaction, "http.client")
4040
}
4141
}
@@ -46,7 +46,7 @@ class TodoSystemTest {
4646
restClient.getTodoRestClient(1L)
4747
assertEquals(HttpStatus.OK, restClient.lastKnownStatusCode)
4848

49-
testHelper.ensureTransactionReceived { transaction ->
49+
testHelper.ensureTransactionReceived { transaction, envelopeHeader ->
5050
testHelper.doesTransactionContainSpanWithOp(transaction, "todoRestClientSpanOtelApi") &&
5151
testHelper.doesTransactionContainSpanWithOp(transaction, "todoRestClientSpanSentryApi") &&
5252
testHelper.doesTransactionContainSpanWithOp(transaction, "http.client")

sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/util/RestTestClient.kt

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,36 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl
3333
}
3434
}
3535

36+
fun getPersonDistributedTracing(id: Long, sentryTraceHeader: String? = null, baggageHeader: String? = null): Person? {
37+
return try {
38+
val response = restTemplate().exchange("$backendBaseUrl/tracing/{id}", HttpMethod.GET, entityWithAuth(headerCallback = tracingHeaders(sentryTraceHeader, baggageHeader)), Person::class.java, mapOf("id" to id))
39+
lastKnownStatusCode = response.statusCode
40+
response.body
41+
} catch (e: HttpStatusCodeException) {
42+
lastKnownStatusCode = e.statusCode
43+
null
44+
}
45+
}
46+
47+
fun createPersonDistributedTracing(person: Person, sentryTraceHeader: String? = null, baggageHeader: String? = null): Person? {
48+
return try {
49+
val response = restTemplate().exchange("$backendBaseUrl/tracing/", HttpMethod.POST, entityWithAuth(person, tracingHeaders(sentryTraceHeader, baggageHeader)), Person::class.java, person)
50+
lastKnownStatusCode = response.statusCode
51+
response.body
52+
} catch (e: HttpStatusCodeException) {
53+
lastKnownStatusCode = e.statusCode
54+
null
55+
}
56+
}
57+
58+
private fun tracingHeaders(sentryTraceHeader: String?, baggageHeader: String?): (HttpHeaders) -> HttpHeaders {
59+
return { httpHeaders ->
60+
sentryTraceHeader?.let { httpHeaders.set("sentry-trace", it) }
61+
baggageHeader?.let { httpHeaders.set("baggage", it) }
62+
httpHeaders
63+
}
64+
}
65+
3666
fun getTodo(id: Long): Todo? {
3767
return try {
3868
val response = restTemplate().exchange("$backendBaseUrl/todo/{id}", HttpMethod.GET, entityWithAuth(), Todo::class.java, mapOf("id" to id))
@@ -66,11 +96,13 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl
6696
}
6797
}
6898

69-
private fun entityWithAuth(request: Any? = null): HttpEntity<Any?> {
99+
private fun entityWithAuth(request: Any? = null, headerCallback: ((HttpHeaders) -> HttpHeaders)? = null): HttpEntity<Any?> {
70100
val headers = HttpHeaders().also {
71101
it.setBasicAuth("user", "password")
72102
}
73103

74-
return HttpEntity<Any?>(request, headers)
104+
val modifiedHeaders = headerCallback?.invoke(headers) ?: headers
105+
106+
return HttpEntity<Any?>(request, modifiedHeaders)
75107
}
76108
}

0 commit comments

Comments
 (0)