├── .gitignore ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── log-correlation ├── README.md ├── build.gradle └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── projectreactor │ │ │ └── contextpropagationdemo │ │ │ ├── ContextPropagationDemoApplication.java │ │ │ ├── ExampleController.java │ │ │ └── WebClientController.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── io │ └── projectreactor │ └── contextpropagationdemo │ └── ContextPropagationDemoApplicationTests.java ├── settings.gradle ├── v01-basic ├── README.md ├── build.gradle └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── projectreactor │ │ │ └── contextpropagationdemo │ │ │ ├── ContextPropagationDemoApplication.java │ │ │ ├── ExampleController.java │ │ │ └── WebClientController.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── io │ └── projectreactor │ └── contextpropagationdemo │ └── ContextPropagationDemoApplicationTests.java ├── v02-add-context-propagation ├── README.md ├── build.gradle └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── projectreactor │ │ │ └── contextpropagationdemo │ │ │ ├── ContextPropagationDemoApplication.java │ │ │ └── WebClientController.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── io │ └── projectreactor │ └── contextpropagationdemo │ └── ContextPropagationDemoApplicationTests.java ├── v03-migrate-to-webflux ├── README.md ├── build.gradle └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── projectreactor │ │ │ └── contextpropagationdemo │ │ │ ├── ContextPropagationDemoApplication.java │ │ │ └── WebClientController.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── io │ └── projectreactor │ └── contextpropagationdemo │ └── ContextPropagationDemoApplicationTests.java ├── v04-revert-to-mvc-use-observation ├── README.md ├── build.gradle └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── projectreactor │ │ │ └── contextpropagationdemo │ │ │ ├── ContextPropagationDemoApplication.java │ │ │ └── WebClientController.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── io │ └── projectreactor │ └── contextpropagationdemo │ └── ContextPropagationDemoApplicationTests.java └── v05-reactor-core-micrometer ├── README.md ├── build.gradle └── src ├── main ├── java │ └── io │ │ └── projectreactor │ │ └── contextpropagationdemo │ │ ├── ContextPropagationDemoApplication.java │ │ └── WebClientController.java └── resources │ └── application.properties └── test └── java └── io └── projectreactor └── contextpropagationdemo └── ContextPropagationDemoApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group = 'io.projectreactor' 6 | version = '0.0.1-SNAPSHOT' 7 | sourceCompatibility = '17' 8 | 9 | repositories { 10 | mavenCentral() 11 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chemicL/context-propagation-demo/e100efcf3cfabcb2ace1bbc2426f79945e8778f8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /log-correlation/README.md: -------------------------------------------------------------------------------- 1 | This module is designed to show the most exciting features of automatic context 2 | propagation with Reactor. 3 | 4 | **The goal**: Learn how to manually configure log correlation. 5 | 6 | **Steps**: 7 | 8 | 1. Prepare. 9 | - Launch $JAVA_HOME/bin/jwebserver using JDK18+. 10 | 2. See it fail. 11 | - Run the app and `curl "localhost:8080/webClient?name=Eva"`. 12 | - Notice the correlation id gets lost. 13 | 3. Make it work. 14 | - build.gradle: Add `context-propagation` library. 15 | - main: Register a `ThreadLocalAccessor`. 16 | - main: Enable automatic context propagation. 17 | - Run and `curl "localhost:8080/webClient?name=Alice"`. 18 | 4. Try a reactive endpoint. 19 | - controller: change signature to `Mono`. 20 | - controller: Remove `block()`. 21 | - Run and `curl "localhost:8080/webClient?name=Bob"`. 22 | 5. Convert to WebFlux. 23 | - build.gradle: Remove `-web` dependency and refresh to run `WebFlux`. 24 | - main: Replace `Filter` with a `WebFilter`. 25 | - Run and `curl "localhost:8080/webClient?name=Carol"`. 26 | 6. Use Observation from Micrometer to correlate logs. 27 | - main: Remove `WebFilter`. 28 | - main: Remove `ThreadLocalAccessor`. 29 | - application.properties: Change log format to work with trace-id and span-id. 30 | - build.gradle: Replace `context-propagation` library with 31 | `io.micrometer:micrometer-tracing-bridge-otel`. -------------------------------------------------------------------------------- /log-correlation/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.1.2' 4 | id 'io.spring.dependency-management' version '1.1.2' 5 | } 6 | 7 | group 'io.projectreactor' 8 | version '0.0.1-SNAPSHOT' 9 | sourceCompatibility = '17' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 17 | // TODO: Remove -web dependency to switch from MVC to WebFlux, refresh project 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | implementation 'org.springframework.boot:spring-boot-starter-webflux' 20 | // TODO: Add context-propagation dependency 21 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 22 | testImplementation 'io.projectreactor:reactor-test' 23 | } 24 | 25 | tasks.named('test') { 26 | useJUnitPlatform() 27 | } -------------------------------------------------------------------------------- /log-correlation/src/main/java/io/projectreactor/contextpropagationdemo/ContextPropagationDemoApplication.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import jakarta.servlet.Filter; 4 | import org.slf4j.MDC; 5 | 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.annotation.Bean; 9 | 10 | @SpringBootApplication 11 | public class ContextPropagationDemoApplication { 12 | 13 | public static void main(String[] args) { 14 | // TODO: How to make all operators see 15 | // the modified MDC regardless of Thread switches? 16 | 17 | // TODO: How to propagate MDC across Thread boundaries? 18 | SpringApplication.run(ContextPropagationDemoApplication.class, args); 19 | } 20 | 21 | // TODO: How to make a filter for WebFlux? 22 | @Bean 23 | Filter correlationFilter() { 24 | return (request, response, chain) -> { 25 | try { 26 | String name = request.getParameter("name"); 27 | if (name != null) { 28 | MDC.put("cid", name); 29 | } 30 | chain.doFilter(request, response); 31 | } finally { 32 | MDC.remove("cid"); 33 | } 34 | }; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /log-correlation/src/main/java/io/projectreactor/contextpropagationdemo/ExampleController.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestParam; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | @RestController 11 | public class ExampleController { 12 | 13 | private static final Logger log 14 | = LoggerFactory.getLogger(ExampleController.class); 15 | 16 | @GetMapping("/hello") 17 | String hello(@RequestParam String name) { 18 | log.info("hello endpoint called"); 19 | return "Hello, " + name + "!"; 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /log-correlation/src/main/java/io/projectreactor/contextpropagationdemo/WebClientController.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import org.springframework.http.HttpEntity; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import org.springframework.web.reactive.function.client.WebClient; 11 | 12 | @RestController 13 | public class WebClientController { 14 | 15 | private static final Logger log 16 | = LoggerFactory.getLogger(WebClientController.class); 17 | 18 | private final WebClient webClient; 19 | 20 | public WebClientController(WebClient.Builder webClientBuilder) { 21 | this.webClient = webClientBuilder.baseUrl("http://127.0.0.1:8000/").build(); 22 | } 23 | 24 | @GetMapping("/webClient") 25 | String webClient(@RequestParam String name) { 26 | log.info("webClient endpoint called"); 27 | return webClient.get() 28 | .uri("/HELP.md") 29 | .retrieve() 30 | .toEntity(String.class) 31 | .doOnNext(entity -> log.info("Response status: {}", entity.getStatusCode())) 32 | .mapNotNull(HttpEntity::getBody) 33 | .block(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /log-correlation/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.pattern.level=%5p [cid:%X{cid}] 2 | # logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}] -------------------------------------------------------------------------------- /log-correlation/src/test/java/io/projectreactor/contextpropagationdemo/ContextPropagationDemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @SpringBootTest 8 | class ContextPropagationDemoApplicationTests { 9 | 10 | @Test 11 | void contextLoads() { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'context-propagation-demo' 2 | include 'log-correlation' 3 | include 'v01-basic' 4 | include 'v02-add-context-propagation' 5 | include 'v03-migrate-to-webflux' 6 | include 'v04-revert-to-mvc-use-observation' 7 | include 'v05-reactor-core-micrometer' 8 | -------------------------------------------------------------------------------- /v01-basic/README.md: -------------------------------------------------------------------------------- 1 | This module just shows you around, no code changes are required. 2 | 3 | **The goal**: Learn how log correlation id gets lost 4 | when transitioning to a reactive chain that transparently switches Threads. 5 | 6 | **Steps**: 7 | 8 | 1. build.gradle: Show dependencies. 9 | 2. application.properties: Show log format. 10 | 3. Show `Filter`. 11 | 4. Run and depict how correlation id gets lost when context is switched after 12 | `WebClient` response is delivered. -------------------------------------------------------------------------------- /v01-basic/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.1.2' 4 | id 'io.spring.dependency-management' version '1.1.2' 5 | } 6 | 7 | group 'io.projectreactor' 8 | version '0.0.1-SNAPSHOT' 9 | sourceCompatibility = '17' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 17 | implementation 'org.springframework.boot:spring-boot-starter-web' 18 | implementation 'org.springframework.boot:spring-boot-starter-webflux' 19 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 20 | testImplementation 'io.projectreactor:reactor-test' 21 | } 22 | 23 | tasks.named('test') { 24 | useJUnitPlatform() 25 | } -------------------------------------------------------------------------------- /v01-basic/src/main/java/io/projectreactor/contextpropagationdemo/ContextPropagationDemoApplication.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import jakarta.servlet.Filter; 4 | import org.slf4j.MDC; 5 | 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.annotation.Bean; 9 | 10 | @SpringBootApplication 11 | public class ContextPropagationDemoApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(ContextPropagationDemoApplication.class, args); 15 | } 16 | 17 | @Bean 18 | Filter correlationFilter() { 19 | return (request, response, chain) -> { 20 | try { 21 | String name = request.getParameter("name"); 22 | if (name != null) { 23 | MDC.put("cid", name); 24 | } 25 | chain.doFilter(request, response); 26 | } finally { 27 | MDC.remove("cid"); 28 | } 29 | }; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /v01-basic/src/main/java/io/projectreactor/contextpropagationdemo/ExampleController.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestParam; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | @RestController 11 | public class ExampleController { 12 | 13 | private static final Logger log 14 | = LoggerFactory.getLogger(ExampleController.class); 15 | 16 | @GetMapping("/hello") 17 | String hello(@RequestParam String name) { 18 | log.info("hello endpoint called"); 19 | return "Hello, " + name + "!"; 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /v01-basic/src/main/java/io/projectreactor/contextpropagationdemo/WebClientController.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import org.springframework.http.HttpEntity; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import org.springframework.web.reactive.function.client.WebClient; 11 | 12 | @RestController 13 | public class WebClientController { 14 | 15 | private static final Logger log = LoggerFactory.getLogger(WebClientController.class); 16 | 17 | private final WebClient webClient; 18 | 19 | public WebClientController(WebClient.Builder webClientBuilder) { 20 | this.webClient = webClientBuilder.baseUrl("http://127.0.0.1:8000/").build(); 21 | } 22 | 23 | @GetMapping("/webClient") 24 | String webClient(@RequestParam String name) { 25 | log.info("webClient endpoint called"); 26 | return webClient.get() 27 | .uri("/HELP.md") 28 | .retrieve() 29 | .toEntity(String.class) 30 | .doOnNext(entity -> log.info("Response status: {}", entity.getStatusCode())) 31 | .mapNotNull(HttpEntity::getBody) 32 | .block(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /v01-basic/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.pattern.level=%5p [cid:%X{cid}] -------------------------------------------------------------------------------- /v01-basic/src/test/java/io/projectreactor/contextpropagationdemo/ContextPropagationDemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @SpringBootTest 8 | class ContextPropagationDemoApplicationTests { 9 | 10 | @Test 11 | void contextLoads() { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /v02-add-context-propagation/README.md: -------------------------------------------------------------------------------- 1 | This module shows how to use the context-propagation library to keep correlation id 2 | across `Thread` boundaries in reactive pipelines in a blocking setup (Spring MVC). 3 | 4 | **The goal**: Learn the basics of Reactor context propagation. 5 | 6 | **Steps**: 7 | 8 | 1. `build.gradle`: New dependency added. 9 | 2. `main` method: `ThreadLocalAccessor` added. 10 | 3. `WebClient` chain uses `handle()` operator. 11 | 4. Use `.contextCapture()` to make correlation id appear in `handle()` lambda. 12 | 5. Use `Hooks.enableAutomaticContextPropagation()`. 13 | 6. Remove `contextCapture()` - `block()` automatically captures in automatic mode. -------------------------------------------------------------------------------- /v02-add-context-propagation/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.1.2' 4 | id 'io.spring.dependency-management' version '1.1.2' 5 | } 6 | 7 | group 'io.projectreactor' 8 | version '0.0.1-SNAPSHOT' 9 | sourceCompatibility = '17' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 17 | implementation 'org.springframework.boot:spring-boot-starter-web' 18 | implementation 'org.springframework.boot:spring-boot-starter-webflux' 19 | // [CHANGE] Added context-propagation library 20 | implementation 'io.micrometer:context-propagation:1.0.4' 21 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 22 | testImplementation 'io.projectreactor:reactor-test' 23 | } 24 | 25 | tasks.named('test') { 26 | useJUnitPlatform() 27 | } -------------------------------------------------------------------------------- /v02-add-context-propagation/src/main/java/io/projectreactor/contextpropagationdemo/ContextPropagationDemoApplication.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import io.micrometer.context.ContextRegistry; 4 | import jakarta.servlet.Filter; 5 | import org.slf4j.MDC; 6 | import reactor.core.publisher.Hooks; 7 | 8 | import org.springframework.boot.SpringApplication; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.springframework.context.annotation.Bean; 11 | 12 | @SpringBootApplication 13 | public class ContextPropagationDemoApplication { 14 | 15 | public static void main(String[] args) { 16 | // TODO: How to get operators other than handle/tap to log properly? 17 | 18 | // [CHANGE] Added accessor: 19 | ContextRegistry.getInstance().registerThreadLocalAccessor( 20 | "cid", 21 | () -> MDC.get("cid"), 22 | cid -> MDC.put("cid", cid), 23 | () -> MDC.remove("cid")); 24 | SpringApplication.run(ContextPropagationDemoApplication.class, args); 25 | } 26 | 27 | @Bean 28 | Filter correlationFilter() { 29 | return (request, response, chain) -> { 30 | try { 31 | String name = request.getParameter("name"); 32 | if (name != null) { 33 | MDC.put("cid", name); 34 | } 35 | chain.doFilter(request, response); 36 | } finally { 37 | MDC.remove("cid"); 38 | } 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /v02-add-context-propagation/src/main/java/io/projectreactor/contextpropagationdemo/WebClientController.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import org.springframework.http.HttpEntity; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RequestParam; 10 | import org.springframework.web.bind.annotation.RestController; 11 | import org.springframework.web.reactive.function.client.WebClient; 12 | 13 | @RestController 14 | public class WebClientController { 15 | 16 | private static final Logger log = LoggerFactory.getLogger(WebClientController.class); 17 | 18 | private final WebClient webClient; 19 | 20 | public WebClientController(WebClient.Builder webClientBuilder) { 21 | this.webClient = webClientBuilder.baseUrl("http://127.0.0.1:8000/").build(); 22 | } 23 | 24 | @GetMapping("/webClient") 25 | String webClient(@RequestParam String name) { 26 | log.info("webClient endpoint called"); 27 | return webClient.get() 28 | .uri("/HELP.md") 29 | .retrieve() 30 | .toEntity(String.class) 31 | .doOnNext(entity -> log.info("Response status: {}", entity.getStatusCode())) 32 | // [CHANGE] Using handle operator: 33 | .>handle((entity, sink) -> { 34 | log.info("Response status: {}", entity.getStatusCode()); 35 | sink.next(entity); 36 | }) 37 | // TODO: How to be able to correlate logs outside of 38 | // handle/tap? For example, in doOnNext() 39 | .mapNotNull(HttpEntity::getBody) 40 | .block(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /v02-add-context-propagation/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.pattern.level=[cid:%X{cid}] %5p -------------------------------------------------------------------------------- /v02-add-context-propagation/src/test/java/io/projectreactor/contextpropagationdemo/ContextPropagationDemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @SpringBootTest 8 | class ContextPropagationDemoApplicationTests { 9 | 10 | @Test 11 | void contextLoads() { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /v03-migrate-to-webflux/README.md: -------------------------------------------------------------------------------- 1 | This module shows how to properly use the scope of Reactor's Context and have 2 | correlation ids present. 3 | 4 | **The goal**: Understand Context scope. 5 | 6 | **Steps**: 7 | 8 | 1. `build.gradle`: Removed `-web` dependency to run `WebFlux` server instead of MVC. 9 | 2. Explain: Removed `Filter`, which can be replaced with a `WebFilter` later. But first: explain chained `Mono`s and `Context` scopes 10 | 3. Show: We're returning `Mono` instead of `String`. 11 | 4. Explain: Need for `Mono.defer()` - framework will subscribe at some point in time. 12 | 5. `contextCapture()` -> `contextWrite(Context.of("cid", name))`; 13 | Describe we now don't see the correlation id in the surrounding `Mono`, just the internal one, due to `Context` scope. 14 | 6. Remove `MDC.put()` and move `contextWrite` to surrounding `Mono`. 15 | 7. Remove `contextWrite()`, go to `WebFilter` in main class, uncomment `WebFilter`. -------------------------------------------------------------------------------- /v03-migrate-to-webflux/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.1.2' 4 | id 'io.spring.dependency-management' version '1.1.2' 5 | } 6 | 7 | group 'io.projectreactor' 8 | version '0.0.1-SNAPSHOT' 9 | sourceCompatibility = '17' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 17 | // [CHANGE] Removing the -web dependency 18 | // implementation 'org.springframework.boot:spring-boot-starter-web' 19 | implementation 'org.springframework.boot:spring-boot-starter-webflux' 20 | implementation 'io.micrometer:context-propagation:1.0.4' 21 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 22 | testImplementation 'io.projectreactor:reactor-test' 23 | } 24 | 25 | tasks.named('test') { 26 | useJUnitPlatform() 27 | } -------------------------------------------------------------------------------- /v03-migrate-to-webflux/src/main/java/io/projectreactor/contextpropagationdemo/ContextPropagationDemoApplication.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import io.micrometer.context.ContextRegistry; 4 | import org.slf4j.MDC; 5 | import reactor.core.publisher.Hooks; 6 | import reactor.util.context.Context; 7 | 8 | import org.springframework.boot.SpringApplication; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.web.server.WebFilter; 12 | 13 | @SpringBootApplication 14 | public class ContextPropagationDemoApplication { 15 | 16 | public static void main(String[] args) { 17 | Hooks.enableAutomaticContextPropagation(); 18 | ContextRegistry.getInstance().registerThreadLocalAccessor( 19 | "cid", 20 | () -> MDC.get("cid"), 21 | cid -> MDC.put("cid", cid), 22 | () -> MDC.remove("cid")); 23 | SpringApplication.run(ContextPropagationDemoApplication.class, args); 24 | } 25 | 26 | // @Bean 27 | // // [CHANGE] Filter -> WebFilter 28 | // WebFilter correlationFilter() { 29 | // return (exchange, chain) -> { 30 | // var names = exchange.getRequest().getQueryParams().get("name"); 31 | // if (names != null && names.size() > 0) { 32 | // return chain.filter(exchange) 33 | // .contextWrite(Context.of("cid", names.get(0))); 34 | // } 35 | // return chain.filter(exchange); 36 | // }; 37 | // } 38 | } 39 | -------------------------------------------------------------------------------- /v03-migrate-to-webflux/src/main/java/io/projectreactor/contextpropagationdemo/WebClientController.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.slf4j.MDC; 6 | import reactor.core.publisher.Mono; 7 | 8 | import org.springframework.http.HttpEntity; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestParam; 11 | import org.springframework.web.bind.annotation.RestController; 12 | import org.springframework.web.reactive.function.client.WebClient; 13 | 14 | @RestController 15 | public class WebClientController { 16 | 17 | private static final Logger log = LoggerFactory.getLogger(WebClientController.class); 18 | 19 | private final WebClient webClient; 20 | 21 | public WebClientController(WebClient.Builder webClientBuilder) { 22 | this.webClient = webClientBuilder.baseUrl("http://127.0.0.1:8000/").build(); 23 | } 24 | 25 | @GetMapping("/webClient") 26 | Mono webClient(@RequestParam String name) { 27 | // [CHANGE] Returning Mono instead of calling imperative code 28 | return Mono.defer(() -> { 29 | MDC.put("cid", name); // TODO: Do we need to write to MDC and capture? 30 | log.info("webClient endpoint called"); 31 | return webClient.get() 32 | .uri("/HELP.md") 33 | .retrieve() 34 | .toEntity(String.class) 35 | .doOnNext(entity -> log.info("Response status: {}", entity.getStatusCode())) 36 | .mapNotNull(HttpEntity::getBody) 37 | .contextCapture(); // TODO: Can we avoid writing to MDC and 38 | // immediately reading the value? 39 | // [CHANGE] Not calling block() 40 | }) 41 | // TODO: Can the outer Mono's (defer) Context be used? 42 | ; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /v03-migrate-to-webflux/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.pattern.level=[cid:%X{cid}] %5p -------------------------------------------------------------------------------- /v03-migrate-to-webflux/src/test/java/io/projectreactor/contextpropagationdemo/ContextPropagationDemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @SpringBootTest 8 | class ContextPropagationDemoApplicationTests { 9 | 10 | @Test 11 | void contextLoads() { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /v04-revert-to-mvc-use-observation/README.md: -------------------------------------------------------------------------------- 1 | This module uses Micrometer's `Observation` natively supported by Spring. 2 | 3 | **The goal**: Show how Observation from Micrometer is transparently accessible. 4 | 5 | **Steps**: 6 | 7 | 1. `build.gradle`: Back to MVC (`-web` dependency back). 8 | 2. `build.gradle`: Remove explicit `context-propagation` dependency - `micrometer-tracing-bridge-*` adds it. 9 | 3. Explain: When we add tracing bridge, tracing hooks into the `Observation` lifecycle. 10 | 4. Explain: Tracing also exposes trace-id and span-id in `MDC` 11 | 5. `application.properties`: Show log format without cid. 12 | 6. `main`: `ThreadLocalAccessor` removed, using `Observation` started by Spring Framework. 13 | 7. Explain: Controller has no logic for capturing or writing to `Context`, just plain app 14 | logic and logging where appropriate. 15 | 8. Optionally: remove `-web` dependency from `build.gradle`, refresh project, and demonstrate the `WebFlux` version works properly too. -------------------------------------------------------------------------------- /v04-revert-to-mvc-use-observation/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.1.2' 4 | id 'io.spring.dependency-management' version '1.1.2' 5 | } 6 | 7 | group 'io.projectreactor' 8 | version '0.0.1-SNAPSHOT' 9 | sourceCompatibility = '17' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 17 | // [CHANGE] Restore the -web dependency 18 | // TODO: Try removing it, refreshing, and run as a WebFlux application 19 | implementation 'org.springframework.boot:spring-boot-starter-web' 20 | implementation 'org.springframework.boot:spring-boot-starter-webflux' 21 | // [CHANGE] Remove context-propagation explicit import, it is part of the bridge 22 | // implementation 'io.micrometer:context-propagation:1.0.4' 23 | // [CHANGE] Add tracing-bridge 24 | implementation 'io.micrometer:micrometer-tracing-bridge-otel' 25 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 26 | testImplementation 'io.projectreactor:reactor-test' 27 | } 28 | 29 | tasks.named('test') { 30 | useJUnitPlatform() 31 | } -------------------------------------------------------------------------------- /v04-revert-to-mvc-use-observation/src/main/java/io/projectreactor/contextpropagationdemo/ContextPropagationDemoApplication.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import reactor.core.publisher.Hooks; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | 8 | @SpringBootApplication 9 | public class ContextPropagationDemoApplication { 10 | 11 | public static void main(String[] args) { 12 | Hooks.enableAutomaticContextPropagation(); 13 | // [CHANGE] No more correlation ID ("cid") accessor 14 | SpringApplication.run(ContextPropagationDemoApplication.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /v04-revert-to-mvc-use-observation/src/main/java/io/projectreactor/contextpropagationdemo/WebClientController.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import reactor.core.publisher.Mono; 6 | 7 | import org.springframework.http.HttpEntity; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestParam; 11 | import org.springframework.web.bind.annotation.RestController; 12 | import org.springframework.web.reactive.function.client.WebClient; 13 | 14 | @RestController 15 | public class WebClientController { 16 | 17 | private static final Logger log = LoggerFactory.getLogger(WebClientController.class); 18 | 19 | private final WebClient webClient; 20 | 21 | public WebClientController(WebClient.Builder webClientBuilder) { 22 | this.webClient = webClientBuilder.baseUrl("http://127.0.0.1:8000/") 23 | .build(); 24 | } 25 | 26 | @GetMapping("/webClient") 27 | // [CHANGE] Back to String. However, MVC also handles Mono/Flux signatures! (below) 28 | // [CHANGE] No more "name" param 29 | String webClient() { 30 | // [CHANGE] Back to imperative code 31 | // [CHANGE] No more "cid" 32 | log.info("webClient endpoint called"); 33 | return webClient.get() 34 | .uri("/HELP.md") 35 | .retrieve() 36 | .toEntity(String.class) 37 | .doOnNext(entity -> log.info("Response status: {}", entity.getStatusCode())) 38 | .mapNotNull(HttpEntity::getBody) 39 | // [CHANGE] calling block, which automatically captures 40 | .block(); 41 | } 42 | 43 | @GetMapping("/webClientReactive") 44 | Mono webClientReactive() { 45 | log.info("webClient endpoint called"); 46 | return webClient.get() 47 | .uri("/HELP.md") 48 | .retrieve() 49 | .toEntity(String.class) 50 | .doOnNext(entity -> log.info("Response status: {}", entity.getStatusCode())) 51 | .mapNotNull(HttpEntity::getBody); 52 | // not calling block in reactive endpoints 53 | } 54 | } -------------------------------------------------------------------------------- /v04-revert-to-mvc-use-observation/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # [CHANGE] Replaced [cid:%X{cid}] with traceId and spanId 2 | logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}] -------------------------------------------------------------------------------- /v04-revert-to-mvc-use-observation/src/test/java/io/projectreactor/contextpropagationdemo/ContextPropagationDemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @SpringBootTest 8 | class ContextPropagationDemoApplicationTests { 9 | 10 | @Test 11 | void contextLoads() { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /v05-reactor-core-micrometer/README.md: -------------------------------------------------------------------------------- 1 | This module shows advanced use of Micrometer's Observation via Reactor's integration. 2 | 3 | **The goal**: Show `Micrometer.observation()` to spawn Observations around a reactive chain. 4 | 5 | 1. `build.gradle`: We added `reactor-core-micrometer` module. 6 | 2. Explain: We need an `ObservationRegistry` bean injected into our controller. 7 | 3. Explain: We’re streaming lines from the file. 8 | 4. Explain: We’re attaching an `Observation` to `Mono` using `tap(Micrometer. 9 | observation())` to observe each line independently using a different tracing span. 10 | 5. Explain: When parent `Observation` is in scope, the above will create child `Observation`s 11 | 6. Explain: We need to capture current `Observation` into the Context for this 12 | relationship to work. This is no longer necessary, as `block` operator automatically 13 | captures, and a reactive chain has an `Observation` attached to the `Context`. -------------------------------------------------------------------------------- /v05-reactor-core-micrometer/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.1.2' 4 | id 'io.spring.dependency-management' version '1.1.2' 5 | } 6 | 7 | group 'io.projectreactor' 8 | version '0.0.1-SNAPSHOT' 9 | sourceCompatibility = '17' 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation 'org.springframework.boot:spring-boot-starter-actuator' 17 | // TODO: Try removing -web, refreshing, and run as a WebFlux application 18 | implementation 'org.springframework.boot:spring-boot-starter-web' 19 | implementation 'org.springframework.boot:spring-boot-starter-webflux' 20 | implementation 'io.micrometer:micrometer-tracing-bridge-otel' 21 | // [CHANGE] Add reactor-core-micrometer 22 | implementation 'io.projectreactor:reactor-core-micrometer' 23 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 24 | testImplementation 'io.projectreactor:reactor-test' 25 | } 26 | 27 | tasks.named('test') { 28 | useJUnitPlatform() 29 | } -------------------------------------------------------------------------------- /v05-reactor-core-micrometer/src/main/java/io/projectreactor/contextpropagationdemo/ContextPropagationDemoApplication.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import reactor.core.publisher.Hooks; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | 8 | @SpringBootApplication 9 | public class ContextPropagationDemoApplication { 10 | 11 | public static void main(String[] args) { 12 | Hooks.enableAutomaticContextPropagation(); 13 | SpringApplication.run(ContextPropagationDemoApplication.class, args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /v05-reactor-core-micrometer/src/main/java/io/projectreactor/contextpropagationdemo/WebClientController.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import java.util.stream.Collectors; 4 | 5 | import io.micrometer.observation.ObservationRegistry; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import reactor.core.observability.micrometer.Micrometer; 9 | import reactor.core.publisher.Mono; 10 | 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | import org.springframework.web.reactive.function.client.WebClient; 14 | 15 | @RestController 16 | public class WebClientController { 17 | 18 | private static final Logger log = LoggerFactory.getLogger(WebClientController.class); 19 | 20 | private final WebClient webClient; 21 | 22 | // [CHANGE] Injecting ObservationRegistry provided by Spring 23 | private final ObservationRegistry observationRegistry; 24 | 25 | public WebClientController(WebClient.Builder webClientBuilder, 26 | ObservationRegistry registry) { 27 | this.webClient = webClientBuilder.baseUrl("http://127.0.0.1:8000/") 28 | .build(); 29 | observationRegistry = registry; 30 | } 31 | 32 | @GetMapping("/webClient") 33 | String webClient() { 34 | log.info("webClient endpoint called"); 35 | return webClient.get() 36 | .uri("/HELP.md") 37 | .retrieve() 38 | // [CHANGE] toEntity -> bodyToFlux to get Flux of lines 39 | .bodyToFlux(String.class) 40 | // [CHANGE] Moved logging to flatMap below 41 | // [CHANGE] Apply Observation for each line 42 | .flatMap(line -> Mono.just(line) 43 | .handle((l, s) -> { 44 | log.info("Next line: {}", l); 45 | s.next(l); 46 | }).tap(Micrometer.observation(observationRegistry)) 47 | ) 48 | .collect(Collectors.joining("\n")) 49 | .block(); 50 | } 51 | 52 | @GetMapping("/webClientReactive") 53 | Mono webClientReactive() { 54 | log.info("webClient endpoint called"); 55 | return webClient.get() 56 | .uri("/HELP.md") 57 | .retrieve() 58 | // [CHANGE] toEntity -> bodyToFlux to get Flux of lines 59 | .bodyToFlux(String.class) 60 | // [CHANGE] Moved logging to flatMap below 61 | // [CHANGE] Apply Observation for each line 62 | .flatMap(line -> Mono.just(line) 63 | .handle((l, s) -> { 64 | log.info("Next line: {}", l); 65 | s.next(l); 66 | }).tap(Micrometer.observation(observationRegistry)) 67 | ) 68 | .collect(Collectors.joining("\n")); 69 | } 70 | } -------------------------------------------------------------------------------- /v05-reactor-core-micrometer/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}] -------------------------------------------------------------------------------- /v05-reactor-core-micrometer/src/test/java/io/projectreactor/contextpropagationdemo/ContextPropagationDemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.projectreactor.contextpropagationdemo; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @SpringBootTest 8 | class ContextPropagationDemoApplicationTests { 9 | 10 | @Test 11 | void contextLoads() { 12 | } 13 | 14 | } 15 | --------------------------------------------------------------------------------