When viewing a single instance of a span in the BugSnag dashboard, a waterfall diagram will show you that instance’s children and their children, and so on, where the children are spans started from within the context of their parent. This page describes the tools available in the SDK to control these parent-child relationships.
By default, when a span is created it becomes the parent span for other spans that start on the same thread. We call this parent span the “current span context” and it remains until it is ended or another span becomes the current context.
For example, in the following snippet span1 becomes the current span context and span2 automatically becomes a child of span1. span3 then becomes the new current context, but with no parent span because span1 and span2 have already ended.
Span span1 = BugsnagPerformance.startSpan("span1");
Span span2 = BugsnagPerformance.startSpan("span2");
span2.end();
span1.end();
Span span3 = BugsnagPerformance.startSpan("span3");
span3.end();
var span1 = BugsnagPerformance.startSpan("span1")
var span2 = BugsnagPerformance.startSpan("span2")
span2.end()
span1.end()
var span3 = BugsnagPerformance.startSpan("span3")
span3.end()
To prevent a span becoming the new current context when it is created, and so being the parent of future spans:
BugsnagPerformance.startSpan("childless", SpanOptions.createAsCurrentContext(false));
BugsnagPerformance.startSpan("childless", SpanOptions.makeCurrentContext(false))
For example, in the following snippet span1 becomes the parent of both span2 and span3:
Span span1 = BugsnagPerformance.startSpan("span1");
Span span2 = BugsnagPerformance.startSpan("span2",
SpanOptions.createAsCurrentContext(false));
Span span3 = BugsnagPerformance.startSpan("span3");
span3.end();
span2.end();
span1.end();
val span1 = startSpan("span1")
val span2 = startSpan(
"span2",
SpanOptions.makeCurrentContext(false),
)
val span3 = startSpan("span3")
span3.end()
span2.end()
span1.end()
You can set a parent of a span manually using the within span option:
BugsnagPerformance.startSpan("child-span", SpanOptions.createWithin(manualParentSpan));
BugsnagPerformance.startSpan("child-span", SpanOptions.within(manualParentSpan))
If a span is known to be independent of any other open spans, you can also set the within span option to null, which will prevent it becoming a child:
BugsnagPerformance.startSpan("top-level", SpanOptions.createWithin(null));
BugsnagPerformance.startSpan("top-level", SpanOptions.within(null))
If you want operations on another thread to become child spans of the operation that spawns them (e.g. because an operation on the spawning thread is dependent on the spawned operations) then the current context must be passed to spawned threads when they are created so that spans inside the new thread become children of the parent operation.
Context can be passed to new threads using either a reference to a parent span or by obtaining the current context using:
SpanContext outsideContext = SpanContext.getCurrent();
val outsideContext = SpanContext.current
Span objects implement SpanContext and so can be used directly wherever a span context is required.
In the example below, threaded child span is manually parented under the span or context that was active when the task was dispatched:
Span outsideContext = BugsnagPerformance.startSpan("parent span");
// Or alternatively:
// SpanContext outsideContext = SpanContext.getCurrent();
ThreadPoolExecutor executor = new ThreadPoolExecutor(/* */);
executor.submit(
new Runnable() {
@Override
public void run() {
Span childSpan =
BugsnagPerformance.startSpan("threaded child span",
SpanOptions.createWithin(outsideContext));
}
}
);
val outsideContext = BugsnagPerformance.startSpan("parent span")
// Or alternatively:
// val outsideContext = SpanContext.current
val executor = ThreadPoolExecutor(/* */)
executor.submit {
val childSpan = BugsnagPerformance.startSpan("threaded child span",
SpanOptions.within(outsideContext))
}
Without this, the spans in the thread are not connected to the context of the spawning operation.
However we provide the following tools to pass the context across threads more easily:
The GlobalSpanContextStorage and HybridSpanContextStorage classes can be used to track the current context across threads automatically. The GlobalSpanContextStorage class uses a single global context stack for all threads, while the HybridSpanContextStorage class allows individual threads to request a thread-local context stack but falls back to a global context stack otherwise.
This allows spans to nest naturally across threads without needing to manually pass context references around, but requires consideration if multiple threads/coroutines are active at the same time to avoid unexpected parent-child relationships.
public class MyApp extends Application {
static {
SpanContext.setDefaultStorage(new HybridSpanContextStorage());
}
}
class MyApp : Application() {
companion object {
init {
SpanContext.defaultStorage = HybridSpanContextStorage()
}
}
}
To create a local context stack for a thread when using HybridSpanContextStorage:
HybridSpanContextStorage.tryStartThreadLocalTrace();
BugsnagPerformance.startSpan("first span in thread-local trace");
HybridSpanContextStorage.tryStartThreadLocalTrace()
BugsnagPerformance.startSpan("first span in thread-local trace")
You can use this approach alongside the following manual context passing approaches if you need more control over which threads share context.
We provide convenience functions for the Java standard Runnable and Callable interfaces. These can be used to wrap tasks when submitting them to Executors or other targets that are not aware of the BugSnag SpanContext:
runOnUiThread(SpanContext.getCurrent().wrap(new DisplayResults(result)));
runOnUiThread(SpanContext.current.wrap(DisplayResults(result)))
Also in the SDK are context-aware implementations of the standard java.util.concurrent executor classes that act as a drop-in replacement: setting the appropriate SpanContext before a task is executed, and cleaning up afterwards:
For java.util.concurrent.ThreadPoolExecutor:
ContextAwareThreadPoolExecutor executor =
new ContextAwareThreadPoolExecutor(2, 4, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10));
executor.submit(
new Runnable() {
@Override
public void run() {
// the current SpanContext is automatically propagated to here from the
// spawning thread
}
}
);
val executor = ContextAwareThreadPoolExecutor(2, 4, 10, TimeUnit.SECONDS,
ArrayBlockingQueue(10))
executor.submit {
// the current SpanContext is automatically propagated to here from the spawning thread
}
For java.util.concurrent.ScheduledThreadPoolExecutor:
ContextAwareScheduledThreadPoolExecutor scheduledExecutor =
new ContextAwareScheduledThreadPoolExecutor(2);
scheduledExecutor.schedule(
new Runnable() {
@Override
public void run() {
// the current SpanContext is automatically propagated to here from the
// spawning thread
}
}, 5, TimeUnit.SECONDS
);
val scheduledExecutor = ContextAwareScheduledThreadPoolExecutor(2)
scheduledExecutor.schedule(
{
// the current SpanContext is automatically propagated to here from the
// spawning thread
},
5, TimeUnit.SECONDS
)
If you use coroutines you can use our CoroutineContext implementation that, when added to a coroutine context, will propagate the current SpanContext across the coroutine suspend and resume boundaries:
First add the bugsnag-android-performance-coroutines module to your project:
dependencies {
implementation "com.bugsnag:bugsnag-android-performance:X.Y.Z"
implementation "com.bugsnag:bugsnag-android-performance-coroutines:X.Y.Z"
}
dependencies {
implementation("com.bugsnag:bugsnag-android-performance:X.Y.Z")
implementation("com.bugsnag:bugsnag-android-performance-coroutines:X.Y.Z")
}
Then you can use it as follows to pass through the current context to your coroutines:
launch(Dispatchers.IO + SpanContext.current.asCoroutineElement()) {
// the current SpanContext is carried over the "launch" boundary from the spawning thread
}
A simple CoroutineScope implementation is also provided to do this automatically:
class MyActivity: AppCompatActivity(), CoroutineScope by BugsnagPerformanceScope() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
launch {
// the current SpanContext is carried over the "launch" boundary from the
// spawning thread
}
}
}
If you want operations on another process or non-native layer (such as JavaScript spans in React Native) to be children or parents of your spans, you can encode any SpanContext into a string which can be parsed back into a parent span context by another layer of your app:
Span span = BugsnagPerformance.startSpan("remote-parent");
String encodedSpanContext = RemoteSpanContext.encodeAsTraceParent(span);
val span: Span = BugsnagPerformance.startSpan("remote-parent")
val encodedSpanContext: String = span.encodeAsTraceParent()
This works with any SpanContext, so if you don’t have a Span and need to encode the “current” context for the thread:
SpanContext currentSpanContext = SpanContext.getCurrent();
String encodedSpanContext = RemoteSpanContext.encodeAsTraceParent(currentSpanContext);
val currentSpanContext: SpanContext = SpanContext.current
val encodedSpanContext: String = currentSpanContext.encodeAsTraceParent()
The encodedSpanContext is a string formatted according to the traceparent header specification (version 00) and can be passed between any layers of your app (or system) and used as a parent when creating a new span:
String encodedSpanContext; // encoded traceparent header from external source
SpanContext remoteParent = RemoteSpanContext.parseTraceParent(encodedSpanContext);
Span span = BugsnagPerformance.startSpan("child-of-remote",
SpanOptions.within(remoteParent));
val encodedSpanContext: String // encoded traceparent header from external source
val remoteParent = RemoteSpanContext.parseTraceParent(encodedSpanContext)
val span: Span = BugsnagPerformance.startSpan("child-of-remote", SpanOptions.within(remoteParent))
The equivalent functionality for encoding and parsing span contexts can be found in the iOS and React Native guides.