reactor / reactor-core

Non-Blocking Reactive Foundation for the JVM

Home Page:http://projectreactor.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Spring Boot 3 webflux missing tracing information (e.g. traceId)

jhengy opened this issue · comments

Expected Behavior

After upgrading to Spring Boot 3, we had to switch our tracing library from spring cloud sleuth to micrometer tracing according to the migration guide https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide. It was mentioned that for reactor Hooks.enableAutomaticContextPropagation() needs to be called to enable automatic context propagation. We expect that doing so will help to propagate tracing information such as traceId in logs along the reactive chain.

Actual Behavior

Despite already calling Hooks.enableAutomaticContextPropagation(), reactive chain involving reactive web client fails to preserve tracing information such as traceId in SLF4J logs when the chain is invoked using .subscribe.

Reproductor

I have prepared a reproducer (spring boot test) to demonstrate the potential bug. Please refer to https://github.com/jhengy/my-simple-springboot-app/tree/demo-context-propagation

To solve the issue, it seems that we have to add .contextCapture() before .subscribe explicitly if reactive web client is used. However, for the same chain, using .block() instead of .subscribe() seems to preserve traceId out of the box without having to add .contextCapture. Is this an expected behaviour? Adding .contextCapture to all reactive chains ending with .subscribe seems a bit repetitive, is there a better and more convenient way (perhaps some kind of global setting) go about doing this? Thanks.

FYI, the original issue was raised in spring-projects/spring-framework#31993

Hey, @jhengy. Thanks for describing the issue and preparing a demo project. For most of the doubts, @bclozel gave a very comprehensive response. Especially the one about subscribe:

Calling subscribe from within a reactive chain means that it decouples completely both reactive streams. In your sample's case, the WebClient will be performing its task completely independently of the controller method where it's been called.

From the above perspective:

  • combined with the fact that the ThreadLocal state needs to reflect what is in the Context,
  • and the fact that we're dealing with a reactive scenario, as opposed to an imperative one

-> If you decide to create disconnected reactive chains that don't share the Context with the pipeline that causes the new one's assembly, it's up to you to decide to capture the ThreadLocal values. In fact you don't need to use contextCapture - you're actually better off to populate the context directly, as the application would behave differently if you used the default (limited) propagation mode and only logged in handle/tap operators.

We had a lot of back-and-forth on this case so it's not unusual that you ask this question :) So the justification behind .block() performing automatic capture is that in most cases when block is called, it is within an imperative scenario, where the ThreadLocal values are the source of truth, instead of the Reactor Context. So to help users of, say, Spring MVC to use WebClient effectively capturing the tracing data, we decided to introduce the automatic capture to the blocking terminal operators.

And in pure reactive scenarios we just assume that one pipeline connects to another by implicitly subscribing to their results, just like flatMap consumes a newly created Publisher instead of expecting the user to directly call subscribe on it. In fact, calling subscribe directly without getting a handle on its results creates an uncontrolled circumstance where you lose the ability of graceful cancelation or termination, so it's rather discouraged.

With the above, I'm closing the issue. However, let me just summon @marcingrzejszczak from the Micrometer team to highlight the fact that the Sleuth -> Micrometer Tracing migration document can be improved to mention the Spring Boot's configuration for Reactor's context propagation mode (spring.reactor.context-propagation).