Skip to content

Commit ff1782b

Browse files
authored
Add tracing support (#921)
* Add tracing interceptor * PR feedback: Remove bitdrift trace id header * iOS test fixes * PR feedback (JNI cleanups, default to w3c)
1 parent a69bda4 commit ff1782b

File tree

43 files changed

+2142
-11
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2142
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
**Added**
99

10-
- Nothing yet!
10+
- Added distributed tracing support for network instrumentation with W3C (`traceparent`), B3 Single (`b3`), and B3 Multi (`X-B3-*`) propagation formats.
1111

1212
**Changed**
1313

examples/swift/hello_world/LoggerCustomer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ final class LoggerCustomer: NSObject, URLSessionDelegate {
108108
requestFieldProvider: CustomNetworkFieldProvider(),
109109
responseFieldProvider: CustomNetworkResponseFieldProvider()
110110
), ],
111-
disableSwizzling: true
111+
disableSwizzling: false
112112
)
113113

114114
Logger.addField(withKey: "field_container_field_key", value: "field_container_value")

platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/okhttp/visitor/OkHttpEventListenerMethodVisitor.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ class OkHttpEventListenerMethodVisitor(
5353
) {
5454
private val captureOkHttpEventListenerFactory =
5555
"io/bitdrift/capture/network/okhttp/CaptureOkHttpEventListenerFactory"
56+
private val captureOkHttpTracingInterceptor =
57+
"io/bitdrift/capture/network/okhttp/CaptureOkHttpTracingInterceptor"
5658

5759
override fun onMethodEnter() {
5860
super.onMethodEnter()
@@ -64,6 +66,8 @@ class OkHttpEventListenerMethodVisitor(
6466
}
6567

6668
private fun addOverwritingEventListener() {
69+
addTracingInterceptor()
70+
6771
// Add the following call at the beginning of the constructor with the Builder parameter:
6872
// builder.eventListenerFactory(new CaptureOkHttpEventListenerFactory());
6973

@@ -95,9 +99,12 @@ class OkHttpEventListenerMethodVisitor(
9599
"(Lokhttp3/EventListener\$Factory;)Lokhttp3/OkHttpClient\$Builder;",
96100
false,
97101
)
102+
visitInsn(Opcodes.POP)
98103
}
99104

100105
private fun addProxyingEventListener() {
106+
addTracingInterceptor()
107+
101108
// Add the following call at the beginning of the constructor with the Builder parameter:
102109
// builder.eventListenerFactory(new CaptureOkHttpEventListenerFactory(builder.eventListenerFactory));
103110

@@ -141,5 +148,35 @@ class OkHttpEventListenerMethodVisitor(
141148
"(Lokhttp3/EventListener\$Factory;)Lokhttp3/OkHttpClient\$Builder;",
142149
false,
143150
)
151+
visitInsn(Opcodes.POP)
152+
}
153+
154+
private fun addTracingInterceptor() {
155+
// Add the following call at the beginning of the constructor with the Builder parameter:
156+
// builder.addInterceptor(new CaptureOkHttpTracingInterceptor());
157+
158+
// OkHttpClient.Builder is the parameter, retrieved here
159+
visitVarInsn(Opcodes.ALOAD, 1)
160+
161+
// Create CaptureOkHttpTracingInterceptor instance
162+
visitTypeInsn(Opcodes.NEW, captureOkHttpTracingInterceptor)
163+
visitInsn(Opcodes.DUP)
164+
visitMethodInsn(
165+
Opcodes.INVOKESPECIAL,
166+
captureOkHttpTracingInterceptor,
167+
"<init>",
168+
"()V",
169+
false,
170+
)
171+
172+
// Call addInterceptor(Interceptor)
173+
visitMethodInsn(
174+
Opcodes.INVOKEVIRTUAL,
175+
"okhttp3/OkHttpClient\$Builder",
176+
"addInterceptor",
177+
"(Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient\$Builder;",
178+
false,
179+
)
180+
visitInsn(Opcodes.POP)
144181
}
145182
}

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,14 @@ object Capture {
226226
val deviceId: String?
227227
get() = logger()?.deviceId
228228

229+
/**
230+
* Whether workflow-controlled tracing is currently active for this session.
231+
* Returns `null` prior to SDK start.
232+
*/
233+
@JvmStatic
234+
val isTracingActive: Boolean?
235+
get() = logger()?.isTracingActive
236+
229237
/**
230238
* Defines the initialization of a new session within the currently running logger
231239
* If no logger is started, this is a no-op.

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/CaptureJniLibrary.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ internal object CaptureJniLibrary : IBridge, IStreamingReportProcessor {
111111
*/
112112
external fun getDeviceId(loggerId: Long): String?
113113

114+
/**
115+
* Returns true when workflow-controlled tracing is active for the current session.
116+
*/
117+
external fun isTracingActive(loggerId: Long): Boolean
118+
114119
/**
115120
* Adds a field that should be attached to all logs emitted by the logger going forward.
116121
* If a field with a given key has already been registered with the logger, its value is

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/ILogger.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ interface ILogger {
3737
*/
3838
val deviceId: String
3939

40+
/**
41+
* Whether workflow-controlled tracing is currently active for this session.
42+
*/
43+
val isTracingActive: Boolean
44+
4045
/**
4146
* Defines the initialization of a new session within the current logger.
4247
*/
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// capture-sdk - bitdrift's client SDK
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
package io.bitdrift.capture
9+
10+
import io.bitdrift.capture.common.RuntimeConfig
11+
import io.bitdrift.capture.common.RuntimeFeature
12+
import io.bitdrift.capture.common.RuntimeStringConfig
13+
14+
internal interface IRuntimeProvider {
15+
fun isRuntimeFeatureEnabled(feature: RuntimeFeature): Boolean
16+
17+
fun getRuntimeConfigValue(config: RuntimeConfig): Int
18+
19+
fun getRuntimeStringConfigValue(config: RuntimeStringConfig): String
20+
}
21+
22+
internal object CaptureRuntimeProvider : IRuntimeProvider {
23+
override fun isRuntimeFeatureEnabled(feature: RuntimeFeature): Boolean =
24+
getLoggerProvider()?.isRuntimeFeatureEnabled(feature) ?: feature.defaultValue
25+
26+
override fun getRuntimeConfigValue(config: RuntimeConfig): Int =
27+
getLoggerProvider()?.getRuntimeConfigValue(config) ?: config.defaultValue
28+
29+
override fun getRuntimeStringConfigValue(config: RuntimeStringConfig): String =
30+
getLoggerProvider()?.getRuntimeStringConfigValue(config) ?: config.defaultValue
31+
32+
private fun getLoggerProvider(): IRuntimeProvider? = Capture.logger() as? IRuntimeProvider
33+
}

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/JniRuntime.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ package io.bitdrift.capture
1010
import io.bitdrift.capture.common.Runtime
1111
import io.bitdrift.capture.common.RuntimeConfig
1212
import io.bitdrift.capture.common.RuntimeFeature
13+
import io.bitdrift.capture.common.RuntimeStringConfig
1314

1415
internal class JniRuntime(
1516
private val logger: LoggerId,
1617
) : Runtime {
1718
override fun isEnabled(feature: RuntimeFeature): Boolean = Jni.isRuntimeEnabled(logger, feature.featureName, feature.defaultValue)
1819

1920
override fun getConfigValue(config: RuntimeConfig): Int = Jni.runtimeValue(logger, config.configName, config.defaultValue)
21+
22+
override fun getConfigValue(config: RuntimeStringConfig): String =
23+
Jni.runtimeStringValue(logger, config.configName, config.defaultValue)
2024
}
2125

2226
internal object Jni {
@@ -31,4 +35,10 @@ internal object Jni {
3135
variableName: String,
3236
defaultValue: Int,
3337
): Int
38+
39+
external fun runtimeStringValue(
40+
logger: LoggerId,
41+
variableName: String,
42+
defaultValue: String,
43+
): String
3444
}

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import androidx.lifecycle.ProcessLifecycleOwner
1616
import io.bitdrift.capture.attributes.ClientAttributes
1717
import io.bitdrift.capture.attributes.NetworkAttributes
1818
import io.bitdrift.capture.common.IWindowManager
19+
import io.bitdrift.capture.common.RuntimeConfig
1920
import io.bitdrift.capture.common.RuntimeFeature
21+
import io.bitdrift.capture.common.RuntimeStringConfig
2022
import io.bitdrift.capture.common.WindowManager
2123
import io.bitdrift.capture.error.ErrorReporterService
2224
import io.bitdrift.capture.error.IErrorReporter
@@ -94,7 +96,8 @@ internal class LoggerImpl(
9496
private val eventListenerDispatcher: CaptureDispatchers.CommonBackground = CaptureDispatchers.CommonBackground,
9597
windowManager: IWindowManager = WindowManager(errorHandler),
9698
) : IInternalLogger,
97-
ICompletedReportsProcessor {
99+
ICompletedReportsProcessor,
100+
IRuntimeProvider {
98101
@OptIn(ExperimentalBitdriftApi::class)
99102
internal val webViewConfiguration: WebViewConfiguration? = configuration.webViewConfiguration
100103

@@ -315,6 +318,9 @@ internal class LoggerImpl(
315318
override val deviceId: String
316319
get() = CaptureJniLibrary.getDeviceId(this.loggerId) ?: "unknown"
317320

321+
override val isTracingActive: Boolean
322+
get() = CaptureJniLibrary.isTracingActive(this.loggerId)
323+
318324
override fun startNewSession() {
319325
CaptureJniLibrary.startNewSession(this.loggerId)
320326
}
@@ -542,6 +548,12 @@ internal class LoggerImpl(
542548
)
543549
}
544550

551+
override fun isRuntimeFeatureEnabled(feature: RuntimeFeature): Boolean = runtime.isEnabled(feature)
552+
553+
override fun getRuntimeConfigValue(config: RuntimeConfig): Int = runtime.getConfigValue(config)
554+
555+
override fun getRuntimeStringConfigValue(config: RuntimeStringConfig): String = runtime.getConfigValue(config)
556+
545557
override fun logSessionReplayScreenshot(
546558
fields: Array<Field>,
547559
duration: Duration,

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/network/okhttp/CaptureOkHttpEventListener.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,11 @@ internal class CaptureOkHttpEventListener internal constructor(
102102
callStartTimeMs = clock.elapsedRealtime()
103103

104104
val request = call.request()
105-
val extraFields = requestExtraFieldsProvider.provideExtraFields(request)
105+
val extraFields =
106+
buildExtraFieldsWithOptionalTraceId(
107+
request,
108+
requestExtraFieldsProvider.provideExtraFields(request),
109+
)
106110

107111
val pathTemplateHeaderValues = request.headers.values("x-capture-path-template")
108112
val pathTemplate =
@@ -356,6 +360,12 @@ internal class CaptureOkHttpEventListener internal constructor(
356360
// successful or not to keep iOS and Android implementation in sync.
357361
val isSuccess = (statusCode in 200..<400)
358362

363+
val extraFields =
364+
buildExtraFieldsWithOptionalTraceId(
365+
request,
366+
responseExtraFieldsProvider.provideExtraFields(response),
367+
)
368+
359369
// Capture response URL attributes in case there was a redirect and attributes such as host,
360370
// path, and query have different values for the original request and the response.
361371
// https://square.github.io/okhttp/features/interceptors/#application-interceptors
@@ -380,7 +390,7 @@ internal class CaptureOkHttpEventListener internal constructor(
380390
response = httpResponse,
381391
durationMs = (clock.elapsedRealtime() - callStartTimeMs),
382392
metrics = getMetrics(),
383-
extraFields = responseExtraFieldsProvider.provideExtraFields(response),
393+
extraFields = extraFields,
384394
)
385395

386396
logger?.log(httpResponseInfo)
@@ -398,6 +408,8 @@ internal class CaptureOkHttpEventListener internal constructor(
398408
// them i.e., redirected request.
399409
val request = lastResponse?.request ?: call.request()
400410

411+
val extraFields = buildExtraFieldsWithOptionalTraceId(request)
412+
401413
// Capture response URL attributes in case there was a redirect and attributes such as host,
402414
// path, and query have different values for the original request and the response.
403415
// https://square.github.io/okhttp/features/interceptors/#application-interceptors
@@ -421,6 +433,7 @@ internal class CaptureOkHttpEventListener internal constructor(
421433
response = httpResponse,
422434
durationMs = (clock.elapsedRealtime() - callStartTimeMs),
423435
metrics = getMetrics(),
436+
extraFields = extraFields,
424437
)
425438
logger?.log(httpResponseInfo)
426439
}
@@ -497,6 +510,16 @@ internal class CaptureOkHttpEventListener internal constructor(
497510
}
498511
}
499512

513+
private fun buildExtraFieldsWithOptionalTraceId(
514+
request: Request,
515+
baseFields: Map<String, String> = emptyMap(),
516+
): Map<String, String> {
517+
val traceId = TracePropagation.extractSampledTraceId(request) ?: return baseFields
518+
val fields = baseFields.toMutableMap()
519+
fields[TracePropagation.TRACE_ID_FIELD_KEY] = traceId
520+
return fields
521+
}
522+
500523
private companion object {
501524
const val TRAFFIC_STATS_TAG = 0x0017D21F7
502525
}

0 commit comments

Comments
 (0)