Skip to content

Commit 3740e7e

Browse files
authored
Change OkHttp sub-spans to span attributes (#3556)
* removed sub-span creation from SentryOkHttpEvent and replaced them with setData * removed http timeout handling in SentryOkHttpEventListener * SentryOkHttpInterceptor now closes the http call * renamed constants to make them nicer in the UI
1 parent aaf7418 commit 3740e7e

File tree

7 files changed

+220
-581
lines changed

7 files changed

+220
-581
lines changed

CHANGELOG.md

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

33
## Unreleased
44

5+
### Breaking Changes
6+
7+
- Change OkHttp sub-spans to span attributes ([#3556](https://github.com/getsentry/sentry-java/pull/3556))
8+
- This will reduce the number of spans created by the SDK
9+
510
### Fixes
611

712
- Use OpenTelemetry span name as fallback for transaction name ([#3557](https://github.com/getsentry/sentry-java/pull/3557))

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

Lines changed: 36 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,28 @@ import io.sentry.Hint
55
import io.sentry.IScopes
66
import io.sentry.ISpan
77
import io.sentry.SentryDate
8-
import io.sentry.SentryLevel
98
import io.sentry.SpanDataConvention
109
import io.sentry.TypeCheckHint
11-
import io.sentry.okhttp.SentryOkHttpEventListener.Companion.CONNECTION_EVENT
12-
import io.sentry.okhttp.SentryOkHttpEventListener.Companion.CONNECT_EVENT
13-
import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_BODY_EVENT
14-
import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEADERS_EVENT
15-
import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BODY_EVENT
16-
import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT
17-
import io.sentry.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT
1810
import io.sentry.util.Platform
1911
import io.sentry.util.UrlUtils
2012
import okhttp3.Request
2113
import okhttp3.Response
2214
import java.util.Locale
2315
import java.util.concurrent.ConcurrentHashMap
24-
import java.util.concurrent.RejectedExecutionException
16+
import java.util.concurrent.TimeUnit
2517
import java.util.concurrent.atomic.AtomicBoolean
2618

2719
private const val PROTOCOL_KEY = "protocol"
2820
private const val ERROR_MESSAGE_KEY = "error_message"
29-
private const val RESPONSE_BODY_TIMEOUT_MILLIS = 800L
3021
internal const val TRACE_ORIGIN = "auto.http.okhttp"
3122

3223
@Suppress("TooManyFunctions")
3324
internal class SentryOkHttpEvent(private val scopes: IScopes, private val request: Request) {
34-
private val eventSpans: MutableMap<String, ISpan> = ConcurrentHashMap()
25+
private val eventDates: MutableMap<String, SentryDate> = ConcurrentHashMap()
3526
private val breadcrumb: Breadcrumb
36-
internal val callRootSpan: ISpan?
27+
internal val callSpan: ISpan?
3728
private var response: Response? = null
3829
private var clientErrorResponse: Response? = null
39-
private val isReadingResponseBody = AtomicBoolean(false)
4030
private val isEventFinished = AtomicBoolean(false)
4131
private val url: String
4232
private val method: String
@@ -50,52 +40,52 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques
5040

5141
// We start the call span that will contain all the others
5242
val parentSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span
53-
callRootSpan = parentSpan?.startChild("http.client", "$method $url")
54-
callRootSpan?.spanContext?.origin = TRACE_ORIGIN
55-
urlDetails.applyToSpan(callRootSpan)
43+
callSpan = parentSpan?.startChild("http.client", "$method $url")
44+
callSpan?.spanContext?.origin = TRACE_ORIGIN
45+
urlDetails.applyToSpan(callSpan)
5646

5747
// We setup a breadcrumb with all meaningful data
5848
breadcrumb = Breadcrumb.http(url, method)
5949
breadcrumb.setData("host", host)
6050
breadcrumb.setData("path", encodedPath)
6151

62-
// We add the same data to the root call span
63-
callRootSpan?.setData("url", url)
64-
callRootSpan?.setData("host", host)
65-
callRootSpan?.setData("path", encodedPath)
66-
callRootSpan?.setData(SpanDataConvention.HTTP_METHOD_KEY, method.uppercase(Locale.ROOT))
52+
// We add the same data to the call span
53+
callSpan?.setData("url", url)
54+
callSpan?.setData("host", host)
55+
callSpan?.setData("path", encodedPath)
56+
callSpan?.setData(SpanDataConvention.HTTP_METHOD_KEY, method.uppercase(Locale.ROOT))
6757
}
6858

6959
/**
7060
* Sets the [Response] that will be sent in the breadcrumb [Hint].
71-
* Also, it sets the protocol and status code in the breadcrumb and the call root span.
61+
* Also, it sets the protocol and status code in the breadcrumb and the call span.
7262
*/
7363
fun setResponse(response: Response) {
7464
this.response = response
7565
breadcrumb.setData(PROTOCOL_KEY, response.protocol.name)
7666
breadcrumb.setData("status_code", response.code)
77-
callRootSpan?.setData(PROTOCOL_KEY, response.protocol.name)
78-
callRootSpan?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, response.code)
67+
callSpan?.setData(PROTOCOL_KEY, response.protocol.name)
68+
callSpan?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, response.code)
7969
}
8070

8171
fun setProtocol(protocolName: String?) {
8272
if (protocolName != null) {
8373
breadcrumb.setData(PROTOCOL_KEY, protocolName)
84-
callRootSpan?.setData(PROTOCOL_KEY, protocolName)
74+
callSpan?.setData(PROTOCOL_KEY, protocolName)
8575
}
8676
}
8777

8878
fun setRequestBodySize(byteCount: Long) {
8979
if (byteCount > -1) {
9080
breadcrumb.setData("request_content_length", byteCount)
91-
callRootSpan?.setData("http.request_content_length", byteCount)
81+
callSpan?.setData("http.request_content_length", byteCount)
9282
}
9383
}
9484

9585
fun setResponseBodySize(byteCount: Long) {
9686
if (byteCount > -1) {
9787
breadcrumb.setData("response_content_length", byteCount)
98-
callRootSpan?.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, byteCount)
88+
callSpan?.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, byteCount)
9989
}
10090
}
10191

@@ -107,44 +97,33 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques
10797
fun setError(errorMessage: String?) {
10898
if (errorMessage != null) {
10999
breadcrumb.setData(ERROR_MESSAGE_KEY, errorMessage)
110-
callRootSpan?.setData(ERROR_MESSAGE_KEY, errorMessage)
100+
callSpan?.setData(ERROR_MESSAGE_KEY, errorMessage)
111101
}
112102
}
113103

114-
/** Starts a span, if the callRootSpan is not null. */
115-
fun startSpan(event: String) {
116-
// Find the parent of the span being created. E.g. secureConnect is child of connect
117-
val parentSpan = findParentSpan(event)
118-
val span = parentSpan?.startChild("http.client.$event", "$method $url") ?: return
119-
if (event == RESPONSE_BODY_EVENT) {
120-
// We save this event is reading the response body, so that it will not be auto-finished
121-
isReadingResponseBody.set(true)
122-
}
123-
span.spanContext.origin = TRACE_ORIGIN
124-
eventSpans[event] = span
104+
/** Record event start if the callRootSpan is not null. */
105+
fun onEventStart(event: String) {
106+
callSpan ?: return
107+
eventDates[event] = scopes.options.dateProvider.now()
125108
}
126109

127-
/** Finishes a previously started span, and runs [beforeFinish] on it, on its parent and on the call root span. */
128-
fun finishSpan(event: String, beforeFinish: ((span: ISpan) -> Unit)? = null): ISpan? {
129-
val span = eventSpans[event] ?: return null
130-
val parentSpan = findParentSpan(event)
131-
beforeFinish?.invoke(span)
132-
moveThrowableToRootSpan(span)
133-
if (parentSpan != null && parentSpan != callRootSpan) {
134-
beforeFinish?.invoke(parentSpan)
135-
moveThrowableToRootSpan(parentSpan)
136-
}
137-
callRootSpan?.let { beforeFinish?.invoke(it) }
138-
span.finish()
139-
return span
110+
/** Record event finish and runs [beforeFinish] on the call span. */
111+
fun onEventFinish(event: String, beforeFinish: ((span: ISpan) -> Unit)? = null) {
112+
val eventDate = eventDates.remove(event) ?: return
113+
callSpan ?: return
114+
beforeFinish?.invoke(callSpan)
115+
val eventDurationNanos = scopes.options.dateProvider.now().diff(eventDate)
116+
callSpan.setData(event, TimeUnit.NANOSECONDS.toMillis(eventDurationNanos))
140117
}
141118

142-
/** Finishes the call root span, and runs [beforeFinish] on it. Then a breadcrumb is sent. */
143-
fun finishEvent(finishDate: SentryDate? = null, beforeFinish: ((span: ISpan) -> Unit)? = null) {
119+
/** Finishes the call span, and runs [beforeFinish] on it. Then a breadcrumb is sent. */
120+
fun finish(beforeFinish: ((span: ISpan) -> Unit)? = null) {
144121
// If the event already finished, we don't do anything
145122
if (isEventFinished.getAndSet(true)) {
146123
return
147124
}
125+
// We clear any date left, in case an event started, but never finished. Shouldn't happen.
126+
eventDates.clear()
148127
// We put data in the hint and send a breadcrumb
149128
val hint = Hint()
150129
hint.set(TypeCheckHint.OKHTTP_REQUEST, request)
@@ -153,75 +132,12 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques
153132
// We send the breadcrumb even without spans.
154133
scopes.addBreadcrumb(breadcrumb, hint)
155134

156-
// No span is created (e.g. no transaction is running)
157-
if (callRootSpan == null) {
158-
// We report the client error even without spans.
159-
clientErrorResponse?.let {
160-
SentryOkHttpUtils.captureClientError(scopes, it.request, it)
161-
}
162-
return
163-
}
164-
165-
// We forcefully finish all spans, even if they should already have been finished through finishSpan()
166-
eventSpans.values.filter { !it.isFinished }.forEach {
167-
moveThrowableToRootSpan(it)
168-
if (finishDate != null) {
169-
it.finish(it.status, finishDate)
170-
} else {
171-
it.finish()
172-
}
173-
}
174-
beforeFinish?.invoke(callRootSpan)
175-
// We report the client error here, after all sub-spans finished, so that it will be bound
176-
// to the root call span.
135+
callSpan?.let { beforeFinish?.invoke(it) }
136+
// We report the client error here so that it will be bound to the call span. We send it even if there is no running span.
177137
clientErrorResponse?.let {
178138
SentryOkHttpUtils.captureClientError(scopes, it.request, it)
179139
}
180-
if (finishDate != null) {
181-
callRootSpan.finish(callRootSpan.status, finishDate)
182-
} else {
183-
callRootSpan.finish()
184-
}
140+
callSpan?.finish()
185141
return
186142
}
187-
188-
/** Move any throwable from an inner span to the call root span. */
189-
private fun moveThrowableToRootSpan(span: ISpan) {
190-
if (span != callRootSpan && span.throwable != null && span.status != null) {
191-
callRootSpan?.throwable = span.throwable
192-
callRootSpan?.status = span.status
193-
span.throwable = null
194-
}
195-
}
196-
197-
private fun findParentSpan(event: String): ISpan? = when (event) {
198-
// PROXY_SELECT, DNS, CONNECT and CONNECTION are not children of one another
199-
SECURE_CONNECT_EVENT -> eventSpans[CONNECT_EVENT]
200-
REQUEST_HEADERS_EVENT -> eventSpans[CONNECTION_EVENT]
201-
REQUEST_BODY_EVENT -> eventSpans[CONNECTION_EVENT]
202-
RESPONSE_HEADERS_EVENT -> eventSpans[CONNECTION_EVENT]
203-
RESPONSE_BODY_EVENT -> eventSpans[CONNECTION_EVENT]
204-
else -> callRootSpan
205-
} ?: callRootSpan
206-
207-
fun scheduleFinish(timestamp: SentryDate) {
208-
try {
209-
scopes.options.executorService.schedule({
210-
if (!isReadingResponseBody.get() &&
211-
(eventSpans.values.all { it.isFinished } || callRootSpan?.isFinished != true)
212-
) {
213-
finishEvent(timestamp)
214-
}
215-
}, RESPONSE_BODY_TIMEOUT_MILLIS)
216-
} catch (e: RejectedExecutionException) {
217-
scopes.options
218-
.logger
219-
.log(
220-
SentryLevel.ERROR,
221-
"Failed to call the executor. OkHttp span will not be finished " +
222-
"automatically. Did you call Sentry.close()?",
223-
e
224-
)
225-
}
226-
}
227143
}

0 commit comments

Comments
 (0)