question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Accessing throw-site MDC context in "global" exception handler

See original GitHub issue

I am trying to enrich an application with some MDC context in order to make debugging issues easier. My goal is to be able to log the MDC context from the point where an exception was thrown alongside the error.

In a non-coroutine world, that is easily possible by only popping the MDC entries when your block of code finishes successfully, but not on error. Your catch on the outermost level or a global exception handler would then still be able to “see” the MDC context from the throw site because it is left intact.

In a coroutine world, we have to wrap our suspend functions with withContext(MDCContext()) {} in order to bridge the ThreadLocal gap. But that means that whenever the coroutine resumes, the MDCContext will be restored to what it was when the MDCContext() was instantiated.

I tried using CoroutineExceptionHandler and supervisorScope to achieve the desired behaviour because I thought the coroutineContext passed to the CoroutineExceptionHandler would refer to the coroutine that failed (so I could get the MDCContext element in there), but it seems to be the supervisor job’s coroutineContext.

I could wrap every body inside withContext(MDCContext()) with a try/catch, log the error there and then rethrow, but that would mean I log the exception N times for N levels of nested MDCContexts, which is unsatisfying.

Any help on how I could achieve this will be greatly appreciated, I am out of ideas.

Please take a look at the following sample code to get an idea of what I am trying to achieve:

import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.slf4j.MDCContext
import kotlinx.coroutines.withContext
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.MDC

private val logger: Logger = LoggerFactory.getLogger("test")

suspend fun deepInSomeOtherCode() {
    MDC.put("nesting", "deeper")
    withContext(MDCContext()) {
        logger.info("deep in the code")
        throw Exception("Oh noes, a bug!")
    }
}

fun main() {
    runBlocking {
        try {
            MDC.put("additional", "info")
            withContext(MDCContext()) {
                // do something
                logger.info("I swear I am useful")
                deepInSomeOtherCode()
            }
        } catch (e: Exception) {
            // I want to be able to log the MDC Context from
            // where the exception occurred (the innermost coroutine context)
            // alongside with this error message. So MDC context should be
            // "additional": "info"
            // "nesting": "deeper"
            // but with this code, MDC will only contain "additional": "info"
            // because MDCContext() will always install the MDC the coroutine was created with
            // upon resuming, so you will always end up with the MDC from the outermost MDCContext()
            logger.error("An error occurred", e)
        }
    }
}

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:10 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
elizarovcommented, Nov 5, 2020

I’d suggest to enrich the exception with the context. This can be done by implementating a function like this and using it every time you need to adjust the context. Something like this:

suspend fun <T> withMDC(key: String, value: String, block: suspend CoroutineScope.() -> T): T {
    val newMap = MDC.getCopyOfContextMap() + mapOf(key, value)
    return try { 
        withContext(MDCContext(newMap), block)
    } catch(e: Throwable) {
       throw enrichExceptionWithContext(e, "$key = $value") // enrich & rethrow
    }
}

There are different approaches to implement enrichExceptionWithContext function that you can choose based on your project’s needs and preferences:

  • You can copy and recreate the exception similarly to how coroutines do it in debug mode, adding context information to the exception’s message.
  • You can keep some side-channel WeakHashMap to associate exceptions with the additional context and use this map in your logging code.
  • You can create a separate exception to record the context in its message and use e.addSuppressed(...) to attach this context to the exception before rethrowing it.

Does it help?

1reaction
elizarovcommented, Dec 3, 2020

There’s one idea that came to my mind. Saving it for the record here for further discussion: https://github.com/Kotlin/kotlinx.coroutines/issues/2426

Read more comments on GitHub >

github_iconTop Results From Across the Web

Preserve custom MDC attributes during exception-handling in ...
It seems to work well that clearing MDC on ServletRequestListener#requestDestroyed() , instead of on Filter . (Here is concrete example.).
Read more >
Improved Java Logging with Mapped Diagnostic Context (MDC)
In this tutorial, we will explore the use of Mapped Diagnostic Context (MDC) to improve the application logging. Mapped Diagnostic Context ...
Read more >
Chapter 8: Mapped Diagnostic Context - Logback - QOS.ch
package org.slf4j; public class MDC { //Put a context value as identified by key ... [] args) throws Exception { // You can...
Read more >
Java Logging with Mapped Diagnostic Context (MDC) - Medium
I have created a simple example of using MDC with Spring. In this example, I have registered a Spring Interceptor which will get...
Read more >
MDC: A Better Way of Logging - DZone Java
In this tutorial, we demonstrate an easier way to analyze logs when multiple clients access an application, using the mapped diagnostic ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found