getsentry / sentry-kotlin-multiplatform

Sentry SDK for Kotlin Multiplatform
MIT License
132 stars 19 forks source link

Add ability to perform basic custom instrumentation with Transaction and Spans #83

Open buenaflor opened 1 year ago

buenaflor commented 1 year ago

This would allow developers to create custom transactions and spans in their code to capture information about their application's performance and behavior.

At the very least support:

Align implementation with:

### Blocked by
- [ ] Create Hub to manage KMP spans
- [ ] Create envelopes and send envelopes to native layers
Legion2 commented 1 year ago

Please also provide withTrace and withSpan kotlin extensions which make it easy to instrument code with an concise and kotin idomatic syntax. These extensions should also support kotlin coroutines and propagate context in coroutines correctly. I started an implementation my self, here is what I did sofar:

data class SpanContext(val span: Span) : AbstractCoroutineContextElement(SpanContext) {
    companion object Key : CoroutineContext.Key<SpanContext>
}

fun CoroutineContext.currentSpan(): Span {
    return get(SpanContext)?.span ?: throw IllegalStateException("No active Span in context")
}

suspend fun <T> withTrace(
    name: String,
    spanName: String,
    block: suspend CoroutineScope.() -> T
): T {
    val trace = startTrace(name, spanName)
    return withSpan(trace, block)
}

suspend fun <T> withSpan(name: String, block: suspend CoroutineScope.() -> T): T {
    val span = currentCoroutineContext().currentSpan()
    val childSpan = span.startChild(name)
    return withSpan(childSpan, block)
}

suspend fun <T> withSpan(span: Span, block: suspend CoroutineScope.() -> T): T {
    return try {
        withContext(SpanContext(span), block)
    } catch (e: Throwable) {
        if (e is CancellationException) {
            span.setStatus("cancelled")
        } else {
            span.setThrowable(e)
            span.setStatus("error")
        }
        throw e
    } finally {
        span.finish()
    }
}

This allows to add the context to the coroutine context, which ensures it is propagates in suspend code with structured concurrency. This means the child span must no be propagated manually. The extension functions allow to instrument code as follows:

suspend fun getData(): String {
  return withSpan("getData") {
    delay(1000)
    "foo"
  }
}