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.

MDC context is not passed correctly to retrofit client when using suspend functionality (coroutine)

See original GitHub issue

What kind of issue is this?

I faced a problem with latest retrofit with Kotlin suspend functions and passing MDC parameters to retrofit.

Problem is that when the MDC has parameters that I would like to see logged for tracing the calls in a transaction the retrofit logging will lose the MDC parameters when using the suspend functions.

Weird thing is that when the same call is done while using Call and a blocking resolving it with the execute method the MDC context is preserved so my best guess is that this has something to do with sharing the coroutineContext is not shared correctly with the kotlin application and retrofit client.

I see that there is a PR open for improving the CoroutineContext sharing. I haven’t tried that yet but I will comment if it changes anything in my sample application that I made. https://github.com/square/retrofit/pull/3240

Here’s the brief code example what I’m trying to achieve. Full example in the link above to the test repository.

    interface SampleClient {
        @GET("/api/call")
        suspend fun getSomething(): ResponseBody

        @GET("/api/call")
        fun getSomethingWithCall() : Call<ResponseBody>
    }

    @Test
    fun suspendClientLogEventsShouldContainMdcId() {
       ...
        val client = createRetrofitClient(mockWebServer)
        runBlocking {
            MDC.putCloseable("txId", expectedTxId).run {
                log.info("Message")
                client.getSomething()
            }
        }
     ....
    }

    @Test
    fun callClientLogEventsShouldContainMdcId() {
        ....
        val client = createRetrofitClient(mockWebServer)
        runBlocking {
            MDC.putCloseable("txId", expectedTxId).run {
                log.info("Message")
                client.getSomethingWithCall().execute()
            }
        }
     ....
    }

       fun createRetrofitClient(mockServer: MockWebServer): SampleClient {
        return Retrofit.Builder()
            .baseUrl(mockServer.url("/"))
            .client(
                createOkHttpClient()
            )
            .build()
            .create(SampleClient::class.java)

    }

    fun createOkHttpClient(): OkHttpClient {
        val logging = HttpLoggingInterceptor { message ->
            log.info(message)
        }
        logging.setLevel(HttpLoggingInterceptor.Level.BASIC)
        return OkHttpClient.Builder()
            .addInterceptor(logging)
            .build()
    }

logback.xml

<configuration scan="false">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%date %level tx-id=%X{txId} [%file:%line] %msg %n</pattern>
        </encoder>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

The first test will produce the following output to log: MDC txId is missing from the client calls.

2021-01-16 18:22:25,077 INFO tx-id=6f9456e6-aab8-44c3-b846-6f6c40d1773d [FailingMdcContextTest.kt:56] Message
2021-01-16 18:22:25,098 INFO tx-id= [FailingMdcContextTest.kt:115] --> GET http://localhost:56406/api/call
2021-01-16 18:22:25,121 INFO tx-id= [FailingMdcContextTest.kt:115] <-- 200 OK http://localhost:56406/api/call (23ms, 0-byte body)

The second test will produce the following output to log: MDC txId is present in the client calls.

2021-01-16 18:32:20,854 INFO tx-id=a4d2e9f9-54aa-452f-a31c-e7c88dd1085b [FailingMdcContextTest.kt:83] Message 
2021-01-16 18:32:20,870 INFO tx-id=a4d2e9f9-54aa-452f-a31c-e7c88dd1085b [FailingMdcContextTest.kt:114] --> GET http://localhost:56468/api/call 
2021-01-16 18:32:20,894 INFO tx-id=a4d2e9f9-54aa-452f-a31c-e7c88dd1085b [FailingMdcContextTest.kt:114] <-- 200 OK http://localhost:56468/api/call (24ms, 0-byte body) 

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:1
  • Comments:7 (2 by maintainers)

github_iconTop GitHub Comments

9reactions
niomcommented, Jan 22, 2021

Ok I have the basic understanding of this.

I can create a basic CallAdaptor and Factory but I have difficulties to figure out how to put the MDC context to the request in CallAdapters adapt method.

The closest I got was to create an anonymous inner class extending the retrofit2.Call and then just wrapping the original call to that and then trying to modify the Request but request it self is a final class and cannot be extended.

Recreating the Request by using the Request.Builder and then trying to hack the MDC context to the Request.tag is also a dead end. At the interceptor the request in chain does not have the MDC context copy in it any more.

I have a strong feeling that I’m trying to over engineer and extending the whole Call feels just a little over kill.

@swankjesse how can I stash the MDC context to the request since it’s final?

I created a branch to my repository: call-adapter that contains my trial and error.

4reactions
swankjessecommented, Jan 21, 2021

Yep! The fix for this looks like a CallAdapter that works with an Interceptor. The CallAdapter stashes MDC stuff into the request, and the interceptor unpacks it and applies it to whichever thread ends up performing the async request.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Unable to use Retrofit with Coroutines with KotlinExtensions
The function in your interface should return Data instead of Call<Data> since it's a suspend function. And since it's a suspend function, ...
Read more >
[Question] Reactive Spring with Kotlin coroutine with MDC ...
I wanted to create a context-based logger in a reactive Spring environment using Kotlin coroutines. ... README.md is contain responses of controller. ......
Read more >
Diff - HEAD^! - platform/external/kotlinx.coroutines - Google Git
+ +## Version 1.6.2 + +* Fixed a bug with `ThreadLocalElement` not being correctly updated when the most outer `suspend` function was called ......
Read more >
Reactor 3 Reference Guide
Reactor is a fully non-blocking reactive programming foundation for the JVM, with efficient demand management (in the form of managing “ ...
Read more >
WhatsNew 1.6 | Ktor Framework
Client. OkHttp and iOS: request with only-if-cache directive in Cache-Control header fails ... ResponseObserver does not respect MDC context.
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