Hi! We are using Spring Webflux with Kotlin Corout...
# spring
m
Hi! We are using Spring Webflux with Kotlin Coroutines and would like to reach MDC-like behaviour. If I understand correctly,
MDCContext
is not enough because MDC is ThreadLocal and one request can "pollute" another. Do you have any suggestions in the topic? I was thinking about using ReactorContext to store log data and retrieve it right before logging. What are your thoughts?
@sdeleuze sorry for tagging you but I was thinking that you are probably the most capable in the topic 🙂
t
did you look at observability, like micrometer? Observations are meant to propagate information through tags. I never used webflux myself, so cannot help for that specifically, but in general
Copy code
import java.util.concurrent.Executors
import io.micrometer.context.ContextExecutorService;
import io.micrometer.context.ContextRegistry;
import io.micrometer.context.ContextSnapshotFactory;
import io.micrometer.context.integration.Slf4jThreadLocalAccessor;
import io.micrometer.core.instrument.kotlin.asContextElement
import io.micrometer.observation.ObservationRegistry

import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async

val contextSnapshotFactory = ContextSnapshotFactory.builder().build()

val dispatcher = ContextExecutorService.wrap(Executors.newVirtualThreadPerTaskExecutor(), contextSnapshotFactory::captureAll).asCoroutineDispatcher()

val observationRegistry: ObservationRegistry // this one is a spring bean, created in `ObservationAutoConfiguration`, it should be available on all platform

CoroutineScope(dispatcher).async(observationRegistry.asContextElement()) { do things }
then the problem become feeding information to the observation registry, and that is the part that is probably more specific to webflux
thank you color 1
e
@Márton Matusek I think that the idea is that MDCContext should integrate with coroutine execution, such that when the Coroutine dispatcher starts executing your coroutine, the context should be restored to the ThreadLocal, and when the dispatcher starts using the Thread to execute some other coroutine, MDCContext would off-load it's ThreadLocals first. Check out the source here,
m
Thank you @Emil Kantis . I also checked the source but might misunderstand something. For me, the problem is that MDCContext seemingly does not solve the case that the same thread is working on a different request.
s
It is supported as of Spring Framework 7 and Spring Boot 4 to be released GA November 20th. See https://docs.spring.io/spring-framework/reference/7.0/languages/kotlin/coroutines.html#coroutines.propagation. With Boot 4 you just have to set
spring.reactor.context-propagation=auto
and it will work out of the box.
You can try Spring Boot 4.0.0-RC1 (4.0.0-RC2 will be released shortly) to check it works as expected. They are deployed to Maven Central so easy to test.
m
Thank you very much @sdeleuze. I tried it and it is working. I had to add https://github.com/micrometer-metrics/context-propagation/blob/f2afb15f63cca717e60[…]io/micrometer/context/integration/Slf4jThreadLocalAccessor.java to the ThreadLocalAccessors using:
Copy code
ContextRegistry.getInstance().registerThreadLocalAccessor(Slf4jThreadLocalAccessor())
Does this make sense? Also, I was able to make it work on Spring Boot 3.5.7 using
spring.reactor.context-propagation=auto
and adding
io.micrometer:context-propagation:1.1.3
as a dependency. I imagine that's included in Spring Boot 4 by default.
s
On Boot 3.x it may sometimes work but will likely break when the dispatcher change threads, that's not supported, so let's focus on Boot
4.0.0-RC1
. I am not an observability expert but I think if you use https://docs.spring.io/spring-boot/reference/actuator/tracing.html#actuator.micrometer-tracing.logging it should work with just
io.micrometer:context-propagation
dependency and
spring.reactor.context-propagation=auto
. Could you please explain your use case with more details and maybe share a git repo or zip archive with a minimal repro. I would like to understand why you need this
Slf4jThreadLocalAccessor
. I think there will be conflicts between what
Slf4jThreadLocalAccessor
does and what Spring Boot 4 + Micrometer context propagation does, so I don't think that's recommended to use it, but I need to understand your use case more to be sure.
m
Thank you! I will provide a code snippet first because the use case is very simple:
Copy code
@RestController
class TestController {

    init {
        ContextRegistry.getInstance().registerThreadLocalAccessor(Slf4jThreadLocalAccessor())
    }

    val logger: Logger = LoggerFactory.getLogger(this::class.java)

    @GetMapping("/test")
    suspend fun test(): String {
        MDC.put("testKey", "testValue")
        testTask()
        delay(50000)
        return "test"
    }

    suspend fun testTask() {
        logger.info("Test endpoint called")
    }

    @GetMapping("/other")
    suspend fun other(): String {
        MDC.put("otherKey", "otherValue")
        otherTask()
        return "other"
    }

    suspend fun otherTask() {
        // if the same thread executes the request that put testKey into MDC
        // the testValue will "leak" into this log from the other endpoint
        logger.info("Other endpoint called")
    }
}
I would like to be able to use MDC to automatically enrich logs without having to pass the keys down the call stack as parameters. The problem is (as the comment explains) that MDC uses ThreadLocal and even if I use MDCContext to propagate MDC context updates across coroutines, that does not solve the problem of the same thread working on different requests.
s
Ok so your use case is pretty different. IMO you don't need the context propagation support for Coroutines we are shipping in Spring Framework 7. You should use MDCContext but you need to take in account the guidelines shared in the documentation > For example, the following code will not work as expected: ... Instead, you should use withContext to capture the updated MDC context:
m
Thanks for the confirmation! MDCContext should be enough within a request but cross-request, I still need the ThreadLocalAccessors in my opinion.
s
Not sure, but not related to Spring itself IMO so I can't really help.
m
The Spring-related part is how controller methods are dispatched. I did not know how to hook into that and solve the MDC propagation cross-request 🙂
s
By default unconfined is used since the underlying infra is Reactive, but you see side effects because you are using unsupported constructs IMO.
If you use supported constructs, you should not see differences related to different kind of dispatching.
m
I think I am not using any unsupported constructs fortunately, the example I sent you reproduces my problem completely. I just wanted to solve MDC ThreadLocal values leaking between requests
But I might overcomplicate something.