Maintaining span context

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.

Automatic context creation

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()

Preventing new contexts

To prevent a span becoming the new current context when it is created, and so being the parent of future spans, set the makeCurrentContext span option:

BugsnagPerformance.startSpan("childless", SpanOptions.DEFAULTS.makeCurrentContext(false));
BugsnagPerformance.startSpan("childless", SpanOptions.DEFAULTS.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.DEFAULTS.makeCurrentContext(false));
Span span3 = BugsnagPerformance.startSpan("span3");
span3.end();
span2.end();
span1.end();
val span1 = startSpan("span1")
val span2 = startSpan(
    "span2",
    SpanOptions.DEFAULTS.makeCurrentContext(false),
)
val span3 = startSpan("span3")
span3.end()
span2.end()
span1.end()

Setting contexts manually

You can set a parent of a span manually using the within span option:

BugsnagPerformance.startSpan("child-span", SpanOptions.DEFAULTS.within(manualParentSpan));
BugsnagPerformance.startSpan("child-span", SpanOptions.DEFAULTS.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.DEFAULTS.within(null));
BugsnagPerformance.startSpan("top-level", SpanOptions.DEFAULTS.within(null))

Setting the context across threads

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.

In the example below, span2 would not be shown as a child of span1 without the additional span option:

Span span1 = BugsnagPerformance.startSpan("span1");

ThreadPoolExecutor executor = new ThreadPoolExecutor(/* */);
executor.submit(
    new Runnable() {
        @Override
        public void run() {
            Span span2 = BugsnagPerformance.startSpan("span2", 
                                                      SpanOptions.DEFAULTS.within(span1));
        }
    }
);
var span1 = BugsnagPerformance.startSpan("span1")
val executor = ThreadPoolExecutor(/* */)
executor.submit {
    var span2 = BugsnagPerformance.startSpan("span2", SpanOptions.DEFAULTS.within(span1))
}

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:

Runnable and callable wrappers

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)))

Thread pools and executors

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
)

Kotlin coroutines

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"
}

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
        }
    }
}