Skip to content

Commit 3fc8b39

Browse files
authored
propagate okhttp status to parent spans (#2872)
* status of OkHttp calls now gets propagated to parent span * when OkHttp call finishes, status of unfinished spans is not overridden and is set to INTERNAL_ERROR
1 parent fe10f05 commit 3fc8b39

File tree

5 files changed

+127
-17
lines changed

5 files changed

+127
-17
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- propagate okhttp status to parent spans ([#2872](https://github.com/getsentry/sentry-java/pull/2872))
6+
7+
### Fixes
8+
39
## 6.27.0
410

511
### Features

sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ internal class SentryOkHttpEvent(private val hub: IHub, private val request: Req
6363
breadcrumb.setData("status_code", response.code)
6464
callRootSpan?.setData(PROTOCOL_KEY, response.protocol.name)
6565
callRootSpan?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, response.code)
66-
callRootSpan?.status = SpanStatus.fromHttpStatusCode(response.code)
6766
}
6867

6968
fun setProtocol(protocolName: String?) {
@@ -98,24 +97,20 @@ internal class SentryOkHttpEvent(private val hub: IHub, private val request: Req
9897
/** Starts a span, if the callRootSpan is not null. */
9998
fun startSpan(event: String) {
10099
// Find the parent of the span being created. E.g. secureConnect is child of connect
101-
val parentSpan = when (event) {
102-
// PROXY_SELECT, DNS, CONNECT and CONNECTION are not children of one another
103-
SECURE_CONNECT_EVENT -> eventSpans[CONNECT_EVENT]
104-
REQUEST_HEADERS_EVENT -> eventSpans[CONNECTION_EVENT]
105-
REQUEST_BODY_EVENT -> eventSpans[CONNECTION_EVENT]
106-
RESPONSE_HEADERS_EVENT -> eventSpans[CONNECTION_EVENT]
107-
RESPONSE_BODY_EVENT -> eventSpans[CONNECTION_EVENT]
108-
else -> callRootSpan
109-
} ?: callRootSpan
100+
val parentSpan = findParentSpan(event)
110101
val span = parentSpan?.startChild("http.client.$event") ?: return
111102
span.spanContext.origin = TRACE_ORIGIN
112103
eventSpans[event] = span
113104
}
114105

115-
/** Finishes a previously started span, and runs [beforeFinish] on it and on the call root span. */
106+
/** Finishes a previously started span, and runs [beforeFinish] on it, on its parent and on the call root span. */
116107
fun finishSpan(event: String, beforeFinish: ((span: ISpan) -> Unit)? = null) {
117108
val span = eventSpans[event] ?: return
109+
val parentSpan = findParentSpan(event)
118110
beforeFinish?.invoke(span)
111+
if (parentSpan != null && parentSpan != callRootSpan) {
112+
beforeFinish?.invoke(parentSpan)
113+
}
119114
callRootSpan?.let { beforeFinish?.invoke(it) }
120115
span.finish()
121116
}
@@ -125,7 +120,10 @@ internal class SentryOkHttpEvent(private val hub: IHub, private val request: Req
125120
callRootSpan ?: return
126121

127122
// We forcefully finish all spans, even if they should already have been finished through finishSpan()
128-
eventSpans.values.filter { !it.isFinished }.forEach { it.finish(SpanStatus.DEADLINE_EXCEEDED) }
123+
eventSpans.values.filter { !it.isFinished }.forEach {
124+
// If a status was set on the span, we use that, otherwise we set its status as error.
125+
it.finish(it.status ?: SpanStatus.INTERNAL_ERROR)
126+
}
129127
beforeFinish?.invoke(callRootSpan)
130128
callRootSpan.finish()
131129

@@ -137,4 +135,14 @@ internal class SentryOkHttpEvent(private val hub: IHub, private val request: Req
137135
hub.addBreadcrumb(breadcrumb, hint)
138136
return
139137
}
138+
139+
private fun findParentSpan(event: String): ISpan? = when (event) {
140+
// PROXY_SELECT, DNS, CONNECT and CONNECTION are not children of one another
141+
SECURE_CONNECT_EVENT -> eventSpans[CONNECT_EVENT]
142+
REQUEST_HEADERS_EVENT -> eventSpans[CONNECTION_EVENT]
143+
REQUEST_BODY_EVENT -> eventSpans[CONNECTION_EVENT]
144+
RESPONSE_HEADERS_EVENT -> eventSpans[CONNECTION_EVENT]
145+
RESPONSE_BODY_EVENT -> eventSpans[CONNECTION_EVENT]
146+
else -> callRootSpan
147+
} ?: callRootSpan
140148
}

sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEventListener.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,10 @@ class SentryOkHttpEventListener(
313313
okHttpEvent.setResponse(response)
314314
okHttpEvent.finishSpan(RESPONSE_HEADERS_EVENT) {
315315
it.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, response.code)
316-
it.status = SpanStatus.fromHttpStatusCode(response.code)
316+
// Let's not override the status of a span that was set
317+
if (it.status == null) {
318+
it.status = SpanStatus.fromHttpStatusCode(response.code)
319+
}
317320
}
318321
}
319322

sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventListenerTest.kt

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import okhttp3.mockwebserver.MockResponse
1919
import okhttp3.mockwebserver.MockWebServer
2020
import okhttp3.mockwebserver.SocketPolicy
2121
import org.mockito.kotlin.any
22+
import org.mockito.kotlin.anyOrNull
2223
import org.mockito.kotlin.eq
2324
import org.mockito.kotlin.mock
2425
import org.mockito.kotlin.spy
@@ -249,7 +250,7 @@ class SentryOkHttpEventListenerTest {
249250

250251
@Test
251252
fun `propagate all calls to the event listener passed in the ctor`() {
252-
val sut = fixture.getSut(eventListener = fixture.mockEventListener, httpStatusCode = 500)
253+
val sut = fixture.getSut(eventListener = fixture.mockEventListener)
253254
val listener = fixture.sentryOkHttpEventListener
254255
val request = postRequest(body = "requestBody")
255256
val call = sut.newCall(request)
@@ -260,7 +261,7 @@ class SentryOkHttpEventListenerTest {
260261

261262
@Test
262263
fun `propagate all calls to the event listener factory passed in the ctor`() {
263-
val sut = fixture.getSut(eventListenerFactory = fixture.mockEventListenerFactory, httpStatusCode = 500)
264+
val sut = fixture.getSut(eventListenerFactory = fixture.mockEventListenerFactory)
264265
val listener = fixture.sentryOkHttpEventListener
265266
val request = postRequest(body = "requestBody")
266267
val call = sut.newCall(request)
@@ -272,7 +273,7 @@ class SentryOkHttpEventListenerTest {
272273
@Test
273274
fun `propagate all calls to the SentryOkHttpEventListener passed in the ctor`() {
274275
val originalListener = spy(SentryOkHttpEventListener(fixture.hub, fixture.mockEventListener))
275-
val sut = fixture.getSut(eventListener = originalListener, httpStatusCode = 500)
276+
val sut = fixture.getSut(eventListener = originalListener)
276277
val listener = fixture.sentryOkHttpEventListener
277278
val request = postRequest(body = "requestBody")
278279
val call = sut.newCall(request)
@@ -284,7 +285,7 @@ class SentryOkHttpEventListenerTest {
284285
@Test
285286
fun `propagate all calls to the SentryOkHttpEventListener factory passed in the ctor`() {
286287
val originalListener = spy(SentryOkHttpEventListener(fixture.hub, fixture.mockEventListener))
287-
val sut = fixture.getSut(eventListenerFactory = { originalListener }, httpStatusCode = 500)
288+
val sut = fixture.getSut(eventListenerFactory = { originalListener })
288289
val listener = fixture.sentryOkHttpEventListener
289290
val request = postRequest(body = "requestBody")
290291
val call = sut.newCall(request)
@@ -305,6 +306,49 @@ class SentryOkHttpEventListenerTest {
305306
assertEquals(9, fixture.sentryTracer.children.size)
306307
}
307308

309+
@Test
310+
fun `status propagates to parent span and call root span`() {
311+
val sut = fixture.getSut(httpStatusCode = 500)
312+
val request = getRequest()
313+
val call = sut.newCall(request)
314+
val response = call.execute()
315+
val okHttpEvent = SentryOkHttpEventListener.eventMap[call]
316+
val callSpan = okHttpEvent?.callRootSpan
317+
val responseHeaderSpan =
318+
fixture.sentryTracer.children.firstOrNull { it.operation == "http.client.response_headers" }
319+
val connectionSpan = fixture.sentryTracer.children.firstOrNull { it.operation == "http.client.connection" }
320+
response.close()
321+
assertNotNull(callSpan)
322+
assertNotNull(responseHeaderSpan)
323+
assertNotNull(connectionSpan)
324+
assertEquals(SpanStatus.fromHttpStatusCode(500), callSpan.status)
325+
assertEquals(SpanStatus.fromHttpStatusCode(500), responseHeaderSpan.status)
326+
assertEquals(SpanStatus.fromHttpStatusCode(500), connectionSpan.status)
327+
}
328+
329+
@Test
330+
fun `call root span status is not overridden if not null`() {
331+
val mockListener = mock<EventListener>()
332+
lateinit var call: Call
333+
whenever(mockListener.connectStart(any(), anyOrNull(), anyOrNull())).then {
334+
val okHttpEvent = SentryOkHttpEventListener.eventMap[call]
335+
val callSpan = okHttpEvent?.callRootSpan
336+
assertNotNull(callSpan)
337+
assertNull(callSpan.status)
338+
callSpan.status = SpanStatus.UNKNOWN
339+
it
340+
}
341+
val sut = fixture.getSut(eventListener = mockListener)
342+
val request = getRequest()
343+
call = sut.newCall(request)
344+
val response = call.execute()
345+
val okHttpEvent = SentryOkHttpEventListener.eventMap[call]
346+
val callSpan = okHttpEvent?.callRootSpan
347+
assertNotNull(callSpan)
348+
response.close()
349+
assertEquals(SpanStatus.UNKNOWN, callSpan.status)
350+
}
351+
308352
private fun verifyDelegation(
309353
listener: SentryOkHttpEventListener,
310354
originalListener: EventListener,

sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventTest.kt

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.sentry.SentryTracer
88
import io.sentry.Span
99
import io.sentry.SpanDataConvention
1010
import io.sentry.SpanOptions
11+
import io.sentry.SpanStatus
1112
import io.sentry.TracesSamplingDecision
1213
import io.sentry.TransactionContext
1314
import io.sentry.TypeCheckHint
@@ -242,6 +243,28 @@ class SentryOkHttpEventTest {
242243
)
243244
}
244245

246+
@Test
247+
fun `when finishEvent, all running spans are finished with internal_error status`() {
248+
val sut = fixture.getSut()
249+
sut.startSpan("span")
250+
val spans = sut.getEventSpans()
251+
assertNull(spans["span"]!!.status)
252+
sut.finishEvent()
253+
assertEquals(SpanStatus.INTERNAL_ERROR, spans["span"]!!.status)
254+
}
255+
256+
@Test
257+
fun `when finishEvent, does not override running spans status if set`() {
258+
val sut = fixture.getSut()
259+
sut.startSpan("span")
260+
val spans = sut.getEventSpans()
261+
assertNull(spans["span"]!!.status)
262+
spans["span"]!!.status = SpanStatus.OK
263+
assertEquals(SpanStatus.OK, spans["span"]!!.status)
264+
sut.finishEvent()
265+
assertEquals(SpanStatus.OK, spans["span"]!!.status)
266+
}
267+
245268
@Test
246269
fun `setResponse set protocol and code in the breadcrumb and root span, and response in the hint`() {
247270
val sut = fixture.getSut()
@@ -443,6 +466,32 @@ class SentryOkHttpEventTest {
443466
assertEquals(rootSpan.spanId, responseBodySpan?.parentSpanId)
444467
}
445468

469+
@Test
470+
fun `finishSpan beforeFinish is called on span, parent and call root span`() {
471+
val sut = fixture.getSut()
472+
sut.startSpan(CONNECTION_EVENT)
473+
sut.startSpan(REQUEST_HEADERS_EVENT)
474+
sut.startSpan("random event")
475+
sut.finishSpan(REQUEST_HEADERS_EVENT) { it.status = SpanStatus.INTERNAL_ERROR }
476+
sut.finishSpan("random event") { it.status = SpanStatus.DEADLINE_EXCEEDED }
477+
sut.finishSpan(CONNECTION_EVENT)
478+
val spans = sut.getEventSpans()
479+
val connectionSpan = spans[CONNECTION_EVENT] as Span?
480+
val requestHeadersSpan = spans[REQUEST_HEADERS_EVENT] as Span?
481+
val randomEventSpan = spans["random event"] as Span?
482+
assertNotNull(connectionSpan)
483+
assertNotNull(requestHeadersSpan)
484+
assertNotNull(randomEventSpan)
485+
// requestHeadersSpan was finished with INTERNAL_ERROR
486+
assertEquals(SpanStatus.INTERNAL_ERROR, requestHeadersSpan.status)
487+
// randomEventSpan was finished with DEADLINE_EXCEEDED
488+
assertEquals(SpanStatus.DEADLINE_EXCEEDED, randomEventSpan.status)
489+
// requestHeadersSpan was finished with INTERNAL_ERROR, and it propagates to its parent
490+
assertEquals(SpanStatus.INTERNAL_ERROR, connectionSpan.status)
491+
// random event was finished last with DEADLINE_EXCEEDED, and it propagates to root call
492+
assertEquals(SpanStatus.DEADLINE_EXCEEDED, sut.callRootSpan!!.status)
493+
}
494+
446495
/** Retrieve all the spans started in the event using reflection. */
447496
private fun SentryOkHttpEvent.getEventSpans() = getProperty<MutableMap<String, ISpan>>("eventSpans")
448497
}

0 commit comments

Comments
 (0)