From 911fb4838a091abe6d4da4a1f6a55682906715e5 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 20 Apr 2021 08:53:38 +0200 Subject: [PATCH 01/59] 3.1.0 initial commit --- benchmarks/pom.xml | 2 +- docs/pom.xml | 2 +- pom.xml | 2 +- spring-cloud-sleuth-api/pom.xml | 2 +- spring-cloud-sleuth-autoconfigure/pom.xml | 2 +- spring-cloud-sleuth-brave/pom.xml | 2 +- spring-cloud-sleuth-dependencies/pom.xml | 2 +- spring-cloud-sleuth-instrumentation/pom.xml | 2 +- spring-cloud-sleuth-samples/pom.xml | 2 +- .../spring-cloud-sleuth-sample-feign/pom.xml | 2 +- .../spring-cloud-sleuth-sample-messaging/pom.xml | 2 +- .../spring-cloud-sleuth-sample-test-core/pom.xml | 2 +- .../spring-cloud-sleuth-sample-websocket/pom.xml | 2 +- .../spring-cloud-sleuth-sample-zipkin/pom.xml | 2 +- spring-cloud-sleuth-samples/spring-cloud-sleuth-sample/pom.xml | 2 +- spring-cloud-sleuth-zipkin/pom.xml | 2 +- spring-cloud-starter-sleuth/pom.xml | 2 +- tests/brave/pom.xml | 2 +- .../pom.xml | 2 +- .../spring-cloud-sleuth-instrumentation-async-tests/pom.xml | 2 +- .../spring-cloud-sleuth-instrumentation-baggage-tests/pom.xml | 2 +- .../pom.xml | 2 +- .../spring-cloud-sleuth-instrumentation-feign-tests/pom.xml | 2 +- .../spring-cloud-sleuth-instrumentation-gateway-tests/pom.xml | 2 +- .../spring-cloud-sleuth-instrumentation-grpc-tests/pom.xml | 2 +- .../spring-cloud-sleuth-instrumentation-lettuce-tests/pom.xml | 2 +- .../spring-cloud-sleuth-instrumentation-messaging-tests/pom.xml | 2 +- .../brave/spring-cloud-sleuth-instrumentation-mvc-tests/pom.xml | 2 +- .../spring-cloud-sleuth-instrumentation-quartz-tests/pom.xml | 2 +- .../spring-cloud-sleuth-instrumentation-reactor-tests/pom.xml | 2 +- .../spring-cloud-sleuth-instrumentation-rxjava-tests/pom.xml | 2 +- .../pom.xml | 2 +- .../spring-cloud-sleuth-instrumentation-webflux-tests/pom.xml | 2 +- tests/brave/spring-cloud-sleuth-zipkin-tests/pom.xml | 2 +- tests/common/pom.xml | 2 +- tests/pom.xml | 2 +- 36 files changed, 36 insertions(+), 36 deletions(-) diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml index 34d1d4cd7..d7b910b9a 100644 --- a/benchmarks/pom.xml +++ b/benchmarks/pom.xml @@ -22,7 +22,7 @@ Benchmarks Benchmarks (JMH) org.springframework.cloud - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT benchmarks diff --git a/docs/pom.xml b/docs/pom.xml index f300fba2f..cf86faac6 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -21,7 +21,7 @@ org.springframework.cloud spring-cloud-sleuth - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT spring-cloud-sleuth-docs jar diff --git a/pom.xml b/pom.xml index 9b3809505..2fb7cb011 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ 4.0.0 spring-cloud-sleuth - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT pom Spring Cloud Sleuth Spring Cloud Sleuth diff --git a/spring-cloud-sleuth-api/pom.xml b/spring-cloud-sleuth-api/pom.xml index 223117cbd..59766a458 100644 --- a/spring-cloud-sleuth-api/pom.xml +++ b/spring-cloud-sleuth-api/pom.xml @@ -28,7 +28,7 @@ org.springframework.cloud spring-cloud-sleuth - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index a934b5b15..15f4a93a0 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -28,7 +28,7 @@ org.springframework.cloud spring-cloud-sleuth - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/spring-cloud-sleuth-brave/pom.xml b/spring-cloud-sleuth-brave/pom.xml index c3bb99eaf..1069e4dbe 100644 --- a/spring-cloud-sleuth-brave/pom.xml +++ b/spring-cloud-sleuth-brave/pom.xml @@ -28,7 +28,7 @@ org.springframework.cloud spring-cloud-sleuth - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/spring-cloud-sleuth-dependencies/pom.xml b/spring-cloud-sleuth-dependencies/pom.xml index 1e06ac341..3f2160aa4 100644 --- a/spring-cloud-sleuth-dependencies/pom.xml +++ b/spring-cloud-sleuth-dependencies/pom.xml @@ -26,7 +26,7 @@ spring-cloud-sleuth-dependencies - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT pom spring-cloud-sleuth-dependencies Spring Cloud Sleuth Dependencies diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index 0b4f970a9..f049e8a35 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -28,7 +28,7 @@ org.springframework.cloud spring-cloud-sleuth - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/spring-cloud-sleuth-samples/pom.xml b/spring-cloud-sleuth-samples/pom.xml index 55161d7bc..db7df8acb 100644 --- a/spring-cloud-sleuth-samples/pom.xml +++ b/spring-cloud-sleuth-samples/pom.xml @@ -28,7 +28,7 @@ org.springframework.cloud spring-cloud-sleuth - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-feign/pom.xml b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-feign/pom.xml index 09402560f..14012ee1d 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-feign/pom.xml +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-feign/pom.xml @@ -28,7 +28,7 @@ org.springframework.cloud spring-cloud-sleuth-samples - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-messaging/pom.xml b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-messaging/pom.xml index ca51608fd..75b4574a8 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-messaging/pom.xml +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-messaging/pom.xml @@ -28,7 +28,7 @@ org.springframework.cloud spring-cloud-sleuth-samples - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/pom.xml b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/pom.xml index 5a68fc05d..438700c2d 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/pom.xml +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-test-core/pom.xml @@ -28,7 +28,7 @@ org.springframework.cloud spring-cloud-sleuth-samples - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-websocket/pom.xml b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-websocket/pom.xml index 802007fed..2114093a3 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-websocket/pom.xml +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-websocket/pom.xml @@ -28,7 +28,7 @@ org.springframework.cloud spring-cloud-sleuth-samples - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/pom.xml b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/pom.xml index 2f555a022..85e6b2855 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/pom.xml +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample-zipkin/pom.xml @@ -28,7 +28,7 @@ org.springframework.cloud spring-cloud-sleuth-samples - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample/pom.xml b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample/pom.xml index c5a00b133..e4bb17d81 100644 --- a/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample/pom.xml +++ b/spring-cloud-sleuth-samples/spring-cloud-sleuth-sample/pom.xml @@ -28,7 +28,7 @@ org.springframework.cloud spring-cloud-sleuth-samples - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/spring-cloud-sleuth-zipkin/pom.xml b/spring-cloud-sleuth-zipkin/pom.xml index 289913b92..69642bfdb 100644 --- a/spring-cloud-sleuth-zipkin/pom.xml +++ b/spring-cloud-sleuth-zipkin/pom.xml @@ -28,7 +28,7 @@ org.springframework.cloud spring-cloud-sleuth - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/spring-cloud-starter-sleuth/pom.xml b/spring-cloud-starter-sleuth/pom.xml index bc574d2d2..7cf9452ec 100644 --- a/spring-cloud-starter-sleuth/pom.xml +++ b/spring-cloud-starter-sleuth/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-sleuth - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. spring-cloud-starter-sleuth diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index 9bf63fe26..eb3531c41 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-annotation-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-annotation-tests/pom.xml index 4582b2c81..4e2cd0abb 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-annotation-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-annotation-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-async-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-async-tests/pom.xml index f2949de3e..7bf3f3a3a 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-async-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-async-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/pom.xml index cddc90def..2516a9b57 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-tests/pom.xml index b2b6fbb9e..cb4d4ae68 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-feign-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-feign-tests/pom.xml index 7316615ab..7d7134fa4 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-feign-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-feign-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-gateway-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-gateway-tests/pom.xml index 87542def1..294339997 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-gateway-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-gateway-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-grpc-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-grpc-tests/pom.xml index 81c7a1911..5e9c81e38 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-grpc-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-grpc-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-lettuce-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-lettuce-tests/pom.xml index 6e5a5261e..bf14afc5c 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-lettuce-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-lettuce-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/pom.xml index ce3d0c390..e688ab54b 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/pom.xml index 6e497304f..3c0302cb6 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-quartz-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-quartz-tests/pom.xml index 8973c1d27..5f5f7a6a4 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-quartz-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-quartz-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-reactor-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-reactor-tests/pom.xml index c1c392076..3b16a8fea 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-reactor-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-reactor-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-rxjava-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-rxjava-tests/pom.xml index 5135bf12b..e5a18622d 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-rxjava-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-rxjava-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-scheduling-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-scheduling-tests/pom.xml index 83b00e163..da9a7a566 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-scheduling-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-scheduling-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-webflux-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-webflux-tests/pom.xml index 7b884de73..80245b044 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-webflux-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-webflux-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/brave/spring-cloud-sleuth-zipkin-tests/pom.xml b/tests/brave/spring-cloud-sleuth-zipkin-tests/pom.xml index 02bee2432..5a6baad01 100644 --- a/tests/brave/spring-cloud-sleuth-zipkin-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-zipkin-tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests-brave - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/common/pom.xml b/tests/common/pom.xml index fc31548be..1b79278cb 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth-tests - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/pom.xml b/tests/pom.xml index df2f5dcf9..04b8b898f 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT .. From 1207be7f4d9cf3b73bed47ecf6fa46f509e3c4bf Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 20 Apr 2021 09:08:20 +0200 Subject: [PATCH 02/59] Trying to fix github actions --- .github/workflows/maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 8b5ff26b1..95ed48397 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -29,4 +29,4 @@ jobs: restore-keys: | ${{ runner.os }}-maven- - name: Build with Maven - run: ./mvnw clean install -B -U + run: ./mvnw clean install -B -U -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false From f2bf396eba0c6a8d5fc91d2ac486a145b5a9dc01 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 20 Apr 2021 09:13:39 +0200 Subject: [PATCH 03/59] Fixes gh actions --- .github/workflows/maven.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 95ed48397..d74eae027 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -2,9 +2,9 @@ name: Build on: push: - branches: [ master ] + branches: [ 3.1.x ] pull_request: - branches: [ master ] + branches: [ 3.1.x ] jobs: build: From 6e8f86ed352d167276cb957b17d757268832f516 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 20 Apr 2021 10:03:36 +0000 Subject: [PATCH 04/59] Adds support for SC CircuitBreaker Reactive (#1914) * Adds support for SC CircuitBreaker Reactive * Changes following the review * Lazilly initializes the TraceFunction * Updated version fixes gh-1910 --- docs/src/main/asciidoc/integrations.adoc | 2 +- .../TraceCircuitBreakerAutoConfiguration.java | 13 +- .../TraceCircuitBreakerFactoryAspect.java | 7 +- .../circuitbreaker/TraceFunction.java | 1 + .../TraceReactiveCircuitBreaker.java | 138 +++++++++++++++++ ...ceReactiveCircuitBreakerFactoryAspect.java | 45 ++++++ .../circuitbreaker/TraceSupplier.java | 1 + tests/brave/pom.xml | 1 + .../pom.xml | 89 +++++++++++ ...eactiveCircuitBreakerIntegrationTests.java | 60 +++++++ .../ReactiveCircuitBreakerTests.java | 43 ++++++ .../src/test/resources/application.yml | 5 + tests/common/pom.xml | 5 + ...eactiveCircuitBreakerIntegrationTests.java | 146 ++++++++++++++++++ .../ReactiveCircuitBreakerTests.java | 94 +++++++++++ 15 files changed, 641 insertions(+), 9 deletions(-) create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreaker.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreakerFactoryAspect.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/pom.xml create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/resources/application.yml create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index b56360cb0..6da99eafc 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -565,5 +565,5 @@ IMPORTANT: The suggested approach to reactive programming and Sleuth is to use t This feature is available for all tracer implementations. -If you have Spring Cloud CircuitBreaker on the classpath, we will wrap the passed command `Supplier` and the fallback `Function` in its trace representations. +If you have Spring Cloud CircuitBreaker on the classpath, we will wrap the passed command `Supplier` and the fallback `Function` in its trace representations. We will also instrument the reactive implementation of the CircuitBreaker. In order to disable this instrumentation set `spring.sleuth.circuitbreaker.enabled` to `false`. \ No newline at end of file diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/circuitbreaker/TraceCircuitBreakerAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/circuitbreaker/TraceCircuitBreakerAutoConfiguration.java index e80b7d20c..f724943c5 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/circuitbreaker/TraceCircuitBreakerAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/circuitbreaker/TraceCircuitBreakerAutoConfiguration.java @@ -21,10 +21,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; +import org.springframework.cloud.sleuth.CurrentTraceContext; import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; import org.springframework.cloud.sleuth.instrument.circuitbreaker.TraceCircuitBreakerFactoryAspect; +import org.springframework.cloud.sleuth.instrument.circuitbreaker.TraceReactiveCircuitBreakerFactoryAspect; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -36,7 +37,6 @@ import org.springframework.context.annotation.Configuration; * @since 2.2.1 */ @Configuration(proxyBeanMethods = false) -@ConditionalOnClass(CircuitBreaker.class) @ConditionalOnBean(Tracer.class) @ConditionalOnProperty(value = "spring.sleuth.circuitbreaker.enabled", matchIfMissing = true) @EnableConfigurationProperties(SleuthCircuitBreakerProperties.class) @@ -44,8 +44,17 @@ import org.springframework.context.annotation.Configuration; public class TraceCircuitBreakerAutoConfiguration { @Bean + @ConditionalOnClass(name = "org.springframework.cloud.client.circuitbreaker.CircuitBreaker") TraceCircuitBreakerFactoryAspect traceCircuitBreakerFactoryAspect(Tracer tracer) { return new TraceCircuitBreakerFactoryAspect(tracer); } + @Bean + @ConditionalOnClass(name = { "reactor.core.publisher.Mono", + "org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker" }) + TraceReactiveCircuitBreakerFactoryAspect traceReactiveCircuitBreakerFactoryAspect(Tracer tracer, + CurrentTraceContext currentTraceContext) { + return new TraceReactiveCircuitBreakerFactoryAspect(tracer, currentTraceContext); + } + } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceCircuitBreakerFactoryAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceCircuitBreakerFactoryAspect.java index d5f3fb3a5..32505fbc7 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceCircuitBreakerFactoryAspect.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceCircuitBreakerFactoryAspect.java @@ -19,7 +19,6 @@ package org.springframework.cloud.sleuth.instrument.circuitbreaker; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Pointcut; import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; import org.springframework.cloud.sleuth.Tracer; @@ -39,11 +38,7 @@ public class TraceCircuitBreakerFactoryAspect { this.tracer = tracer; } - @Pointcut("execution(public * org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory.create(..))") - private void anyCircuitBreakerFactoryCreate() { - } // NOSONAR - - @Around("anyCircuitBreakerFactoryCreate()") + @Around("execution(public * org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory.create(..))") public Object wrapFactory(ProceedingJoinPoint pjp) throws Throwable { CircuitBreaker circuitBreaker = (CircuitBreaker) pjp.proceed(); return new TraceCircuitBreaker(circuitBreaker, this.tracer); diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java index 361400b51..d5f8f6df8 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java @@ -44,6 +44,7 @@ class TraceFunction implements Function { @Override public T apply(Throwable throwable) { + // TODO: This name needs to be better String name = this.delegate.getClass().getSimpleName(); Span span = this.span.get().name(name); Throwable tr = null; diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreaker.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreaker.java new file mode 100644 index 000000000..b283db6e8 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreaker.java @@ -0,0 +1,138 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.circuitbreaker; + +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; + +import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; + +class TraceReactiveCircuitBreaker implements ReactiveCircuitBreaker { + + private static final Log log = LogFactory.getLog(TraceReactiveCircuitBreaker.class); + + private final ReactiveCircuitBreaker delegate; + + private final Tracer tracer; + + private final CurrentTraceContext currentTraceContext; + + TraceReactiveCircuitBreaker(ReactiveCircuitBreaker delegate, Tracer tracer, + CurrentTraceContext currentTraceContext) { + this.delegate = delegate; + this.tracer = tracer; + this.currentTraceContext = currentTraceContext; + } + + @Override + public Mono run(Mono toRun) { + return runAndTraceMono(() -> this.delegate.run(toRun)); + } + + @Override + public Mono run(Mono toRun, Function> fallback) { + return runAndTraceMono( + () -> this.delegate.run(toRun, fallback != null ? new TraceFunction<>(this.tracer, fallback) : null)); + } + + @Override + public Flux run(Flux toRun) { + return runAndTraceFlux(() -> this.delegate.run(toRun)); + } + + @Override + public Flux run(Flux toRun, Function> fallback) { + return runAndTraceFlux( + () -> this.delegate.run(toRun, fallback != null ? new TraceFunction<>(this.tracer, fallback) : null)); + } + + private Mono runAndTraceMono(Supplier> mono) { + return Mono.deferContextual(contextView -> { + Span span = contextView.get(Span.class); + Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); + return mono.get().doOnError(span::error).doFinally(signalType -> { + span.end(); + scope.close(); + }); + }).contextWrite(this::enhanceContext); + } + + private Flux runAndTraceFlux(Supplier> flux) { + return Flux.deferContextual(contextView -> { + Span span = contextView.get(Span.class); + Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); + return flux.get().doOnError(span::error).doFinally(signalType -> { + span.end(); + scope.close(); + }); + }).contextWrite(this::enhanceContext); + } + + private Span spanFromContext(reactor.util.context.Context context) { + TraceContext traceContext = context.getOrDefault(TraceContext.class, null); + Span span = null; + if (traceContext == null) { + span = context.getOrDefault(Span.class, null); + } + if (traceContext == null && span == null) { + span = this.tracer.nextSpan(); + if (log.isDebugEnabled()) { + log.debug("There was no previous span in reactor context, created a new one [" + span + "]"); + } + } + else if (traceContext != null) { + // there was a previous span - we create a child one + try (CurrentTraceContext.Scope scope = this.currentTraceContext.maybeScope(traceContext)) { + if (log.isDebugEnabled()) { + log.debug("Found a trace context in reactor context [" + traceContext + "]"); + } + span = this.tracer.nextSpan(); + if (log.isDebugEnabled()) { + log.debug("Created a child span [" + span + "]"); + } + } + } + else { + if (log.isDebugEnabled()) { + log.debug("Found a span in reactor context [" + span + "]"); + } + span = this.tracer.nextSpan(span); + if (log.isDebugEnabled()) { + log.debug("Created a child span [" + span + "]"); + } + } + // TODO: Better name? + return span.name("function"); + } + + private Context enhanceContext(Context context) { + Span span = spanFromContext(context); + return context.put(Span.class, span).put(TraceContext.class, span.context()).put(Tracer.SpanInScope.class, + this.tracer.withSpan(span)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreakerFactoryAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreakerFactoryAspect.java new file mode 100644 index 000000000..ea2d99f1e --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreakerFactoryAspect.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.circuitbreaker; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Tracer; + +@Aspect +public class TraceReactiveCircuitBreakerFactoryAspect { + + private final Tracer tracer; + + private final CurrentTraceContext currentTraceContext; + + public TraceReactiveCircuitBreakerFactoryAspect(Tracer tracer, CurrentTraceContext currentTraceContext) { + this.tracer = tracer; + this.currentTraceContext = currentTraceContext; + } + + @Around("execution(public * org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory.create(..))") + public Object wrapFactory(ProceedingJoinPoint pjp) throws Throwable { + ReactiveCircuitBreaker circuitBreaker = (ReactiveCircuitBreaker) pjp.proceed(); + return new TraceReactiveCircuitBreaker(circuitBreaker, this.tracer, this.currentTraceContext); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceSupplier.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceSupplier.java index 3e6ffa2d5..503229151 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceSupplier.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceSupplier.java @@ -44,6 +44,7 @@ class TraceSupplier implements Supplier { @Override public T get() { + // TODO: This name needs to be better String name = this.delegate.getClass().getSimpleName(); Span span = this.span.get().name(name); Throwable tr = null; diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index eb3531c41..09478bd12 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -39,6 +39,7 @@ spring-cloud-sleuth-instrumentation-async-tests spring-cloud-sleuth-instrumentation-baggage-tests spring-cloud-sleuth-instrumentation-circuitbreaker-tests + spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests spring-cloud-sleuth-instrumentation-feign-tests spring-cloud-sleuth-instrumentation-gateway-tests spring-cloud-sleuth-instrumentation-grpc-tests diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/pom.xml new file mode 100644 index 000000000..43d9e1ab6 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/pom.xml @@ -0,0 +1,89 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests + jar + Spring Cloud Sleuth Brave Circuitbreaker Reactive Instrumentation Tests + Spring Cloud Sleuth Brave Circuitbreaker Reactive Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + + + + org.springframework.cloud + spring-cloud-sleuth-tests-common + ${project.version} + + + io.projectreactor + reactor-core + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.cloud + spring-cloud-starter-circuitbreaker-reactor-resilience4j + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-test + + + io.zipkin.brave + brave-tests + + + org.awaitility + awaitility + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java new file mode 100644 index 000000000..df5d1a819 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.circuitbreaker; + +import brave.sampler.Sampler; +import org.assertj.core.api.BDDAssertions; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +@SpringBootTest +@ContextConfiguration(classes = ReactiveCircuitBreakerIntegrationTests.Config.class) +public class ReactiveCircuitBreakerIntegrationTests + extends org.springframework.cloud.sleuth.instrument.circuitbreaker.ReactiveCircuitBreakerIntegrationTests { + + @Override + public void assertException(FinishedSpan finishedSpan) { + BDDAssertions.then(finishedSpan.getTags().get("error")).contains("boom"); + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + TestSpanHandler testSpanHandlerSupplier(brave.test.TestSpanHandler testSpanHandler) { + return new BraveTestSpanHandler(testSpanHandler); + } + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + brave.test.TestSpanHandler braveTestSpanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java new file mode 100644 index 000000000..7ac516cce --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.circuitbreaker; + +import org.assertj.core.api.BDDAssertions; + +import org.springframework.cloud.sleuth.brave.BraveTestTracing; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestTracingAware; + +public class ReactiveCircuitBreakerTests + extends org.springframework.cloud.sleuth.instrument.circuitbreaker.ReactiveCircuitBreakerTests { + + BraveTestTracing testTracing; + + @Override + public TestTracingAware tracerTest() { + if (this.testTracing == null) { + this.testTracing = new BraveTestTracing(); + } + return this.testTracing; + } + + @Override + public void additionalAssertions(FinishedSpan finishedSpan) { + BDDAssertions.then(finishedSpan.getTags().get("error")).contains("boom2"); + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/resources/application.yml b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/resources/application.yml new file mode 100644 index 000000000..f5756fce9 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests/src/test/resources/application.yml @@ -0,0 +1,5 @@ +logging.level.org.springframework.cloud: DEBUG +logging.level.com.netflix.discovery.InstanceInfoReplicator: ERROR +logging.level.org.springframework.cloud.sleuth.brave.instrument.web.client.feign: TRACE + +spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration, org.springframework.cloud.gateway.config.GatewayAutoConfiguration, org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration, org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration diff --git a/tests/common/pom.xml b/tests/common/pom.xml index 1b79278cb..628288865 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -89,6 +89,11 @@ spring-cloud-starter-circuitbreaker-resilience4j true + + org.springframework.cloud + spring-cloud-starter-circuitbreaker-reactor-resilience4j + true + org.springframework.boot spring-boot-starter-quartz diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java new file mode 100644 index 000000000..3ee120771 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerIntegrationTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.circuitbreaker; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory; +import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreakerFactory; +import org.springframework.cloud.sleuth.ScopedSpan; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +@ContextConfiguration(classes = ReactiveCircuitBreakerIntegrationTests.TestConfig.class) +public abstract class ReactiveCircuitBreakerIntegrationTests { + + @Autowired + TestSpanHandler spans; + + @Autowired + Tracer tracer; + + @Autowired + ReactiveCircuitBreakerFactory factory; + + @Autowired + CircuitService circuitService; + + @BeforeEach + public void setup() { + this.spans.clear(); + } + + @Test + public void should_pass_tracing_information_when_using_circuit_breaker() { + // given + Tracer tracer = this.tracer; + ScopedSpan scopedSpan = null; + try { + scopedSpan = tracer.startScopedSpan("start"); + // when + Span span = this.factory.create("name").run(Mono.defer(() -> Mono.just(tracer.currentSpan()))).block(); + + BDDAssertions.then(span).isNotNull(); + BDDAssertions.then(scopedSpan.context().traceId()).isEqualTo(span.context().traceId()); + } + finally { + scopedSpan.end(); + } + } + + @Test + public void should_pass_tracing_information_when_using_circuit_breaker_with_fallback() { + // when + BDDAssertions.then(this.circuitService.call().block()).isEqualTo("fallback"); + + BDDAssertions.then(this.spans).hasSize(2); + String traceId = this.circuitService.firstSpan.context().traceId(); + BDDAssertions.then(this.circuitService.secondSpan.context().traceId()).isEqualTo(traceId); + + FinishedSpan finishedSpan = this.spans.get(0); + BDDAssertions.then(finishedSpan.getName()).contains("CircuitBreakerIntegrationTests"); + + finishedSpan = this.spans.get(1); + BDDAssertions.then(finishedSpan.getName()).contains("function"); + } + + public void assertException(FinishedSpan finishedSpan) { + throw new UnsupportedOperationException("Implement this assertion"); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + public static class TestConfig { + + @Bean + ReactiveResilience4JCircuitBreakerFactory reactiveResilience4JCircuitBreakerFactory() { + return new ReactiveResilience4JCircuitBreakerFactory(); + } + + @Bean + CircuitService circuitService(ReactiveCircuitBreakerFactory reactiveCircuitBreakerFactory, Tracer tracer) { + return new CircuitService(reactiveCircuitBreakerFactory, tracer); + } + + } + + static class CircuitService { + + private static final Logger log = LoggerFactory.getLogger(CircuitService.class); + + private final ReactiveCircuitBreakerFactory factory; + + private final Tracer tracer; + + Span firstSpan; + + Span secondSpan; + + CircuitService(ReactiveCircuitBreakerFactory factory, Tracer tracer) { + this.factory = factory; + this.tracer = tracer; + } + + Mono call() { + return this.factory.create("circuit").run(Mono.defer(() -> { + this.firstSpan = this.tracer.currentSpan(); + log.info(" Hello from consumer", + this.tracer.currentSpan().context().traceId()); + return Mono.error(new IllegalStateException("boom")); + }), throwable -> { + this.secondSpan = this.tracer.currentSpan(); + log.info(" Hello from producer", + this.tracer.currentSpan().context().traceId()); + return Mono.just("fallback"); + }); + } + + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java new file mode 100644 index 000000000..021fbcd9e --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/ReactiveCircuitBreakerTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.circuitbreaker; + +import java.util.concurrent.atomic.AtomicReference; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory; +import org.springframework.cloud.sleuth.ScopedSpan; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestTracingAwareSupplier; + +public abstract class ReactiveCircuitBreakerTests implements TestTracingAwareSupplier { + + @Test + public void should_pass_tracing_information_when_using_circuit_breaker() { + // given + Tracer tracer = tracerTest().tracing().tracer(); + ScopedSpan scopedSpan = null; + try { + scopedSpan = tracer.startScopedSpan("start"); + // when + Span span = new TraceReactiveCircuitBreaker(new ReactiveResilience4JCircuitBreakerFactory().create("name"), + tracer, tracerTest().tracing().currentTraceContext()) + .run(Mono.defer(() -> Mono.just(tracer.currentSpan()))).block(); + + BDDAssertions.then(span).isNotNull(); + BDDAssertions.then(scopedSpan.context().traceId()).isEqualTo(span.context().traceId()); + } + finally { + scopedSpan.end(); + } + } + + @Test + public void should_pass_tracing_information_when_using_circuit_breaker_with_fallback() { + // given + Tracer tracer = tracerTest().tracing().tracer(); + AtomicReference first = new AtomicReference<>(); + AtomicReference second = new AtomicReference<>(); + ScopedSpan scopedSpan = null; + try { + scopedSpan = tracer.startScopedSpan("start"); + // when + BDDAssertions.thenThrownBy(() -> new TraceReactiveCircuitBreaker( + new ReactiveResilience4JCircuitBreakerFactory().create("name"), tracer, + tracerTest().tracing().currentTraceContext()).run(Mono.defer(() -> { + first.set(tracer.currentSpan()); + throw new IllegalStateException("boom"); + }), throwable -> { + second.set(tracer.currentSpan()); + throw new IllegalStateException("boom2"); + }).block()).isInstanceOf(IllegalStateException.class).hasMessageContaining("boom2"); + + BDDAssertions.then(tracerTest().handler().reportedSpans()).hasSize(2); + BDDAssertions.then(first.get()).isNotNull(); + BDDAssertions.then(second.get()).isNotNull(); + BDDAssertions.then(scopedSpan.context().traceId()).isEqualTo(first.get().context().traceId()); + BDDAssertions.then(scopedSpan.context().traceId()).isEqualTo(second.get().context().traceId()); + BDDAssertions.then(first.get().context().spanId()).isNotEqualTo(second.get().context().spanId()); + + FinishedSpan finishedSpan = tracerTest().handler().reportedSpans().get(1); + BDDAssertions.then(finishedSpan.getName()).contains("function"); + additionalAssertions(finishedSpan); + } + finally { + scopedSpan.end(); + } + } + + public void additionalAssertions(FinishedSpan finishedSpan) { + throw new UnsupportedOperationException("Assert errors"); + } + +} From 2fca60a112e360b6913ed7b2b9d12c768ce751ad Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 21 Apr 2021 10:11:28 +0200 Subject: [PATCH 05/59] Added spring-cloud-task support fixes gh-1903 --- .editorconfig | 4 + pom.xml | 8 ++ .../cloud/sleuth/SpanAndScope.java | 44 +++++++ .../cloud/sleuth/ThreadLocalSpan.java | 90 +++++++++++++++ spring-cloud-sleuth-autoconfigure/pom.xml | 5 + ...aceApplicationRunnerBeanPostProcessor.java | 47 ++++++++ ...aceCommandLineRunnerBeanPostProcessor.java | 47 ++++++++ .../task/TraceTaskAutoConfiguration.java | 61 ++++++++++ ...itional-spring-configuration-metadata.json | 8 +- .../main/resources/META-INF/spring.factories | 1 + spring-cloud-sleuth-instrumentation/pom.xml | 5 + .../messaging/TracingChannelInterceptor.java | 68 ++--------- .../task/TraceApplicationRunner.java | 65 +++++++++++ .../task/TraceCommandLineRunner.java | 64 +++++++++++ .../task/TraceTaskExecutionListener.java | 89 +++++++++++++++ tests/brave/pom.xml | 1 + .../pom.xml | 4 - .../pom.xml | 81 +++++++++++++ .../task/SpringCloudTaskIntegrationTests.java | 53 +++++++++ .../src/test/resources/application.yml | 1 + tests/common/pom.xml | 5 + .../task/SpringCloudTaskIntegrationTests.java | 107 ++++++++++++++++++ 22 files changed, 792 insertions(+), 66 deletions(-) create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/ThreadLocalSpan.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceApplicationRunnerBeanPostProcessor.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceCommandLineRunnerBeanPostProcessor.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceTaskAutoConfiguration.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceApplicationRunner.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceCommandLineRunner.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceTaskExecutionListener.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-task-tests/pom.xml create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-task-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/task/SpringCloudTaskIntegrationTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-task-tests/src/test/resources/application.yml create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/task/SpringCloudTaskIntegrationTests.java diff --git a/.editorconfig b/.editorconfig index ddda9782f..ffe385c6d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,9 @@ root = true +[*] +end_of_line = crlf +insert_final_newline = true + [*.java] indent_style = tab indent_size = 4 diff --git a/pom.xml b/pom.xml index 2fb7cb011..286d8b4e4 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,7 @@ 3.1.3-SNAPSHOT 3.0.3-SNAPSHOT 3.0.3-SNAPSHOT + 2.3.2-SNAPSHOT 5.13.2 0.32.0 2.3.4.RELEASE @@ -228,6 +229,13 @@ pom import + + org.springframework.cloud + spring-cloud-task-dependencies + ${spring-cloud-task.version} + pom + import + org.springframework.security.oauth.boot spring-security-oauth2-autoconfigure diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java new file mode 100644 index 000000000..e18e028d7 --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth; + +/** + * Container object for {@link Span} and its corresponding {@link Tracer.SpanInScope}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class SpanAndScope { + + private final Span span; + + private final Tracer.SpanInScope scope; + + public SpanAndScope(Span span, Tracer.SpanInScope scope) { + this.span = span; + this.scope = scope; + } + + public Span getSpan() { + return this.span; + } + + public Tracer.SpanInScope getScope() { + return this.scope; + } + +} diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/ThreadLocalSpan.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/ThreadLocalSpan.java new file mode 100644 index 000000000..aa2c0226b --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/ThreadLocalSpan.java @@ -0,0 +1,90 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth; + +import java.util.Deque; +import java.util.NoSuchElementException; +import java.util.concurrent.LinkedBlockingDeque; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Represents a {@link Span} stored in thread local. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class ThreadLocalSpan { + + private static final Log log = LogFactory.getLog(ThreadLocalSpan.class); + + private final ThreadLocal threadLocalSpan = new ThreadLocal<>(); + + private final Deque spans = new LinkedBlockingDeque<>(); + + private final Tracer tracer; + + public ThreadLocalSpan(Tracer tracer) { + this.tracer = tracer; + } + + /** + * Sets given span and scope. + * @param span - span to be put in scope + */ + public void set(Span span) { + Tracer.SpanInScope spanInScope = this.tracer.withSpan(span); + SpanAndScope newSpanAndScope = new SpanAndScope(span, spanInScope); + SpanAndScope scope = this.threadLocalSpan.get(); + if (scope != null) { + this.spans.addFirst(scope); + } + this.threadLocalSpan.set(newSpanAndScope); + } + + /** + * @return currently stored span and scope + */ + public SpanAndScope get() { + return this.threadLocalSpan.get(); + } + + /** + * Removes the current span from thread local and brings back the previous span to the + * current thread local. + */ + public void remove() { + this.threadLocalSpan.remove(); + if (this.spans.isEmpty()) { + return; + } + try { + SpanAndScope span = this.spans.removeFirst(); + if (log.isDebugEnabled()) { + log.debug("Took span [" + span + "] from thread local"); + } + this.threadLocalSpan.set(span); + } + catch (NoSuchElementException ex) { + if (log.isTraceEnabled()) { + log.trace("Failed to remove a span from the queue", ex); + } + } + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index 15f4a93a0..769757ac6 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -103,6 +103,11 @@ spring-cloud-context true + + org.springframework.cloud + spring-cloud-starter-task + true + io.reactivex rxjava diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceApplicationRunnerBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceApplicationRunnerBeanPostProcessor.java new file mode 100644 index 000000000..5efd90f4c --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceApplicationRunnerBeanPostProcessor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.task; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.ApplicationRunner; +import org.springframework.cloud.sleuth.instrument.task.TraceApplicationRunner; + +/** + * Registers beans related to task scheduling. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceApplicationRunnerBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + public TraceApplicationRunnerBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ApplicationRunner && !(bean instanceof TraceApplicationRunner)) { + return new TraceApplicationRunner(this.beanFactory, (ApplicationRunner) bean, beanName); + } + return bean; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceCommandLineRunnerBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceCommandLineRunnerBeanPostProcessor.java new file mode 100644 index 000000000..9fc69d7da --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceCommandLineRunnerBeanPostProcessor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.task; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.cloud.sleuth.instrument.task.TraceCommandLineRunner; + +/** + * Registers beans related to task scheduling. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceCommandLineRunnerBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + public TraceCommandLineRunnerBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof CommandLineRunner && !(bean instanceof TraceCommandLineRunner)) { + return new TraceCommandLineRunner(this.beanFactory, (CommandLineRunner) bean, beanName); + } + return bean; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceTaskAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceTaskAutoConfiguration.java new file mode 100644 index 000000000..591ca5941 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceTaskAutoConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.task; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.task.TraceTaskExecutionListener; +import org.springframework.cloud.task.listener.TaskExecutionListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Registers beans related to Spring Cloud Task scheduling. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(TaskExecutionListener.class) +@ConditionalOnProperty(value = "spring.sleuth.task.enabled", matchIfMissing = true) +@ConditionalOnBean(Tracer.class) +@AutoConfigureAfter(BraveAutoConfiguration.class) +public class TraceTaskAutoConfiguration { + + @Bean + TraceTaskExecutionListener traceTaskExecutionListener(Tracer tracer, + @Value("${spring.application.name:default}") String appName) { + return new TraceTaskExecutionListener(tracer, appName); + } + + @Bean + static TraceCommandLineRunnerBeanPostProcessor traceCommandLineRunnerBeanPostProcessor(BeanFactory beanFactory) { + return new TraceCommandLineRunnerBeanPostProcessor(beanFactory); + } + + @Bean + static TraceApplicationRunnerBeanPostProcessor traceApplicationRunnerBeanPostProcessor(BeanFactory beanFactory) { + return new TraceApplicationRunnerBeanPostProcessor(beanFactory); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index d655251ff..e46fecde3 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -128,7 +128,13 @@ { "name": "spring.sleuth.integration.enabled", "type": "java.lang.Boolean", - "description": "Enable Spring Integration sleuth instrumentation.", + "description": "Enable Spring Integration instrumentation.", + "defaultValue": true + }, + { + "name": "spring.sleuth.task.enabled", + "type": "java.lang.Boolean", + "description": "Enable Spring Cloud Task instrumentation.", "defaultValue": true } ] diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index 1c5ba3eba..5e902cd8a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -6,6 +6,7 @@ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncDefaultAu org.springframework.cloud.sleuth.autoconfig.instrument.circuitbreaker.TraceCircuitBreakerAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.rxjava.TraceRxJavaAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.quartz.TraceQuartzAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.task.TraceTaskAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.web.TraceWebAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.web.client.TraceWebClientAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.web.client.feign.TraceFeignClientAutoConfiguration,\ diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index f049e8a35..6aff38d57 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -127,6 +127,11 @@ spring-cloud-starter-gateway true + + org.springframework.cloud + spring-cloud-starter-task + true + org.aspectj aspectjrt diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java index 9640fdb2a..ed9d3780d 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java @@ -18,8 +18,6 @@ package org.springframework.cloud.sleuth.instrument.messaging; import java.util.Iterator; import java.util.Map; -import java.util.NoSuchElementException; -import java.util.concurrent.LinkedBlockingDeque; import java.util.function.Function; import org.apache.commons.logging.Log; @@ -28,6 +26,8 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeansException; import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.ThreadLocalSpan; import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.propagation.Propagator; import org.springframework.cloud.stream.binder.BinderType; @@ -107,7 +107,7 @@ public final class TracingChannelInterceptor extends ChannelInterceptorAdapter private final Propagator propagator; - private final ThreadLocalSpan threadLocalSpan = new ThreadLocalSpan(); + private final ThreadLocalSpan threadLocalSpan; private final Function remoteServiceNameMapper; @@ -115,6 +115,7 @@ public final class TracingChannelInterceptor extends ChannelInterceptorAdapter Propagator.Setter setter, Propagator.Getter getter, Function remoteServiceNameMapper, MessageSpanCustomizer messageSpanCustomizer) { this.tracer = tracer; + this.threadLocalSpan = new ThreadLocalSpan(tracer); this.propagator = propagator; this.injector = setter; this.extractor = getter; @@ -163,8 +164,7 @@ public final class TracingChannelInterceptor extends ChannelInterceptorAdapter } private void setSpanInScope(Span span) { - Tracer.SpanInScope spanInScope = this.tracer.withSpan(span); - this.threadLocalSpan.set(new SpanAndScope(span, spanInScope)); + this.threadLocalSpan.set(span); if (log.isDebugEnabled()) { log.debug("Put span in scope " + span); } @@ -362,8 +362,8 @@ public final class TracingChannelInterceptor extends ChannelInterceptorAdapter if (spanAndScope == null) { return; } - Span span = spanAndScope.span; - Tracer.SpanInScope scope = spanAndScope.scope; + Span span = spanAndScope.getSpan(); + Tracer.SpanInScope scope = spanAndScope.getScope(); if (span.isNoop()) { if (log.isDebugEnabled()) { log.debug("Span " + span + " is noop - will stope the scope"); @@ -424,57 +424,3 @@ public final class TracingChannelInterceptor extends ChannelInterceptorAdapter } } - -class SpanAndScope { - - final Span span; - - final Tracer.SpanInScope scope; - - SpanAndScope(Span span, Tracer.SpanInScope scope) { - this.span = span; - this.scope = scope; - } - -} - -class ThreadLocalSpan { - - private static final Log log = LogFactory.getLog(ThreadLocalSpan.class); - - final ThreadLocal threadLocalSpan = new ThreadLocal<>(); - - final LinkedBlockingDeque spans = new LinkedBlockingDeque<>(); - - void set(SpanAndScope spanAndScope) { - SpanAndScope scope = this.threadLocalSpan.get(); - if (scope != null) { - this.spans.addFirst(scope); - } - this.threadLocalSpan.set(spanAndScope); - } - - SpanAndScope get() { - return this.threadLocalSpan.get(); - } - - void remove() { - this.threadLocalSpan.remove(); - if (this.spans.isEmpty()) { - return; - } - try { - SpanAndScope span = this.spans.removeFirst(); - if (log.isDebugEnabled()) { - log.debug("Took span [" + span + "] from thread local"); - } - this.threadLocalSpan.set(span); - } - catch (NoSuchElementException ex) { - if (log.isTraceEnabled()) { - log.trace("Failed to remove a span from the queue", ex); - } - } - } - -} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceApplicationRunner.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceApplicationRunner.java new file mode 100644 index 000000000..4e3f6c77d --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceApplicationRunner.java @@ -0,0 +1,65 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.task; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; + +/** + * Trace representation of a {@link ApplicationRunner}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceApplicationRunner implements ApplicationRunner { + + private final BeanFactory beanFactory; + + private final ApplicationRunner delegate; + + private final String beanName; + + private Tracer tracer; + + public TraceApplicationRunner(BeanFactory beanFactory, ApplicationRunner delegate, String beanName) { + this.beanFactory = beanFactory; + this.delegate = delegate; + this.beanName = beanName; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + Span span = tracer().nextSpan().name(this.beanName); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + this.delegate.run(args); + } + finally { + span.end(); + } + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceCommandLineRunner.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceCommandLineRunner.java new file mode 100644 index 000000000..70b9ee320 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceCommandLineRunner.java @@ -0,0 +1,64 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.task; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; + +/** + * Trace representation of a {@link CommandLineRunner}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceCommandLineRunner implements CommandLineRunner { + + private final BeanFactory beanFactory; + + private final CommandLineRunner delegate; + + private final String beanName; + + private Tracer tracer; + + public TraceCommandLineRunner(BeanFactory beanFactory, CommandLineRunner delegate, String beanName) { + this.beanFactory = beanFactory; + this.delegate = delegate; + this.beanName = beanName; + } + + @Override + public void run(String... args) throws Exception { + Span span = tracer().nextSpan().name(this.beanName); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + this.delegate.run(args); + } + finally { + span.end(); + } + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceTaskExecutionListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceTaskExecutionListener.java new file mode 100644 index 000000000..d50d7ab00 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceTaskExecutionListener.java @@ -0,0 +1,89 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.task; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.ThreadLocalSpan; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.task.listener.TaskExecutionListener; +import org.springframework.cloud.task.repository.TaskExecution; +import org.springframework.core.Ordered; + +/** + * Sets the span upon starting and closes it upon ending a task. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceTaskExecutionListener implements TaskExecutionListener, Ordered { + + private static final Log log = LogFactory.getLog(TraceTaskExecutionListener.class); + + private final Tracer tracer; + + private final ThreadLocalSpan threadLocalSpan; + + private final String projectName; + + public TraceTaskExecutionListener(Tracer tracer, String projectName) { + this.tracer = tracer; + this.threadLocalSpan = new ThreadLocalSpan(tracer); + this.projectName = projectName; + } + + @Override + public void onTaskStartup(TaskExecution taskExecution) { + Span span = this.tracer.nextSpan().name(this.projectName).start(); + this.threadLocalSpan.set(span); + if (log.isDebugEnabled()) { + log.debug("Put the span [" + span + "] to thread local"); + } + } + + @Override + public void onTaskEnd(TaskExecution taskExecution) { + SpanAndScope spanAndScope = this.threadLocalSpan.get(); + Span span = spanAndScope.getSpan(); + span.end(); + spanAndScope.getScope().close(); + if (log.isDebugEnabled()) { + log.debug("Removed the [" + span + "] from thread local"); + } + } + + @Override + public void onTaskFailed(TaskExecution taskExecution, Throwable throwable) { + SpanAndScope spanAndScope = this.threadLocalSpan.get(); + Span span = spanAndScope.getSpan(); + span.error(throwable); + span.end(); + spanAndScope.getScope().close(); + if (log.isDebugEnabled()) { + log.debug("Removed the [" + span + "] from thread local and added error"); + } + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE; + } + +} diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index 09478bd12..8796d03bf 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -50,6 +50,7 @@ spring-cloud-sleuth-instrumentation-reactor-tests spring-cloud-sleuth-instrumentation-rxjava-tests spring-cloud-sleuth-instrumentation-scheduling-tests + spring-cloud-sleuth-instrumentation-task-tests spring-cloud-sleuth-instrumentation-webflux-tests spring-cloud-sleuth-zipkin-tests diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-scheduling-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-scheduling-tests/pom.xml index da9a7a566..45d3de776 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-scheduling-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-scheduling-tests/pom.xml @@ -51,10 +51,6 @@ - - org.springframework.cloud - spring-cloud-starter-sleuth - org.springframework.cloud spring-cloud-starter-sleuth diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-task-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-task-tests/pom.xml new file mode 100644 index 000000000..667d54797 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-task-tests/pom.xml @@ -0,0 +1,81 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-task-tests + jar + Spring Cloud Sleuth Brave Task Instrumentation Tests + Spring Cloud Sleuth Brave Task Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + + + + org.springframework.cloud + spring-cloud-sleuth-tests-common + ${project.version} + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.cloud + spring-cloud-starter-task + + + org.springframework.boot + spring-boot-starter-test + + + io.zipkin.brave + brave-tests + + + org.awaitility + awaitility + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-task-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/task/SpringCloudTaskIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-task-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/task/SpringCloudTaskIntegrationTests.java new file mode 100644 index 000000000..55d08c60a --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-task-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/task/SpringCloudTaskIntegrationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.task; + +import brave.sampler.Sampler; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +@SpringBootTest +@ContextConfiguration(classes = SpringCloudTaskIntegrationTests.Config.class) +public class SpringCloudTaskIntegrationTests + extends org.springframework.cloud.sleuth.instrument.task.SpringCloudTaskIntegrationTests { + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + TestSpanHandler testSpanHandlerSupplier(brave.test.TestSpanHandler testSpanHandler) { + return new BraveTestSpanHandler(testSpanHandler); + } + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + brave.test.TestSpanHandler braveTestSpanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-task-tests/src/test/resources/application.yml b/tests/brave/spring-cloud-sleuth-instrumentation-task-tests/src/test/resources/application.yml new file mode 100644 index 000000000..2b87cbf49 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-task-tests/src/test/resources/application.yml @@ -0,0 +1 @@ +logging.level.org.springframework.cloud: DEBUG diff --git a/tests/common/pom.xml b/tests/common/pom.xml index 628288865..01c5cdc48 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -84,6 +84,11 @@ spring-cloud-starter-gateway true + + org.springframework.cloud + spring-cloud-starter-task + true + org.springframework.cloud spring-cloud-starter-circuitbreaker-resilience4j diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/task/SpringCloudTaskIntegrationTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/task/SpringCloudTaskIntegrationTests.java new file mode 100644 index 000000000..e1c5f83f3 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/task/SpringCloudTaskIntegrationTests.java @@ -0,0 +1,107 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.task; + +import java.util.Iterator; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.cloud.task.configuration.EnableTask; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.BDDAssertions.then; + +@ContextConfiguration(classes = SpringCloudTaskIntegrationTests.TestConfig.class) +@TestPropertySource(properties = "spring.application.name=MyApplication") +public abstract class SpringCloudTaskIntegrationTests { + + @Autowired + TestSpanHandler spans; + + @Test + public void should_pass_tracing_information_when_using_spring_cloud_task() { + Set traceIds = this.spans.reportedSpans().stream().map(FinishedSpan::getTraceId) + .collect(Collectors.toSet()); + then(traceIds).as("There's one traceid").hasSize(1); + Set spanIds = this.spans.reportedSpans().stream().map(FinishedSpan::getSpanId) + .collect(Collectors.toSet()); + + then(spanIds).as("There are 3 spans").hasSize(3); + Iterator spanIterator = this.spans.reportedSpans().iterator(); + + FinishedSpan first = spanIterator.next(); + FinishedSpan second = spanIterator.next(); + FinishedSpan third = spanIterator.next(); + then(first.getName()).isEqualTo("myApplicationRunner"); + then(second.getName()).isEqualTo("myCommandLineRunner"); + then(third.getName()).isEqualTo("MyApplication"); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @EnableTask + public static class TestConfig { + + @Bean + MyCommandLineRunner myCommandLineRunner() { + return new MyCommandLineRunner(); + } + + @Bean + MyApplicationRunner myApplicationRunner() { + return new MyApplicationRunner(); + } + + } + + static class MyCommandLineRunner implements CommandLineRunner { + + private static final Log log = LogFactory.getLog(MyCommandLineRunner.class); + + @Override + public void run(String... args) throws Exception { + log.info("Ran MyCommandLineRunner"); + } + + } + + static class MyApplicationRunner implements ApplicationRunner { + + private static final Log log = LogFactory.getLog(MyApplicationRunner.class); + + @Override + public void run(ApplicationArguments args) throws Exception { + log.info("Ran MyApplicationRunner"); + } + + } + +} From 92d0a30a959affc8e8ef3ad9cd880f4fafdd60b1 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 21 Apr 2021 12:20:25 +0000 Subject: [PATCH 06/59] Added Spring Cloud Config server support; fixes 1915 (#1917) --- docs/src/main/asciidoc/integrations.adoc | 10 +- pom.xml | 8 ++ spring-cloud-sleuth-autoconfigure/pom.xml | 10 ++ ...aceSpringCloudConfigAutoConfiguration.java | 51 ++++++++++ ...itional-spring-configuration-metadata.json | 6 ++ .../main/resources/META-INF/spring.factories | 1 + ...ringCloudConfigAutoConfigurationTests.java | 39 ++++++++ spring-cloud-sleuth-instrumentation/pom.xml | 10 ++ .../TraceEnvironmentRepositoryAspect.java | 54 ++++++++++ tests/brave/pom.xml | 1 + .../pom.xml | 84 ++++++++++++++++ .../config/ConfigServerIntegrationTests.java | 53 ++++++++++ .../src/test/resources/application.yml | 3 + tests/common/pom.xml | 5 + .../config/ConfigServerIntegrationTests.java | 98 +++++++++++++++++++ 15 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/config/TraceSpringCloudConfigAutoConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/config/TraceSpringCloudConfigAutoConfigurationTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/config/TraceEnvironmentRepositoryAspect.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-config-server-tests/pom.xml create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-config-server-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/config/ConfigServerIntegrationTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-config-server-tests/src/test/resources/application.yml create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/config/ConfigServerIntegrationTests.java diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 6da99eafc..a05b772c4 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -566,4 +566,12 @@ IMPORTANT: The suggested approach to reactive programming and Sleuth is to use t This feature is available for all tracer implementations. If you have Spring Cloud CircuitBreaker on the classpath, we will wrap the passed command `Supplier` and the fallback `Function` in its trace representations. We will also instrument the reactive implementation of the CircuitBreaker. -In order to disable this instrumentation set `spring.sleuth.circuitbreaker.enabled` to `false`. \ No newline at end of file +In order to disable this instrumentation set `spring.sleuth.circuitbreaker.enabled` to `false`. + +[[sleuth-config-server-integration]] +== Spring Cloud Config Server + +This feature is available for all tracer implementations. + +If you have Spring Cloud Config Server running on the classpath, we will wrap the `EnvironmentRepository` in a span. +In order to disable this instrumentation set `spring.sleuth.config.server.enabled` to `false`. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 286d8b4e4..08c6c9d63 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,7 @@ 3.0.3-SNAPSHOT 3.0.3-SNAPSHOT 3.0.3-SNAPSHOT + 3.0.3-SNAPSHOT 2.0.2-SNAPSHOT 3.1.3-SNAPSHOT 3.1.3-SNAPSHOT @@ -229,6 +230,13 @@ pom import + + org.springframework.cloud + spring-cloud-config-dependencies + ${spring-cloud-config.version} + pom + import + org.springframework.cloud spring-cloud-task-dependencies diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index 769757ac6..ac237bbf7 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -73,6 +73,16 @@ spring-integration-core true + + org.springframework.cloud + spring-cloud-config-server + true + + + org.springframework.cloud + spring-cloud-starter-config + true + org.springframework.cloud spring-cloud-function-context diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/config/TraceSpringCloudConfigAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/config/TraceSpringCloudConfigAutoConfiguration.java new file mode 100644 index 000000000..1276618a1 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/config/TraceSpringCloudConfigAutoConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.config; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.config.server.config.ConfigServerConfiguration; +import org.springframework.cloud.config.server.config.ConfigServerProperties; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.config.TraceEnvironmentRepositoryAspect; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Spring Cloud Config Server and + * Client. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean({ Tracer.class, ConfigServerProperties.class }) +@ConditionalOnClass(ConfigServerConfiguration.class) +@ConditionalOnProperty(value = "spring.sleuth.config.server.enabled", matchIfMissing = true) +@AutoConfigureAfter(BraveAutoConfiguration.class) +public class TraceSpringCloudConfigAutoConfiguration { + + @Bean + TraceEnvironmentRepositoryAspect traceEnvironmentRepositoryAspect(Tracer tracer) { + return new TraceEnvironmentRepositoryAspect(tracer); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index e46fecde3..b2ebb1d5a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -136,6 +136,12 @@ "type": "java.lang.Boolean", "description": "Enable Spring Cloud Task instrumentation.", "defaultValue": true + }, + { + "name": "spring.sleuth.config.server.enabled", + "type": "java.lang.Boolean", + "description": "Enable Spring Cloud Config Server instrumentation.", + "defaultValue": true } ] } diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index 5e902cd8a..d2df0f518 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -3,6 +3,7 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncCustomAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncDefaultAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.config.TraceSpringCloudConfigAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.circuitbreaker.TraceCircuitBreakerAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.rxjava.TraceRxJavaAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.quartz.TraceQuartzAutoConfiguration,\ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/config/TraceSpringCloudConfigAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/config/TraceSpringCloudConfigAutoConfigurationTests.java new file mode 100644 index 000000000..8d75ab817 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/config/TraceSpringCloudConfigAutoConfigurationTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.config; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.config.server.config.ConfigServerProperties; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.config.TraceEnvironmentRepositoryAspect; + +class TraceSpringCloudConfigAutoConfigurationTests { + + @Test + void should_register_the_aspect() { + new ApplicationContextRunner().withPropertyValues("spring.sleuth.noop.enabled=true") + .withBean(ConfigServerProperties.class) + .withConfiguration(AutoConfigurations.of(TraceNoOpAutoConfiguration.class, + TraceSpringCloudConfigAutoConfiguration.class)) + .run(context -> BDDAssertions.then(context).hasSingleBean(TraceEnvironmentRepositoryAspect.class)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index 6aff38d57..356f74fe0 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -72,6 +72,16 @@ spring-integration-core true + + org.springframework.cloud + spring-cloud-config-server + true + + + org.springframework.cloud + spring-cloud-starter-config + true + org.springframework.cloud spring-cloud-function-context diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/config/TraceEnvironmentRepositoryAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/config/TraceEnvironmentRepositoryAspect.java new file mode 100644 index 000000000..93b420f00 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/config/TraceEnvironmentRepositoryAspect.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.config; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; + +/** + * Aspect wrapping resolution of properties. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Aspect +public class TraceEnvironmentRepositoryAspect { + + private final Tracer tracer; + + public TraceEnvironmentRepositoryAspect(Tracer tracer) { + this.tracer = tracer; + } + + @Around("execution (* org.springframework.cloud.config.server.environment.EnvironmentRepository.*(..))") + public Object traceFindEnvironment(final ProceedingJoinPoint pjp) throws Throwable { + Span findOneSpan = this.tracer.nextSpan().name("find"); + findOneSpan.tag("config.environment.class", pjp.getTarget().getClass().getName()); + findOneSpan.tag("config.environment.method", pjp.getSignature().getName()); + try (Tracer.SpanInScope ws = this.tracer.withSpan(findOneSpan.start())) { + return pjp.proceed(); + } + finally { + findOneSpan.end(); + } + } + +} diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index 8796d03bf..2e21ae75f 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -38,6 +38,7 @@ spring-cloud-sleuth-instrumentation-annotation-tests spring-cloud-sleuth-instrumentation-async-tests spring-cloud-sleuth-instrumentation-baggage-tests + spring-cloud-sleuth-instrumentation-config-server-tests spring-cloud-sleuth-instrumentation-circuitbreaker-tests spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests spring-cloud-sleuth-instrumentation-feign-tests diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-config-server-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-config-server-tests/pom.xml new file mode 100644 index 000000000..6d6f4eb10 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-config-server-tests/pom.xml @@ -0,0 +1,84 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-config-server-tests + jar + Spring Cloud Sleuth Brave Config Server Instrumentation Tests + Spring Cloud Sleuth Brave Config Server Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + + + + org.springframework.cloud + spring-cloud-sleuth-tests-common + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.cloud + spring-cloud-config-server + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-test + + + io.zipkin.brave + brave-tests + + + org.awaitility + awaitility + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-config-server-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/config/ConfigServerIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-config-server-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/config/ConfigServerIntegrationTests.java new file mode 100644 index 000000000..fe1251a31 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-config-server-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/config/ConfigServerIntegrationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.config; + +import brave.sampler.Sampler; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ContextConfiguration(classes = ConfigServerIntegrationTests.Config.class) +public class ConfigServerIntegrationTests + extends org.springframework.cloud.sleuth.instrument.config.ConfigServerIntegrationTests { + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + TestSpanHandler testSpanHandlerSupplier(brave.test.TestSpanHandler testSpanHandler) { + return new BraveTestSpanHandler(testSpanHandler); + } + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + brave.test.TestSpanHandler braveTestSpanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-config-server-tests/src/test/resources/application.yml b/tests/brave/spring-cloud-sleuth-instrumentation-config-server-tests/src/test/resources/application.yml new file mode 100644 index 000000000..f0238e134 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-config-server-tests/src/test/resources/application.yml @@ -0,0 +1,3 @@ +logging.level.org.springframework.cloud: DEBUG + +spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration diff --git a/tests/common/pom.xml b/tests/common/pom.xml index 01c5cdc48..f405ccd78 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -119,6 +119,11 @@ spring-cloud-starter-loadbalancer true + + org.springframework.cloud + spring-cloud-config-server + true + org.apache.httpcomponents httpclient diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/config/ConfigServerIntegrationTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/config/ConfigServerIntegrationTests.java new file mode 100644 index 000000000..e27a43140 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/config/ConfigServerIntegrationTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.config; + +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.cloud.config.server.EnableConfigServer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.awaitility.Awaitility.await; + +@ContextConfiguration(classes = ConfigServerIntegrationTests.TestConfig.class) +@TestPropertySource(properties = { "server.port=0", + "spring.cloud.config.server.git.uri=https://github.com/spring-cloud-samples/config-repo" }) +public abstract class ConfigServerIntegrationTests { + + @Autowired + TestSpanHandler spans; + + @Autowired + WebClientService webClientService; + + @LocalServerPort + int port; + + @BeforeEach + public void setup() { + this.spans.clear(); + } + + @Test + public void should_instrument_config_server() { + this.webClientService.call(port); + + await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { + then(this.spans.reportedSpans()).as("1 for mvc, 1 for composite env repo and 1 for git env repo") + .hasSize(3); + then(this.spans.reportedSpans().stream().map(FinishedSpan::getTraceId).collect(Collectors.toSet())) + .as("There must be 1 trace id").hasSize(1); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @EnableConfigServer + public static class TestConfig { + + @Bean + WebClientService webClientService() { + return new WebClientService(); + } + + } + + public static class WebClientService { + + private static final Logger log = LoggerFactory.getLogger(WebClientService.class); + + void call(int port) { + log.info("Sending request"); + String result = new RestTemplate().getForObject("http://localhost:" + port + "/master/application.yml", + String.class); + log.info("Got [\n" + result + "\n]"); + } + + } + +} From c4ab9dcd498581605ff21e6b23333ada339f48d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flaviu=20Mure=C8=99an?= Date: Thu, 22 Apr 2021 16:27:34 +0200 Subject: [PATCH 07/59] Kafka instrumentation (#1920) fixes gh-1906 --- pom.xml | 8 + spring-cloud-sleuth-instrumentation/pom.xml | 5 + .../kafka/KafkaTracingCallback.java | 70 ++++ .../instrument/kafka/KafkaTracingUtils.java | 46 +++ .../kafka/TracingKafkaConsumer.java | 314 ++++++++++++++++++ .../kafka/TracingKafkaProducer.java | 147 ++++++++ .../kafka/TracingKafkaProducerFactory.java | 54 +++ .../kafka/TracingKafkaPropagatorGetter.java | 44 +++ .../kafka/TracingKafkaPropagatorSetter.java | 40 +++ .../kafka/TracingKafkaReceiver.java | 112 +++++++ .../kafka/KafkaTracingCallbackTest.java | 65 ++++ .../kafka/TracingKafkaConsumerTest.java | 67 ++++ .../kafka/TracingKafkaProducerTest.java | 79 +++++ .../kafka/TracingKafkaReceiverTest.java | 61 ++++ tests/brave/pom.xml | 1 + .../pom.xml | 97 ++++++ .../instrument/kafka/KafkaProducerTest.java | 34 ++ tests/common/pom.xml | 20 ++ .../instrument/kafka/KafkaProducerTest.java | 98 ++++++ 19 files changed, 1362 insertions(+) create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingCallback.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingUtils.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumer.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducer.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerFactory.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorGetter.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorSetter.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiver.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingCallbackTest.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumerTest.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerTest.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiverTest.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/pom.xml create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaProducerTest.java diff --git a/pom.xml b/pom.xml index 08c6c9d63..7b1064f32 100644 --- a/pom.xml +++ b/pom.xml @@ -92,6 +92,7 @@ 4.0.3 0.21.3 0.14.1 + 1.15.3 @@ -293,6 +294,13 @@ archunit-junit5 ${archunit-junit5.version} + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index 356f74fe0..4793fa853 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -52,6 +52,11 @@ reactor-core true + + io.projectreactor.kafka + reactor-kafka + true + org.reactivestreams reactive-streams diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingCallback.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingCallback.java new file mode 100644 index 000000000..4e8dfd44a --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingCallback.java @@ -0,0 +1,70 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.kafka.clients.producer.Callback; +import org.apache.kafka.clients.producer.RecordMetadata; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; + +/** + * This decorates a Kafka {@link Callback} and completes the {@link Span.Kind#PRODUCER} + * span created for the record when {@code onCompletion()} is invoked (i.e. the broker has + * acknowledged or an {@link Exception}) was thrown. + * + * @author Anders Clausen + * @author Flaviu Muresan + * @since 3.0.3 + */ +public class KafkaTracingCallback implements Callback { + + private static final Log log = LogFactory.getLog(KafkaTracingCallback.class); + + private final Callback callback; + + private final Tracer tracer; + + private final Span span; + + public KafkaTracingCallback(Callback callback, Tracer tracer, Span span) { + this.callback = callback; + this.tracer = tracer; + this.span = span; + } + + @Override + public void onCompletion(RecordMetadata recordMetadata, Exception e) { + try (Tracer.SpanInScope spanInScope = tracer.withSpan(this.span)) { + if (this.callback != null) { + this.callback.onCompletion(recordMetadata, e); + } + } + finally { + if (e != null) { + this.span.error(e); + } + this.span.end(); + if (log.isDebugEnabled()) { + log.debug("Finished producer span " + span); + } + } + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingUtils.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingUtils.java new file mode 100644 index 000000000..3c3b23461 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingUtils.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.kafka.clients.consumer.ConsumerRecord; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.propagation.Propagator; + +final class KafkaTracingUtils { + + private static final Log log = LogFactory.getLog(KafkaTracingUtils.class); + + private KafkaTracingUtils() { + } + + static void buildAndFinishSpan(ConsumerRecord consumerRecord, Propagator propagator, + Propagator.Getter> extractor) { + Span.Builder spanBuilder = propagator.extract(consumerRecord, extractor).kind(Span.Kind.CONSUMER) + .name("kafka.consume").tag("kafka.topic", consumerRecord.topic()) + .tag("kafka.offset", Long.toString(consumerRecord.offset())) + .tag("kafka.partition", Integer.toString(consumerRecord.partition())); + Span span = spanBuilder.start(); + if (log.isDebugEnabled()) { + log.debug("Extracted span from event headers " + span); + } + span.end(); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumer.java new file mode 100644 index 000000000..9698f15cd --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumer.java @@ -0,0 +1,314 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerGroupMetadata; +import org.apache.kafka.clients.consumer.ConsumerRebalanceListener; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.clients.consumer.OffsetAndTimestamp; +import org.apache.kafka.clients.consumer.OffsetCommitCallback; +import org.apache.kafka.common.Metric; +import org.apache.kafka.common.MetricName; +import org.apache.kafka.common.PartitionInfo; +import org.apache.kafka.common.TopicPartition; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.propagation.Propagator; + +/** + * This decorates a Kafka {@link Consumer}. It creates and completes a + * {@link Span.Kind#CONSUMER} span for each record received. This span will be a child + * span of the one extracted from the record headers. + * + * @author Anders Clausen + * @author Flaviu Muresan + * @since 3.0.3 + */ +public class TracingKafkaConsumer implements Consumer { + + private final Consumer delegate; + + private final Propagator propagator; + + private final Propagator.Getter> extractor; + + public TracingKafkaConsumer(Consumer consumer, Propagator propagator, + Propagator.Getter> getter) { + this.delegate = consumer; + this.propagator = propagator; + this.extractor = getter; + } + + @Override + public Set assignment() { + return this.delegate.assignment(); + } + + @Override + public Set subscription() { + return this.delegate.subscription(); + } + + @Override + public void subscribe(Collection collection) { + this.delegate.subscribe(collection); + } + + @Override + public void subscribe(Collection collection, ConsumerRebalanceListener consumerRebalanceListener) { + this.delegate.subscribe(collection, consumerRebalanceListener); + } + + @Override + public void assign(Collection collection) { + this.delegate.assign(collection); + } + + @Override + public void subscribe(Pattern pattern, ConsumerRebalanceListener consumerRebalanceListener) { + this.delegate.subscribe(pattern, consumerRebalanceListener); + } + + @Override + public void subscribe(Pattern pattern) { + this.delegate.subscribe(pattern); + } + + @Override + public void unsubscribe() { + this.delegate.unsubscribe(); + } + + @Deprecated + @Override + public ConsumerRecords poll(long l) { + ConsumerRecords consumerRecords = this.delegate.poll(l); + for (ConsumerRecord consumerRecord : consumerRecords) { + KafkaTracingUtils.buildAndFinishSpan(consumerRecord, this.propagator, this.extractor); + } + return consumerRecords; + } + + @Override + public ConsumerRecords poll(Duration duration) { + ConsumerRecords consumerRecords = this.delegate.poll(duration); + for (ConsumerRecord consumerRecord : consumerRecords) { + KafkaTracingUtils.buildAndFinishSpan(consumerRecord, this.propagator, this.extractor); + } + return consumerRecords; + } + + @Override + public void commitSync() { + this.delegate.commitSync(); + } + + @Override + public void commitSync(Duration duration) { + this.delegate.commitSync(duration); + } + + @Override + public void commitSync(Map map) { + this.delegate.commitSync(map); + } + + @Override + public void commitSync(Map map, Duration duration) { + this.delegate.commitSync(map, duration); + } + + @Override + public void commitAsync() { + this.delegate.commitAsync(); + } + + @Override + public void commitAsync(OffsetCommitCallback offsetCommitCallback) { + this.delegate.commitAsync(offsetCommitCallback); + } + + @Override + public void commitAsync(Map map, OffsetCommitCallback offsetCommitCallback) { + this.delegate.commitAsync(map, offsetCommitCallback); + } + + @Override + public void seek(TopicPartition topicPartition, long l) { + this.delegate.seek(topicPartition, l); + } + + @Override + public void seek(TopicPartition topicPartition, OffsetAndMetadata offsetAndMetadata) { + this.delegate.seek(topicPartition, offsetAndMetadata); + } + + @Override + public void seekToBeginning(Collection collection) { + this.delegate.seekToBeginning(collection); + } + + @Override + public void seekToEnd(Collection collection) { + this.delegate.seekToEnd(collection); + } + + @Override + public long position(TopicPartition topicPartition) { + return this.delegate.position(topicPartition); + } + + @Override + public long position(TopicPartition topicPartition, Duration duration) { + return this.delegate.position(topicPartition, duration); + } + + @Override + @Deprecated + public OffsetAndMetadata committed(TopicPartition topicPartition) { + return this.delegate.committed(topicPartition); + } + + @Override + @Deprecated + public OffsetAndMetadata committed(TopicPartition topicPartition, Duration duration) { + return this.delegate.committed(topicPartition, duration); + } + + @Override + public Map committed(Set set) { + return this.delegate.committed(set); + } + + @Override + public Map committed(Set set, Duration duration) { + return this.delegate.committed(set, duration); + } + + @Override + public Map metrics() { + return this.delegate.metrics(); + } + + @Override + public List partitionsFor(String s) { + return this.delegate.partitionsFor(s); + } + + @Override + public List partitionsFor(String s, Duration duration) { + return this.delegate.partitionsFor(s, duration); + } + + @Override + public Map> listTopics() { + return this.delegate.listTopics(); + } + + @Override + public Map> listTopics(Duration duration) { + return this.delegate.listTopics(duration); + } + + @Override + public Set paused() { + return this.delegate.paused(); + } + + @Override + public void pause(Collection collection) { + this.delegate.pause(collection); + } + + @Override + public void resume(Collection collection) { + this.delegate.resume(collection); + } + + @Override + public Map offsetsForTimes(Map map) { + return this.delegate.offsetsForTimes(map); + } + + @Override + public Map offsetsForTimes(Map map, Duration duration) { + return this.delegate.offsetsForTimes(map, duration); + } + + @Override + public Map beginningOffsets(Collection collection) { + return this.delegate.beginningOffsets(collection); + } + + @Override + public Map beginningOffsets(Collection collection, Duration duration) { + return this.delegate.beginningOffsets(collection, duration); + } + + @Override + public Map endOffsets(Collection collection) { + return this.delegate.endOffsets(collection); + } + + @Override + public Map endOffsets(Collection collection, Duration duration) { + return this.delegate.endOffsets(collection, duration); + } + + @Override + public ConsumerGroupMetadata groupMetadata() { + return this.delegate.groupMetadata(); + } + + @Override + public void enforceRebalance() { + this.delegate.enforceRebalance(); + } + + @Override + public void close() { + this.delegate.close(); + } + + @Override + @Deprecated + public void close(long l, TimeUnit timeUnit) { + this.delegate.close(l, timeUnit); + } + + @Override + public void close(Duration duration) { + this.delegate.close(duration); + } + + @Override + public void wakeup() { + this.delegate.wakeup(); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducer.java new file mode 100644 index 000000000..51d778076 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducer.java @@ -0,0 +1,147 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.kafka.clients.consumer.ConsumerGroupMetadata; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.clients.producer.Callback; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.clients.producer.RecordMetadata; +import org.apache.kafka.common.Metric; +import org.apache.kafka.common.MetricName; +import org.apache.kafka.common.PartitionInfo; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.errors.ProducerFencedException; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.propagation.Propagator; + +/** + * This decorates a Kafka {@link Producer} and creates a {@link Span.Kind#PRODUCER} span + * for each record sent. This span is also injected onto each record (via headers) so it + * becomes the parent when a consumer later receives the record. + * + * @author Anders Clausen + * @author Flaviu Muresan + * @since 3.0.3 + */ +public class TracingKafkaProducer implements Producer { + + private static final Log log = LogFactory.getLog(TracingKafkaProducer.class); + + private final Producer delegate; + + private final Tracer tracer; + + private final Propagator propagator; + + private final Propagator.Setter> injector; + + public TracingKafkaProducer(Producer producer, Tracer tracer, Propagator propagator, + Propagator.Setter> setter) { + this.delegate = producer; + this.tracer = tracer; + this.propagator = propagator; + this.injector = setter; + } + + @Override + public void initTransactions() { + this.delegate.initTransactions(); + } + + @Override + public void beginTransaction() throws ProducerFencedException { + this.delegate.beginTransaction(); + } + + @Override + public void sendOffsetsToTransaction(Map map, String s) + throws ProducerFencedException { + this.delegate.sendOffsetsToTransaction(map, s); + } + + @Override + public void sendOffsetsToTransaction(Map map, + ConsumerGroupMetadata consumerGroupMetadata) throws ProducerFencedException { + this.delegate.sendOffsetsToTransaction(map, consumerGroupMetadata); + } + + @Override + public void commitTransaction() throws ProducerFencedException { + this.delegate.commitTransaction(); + } + + @Override + public void abortTransaction() throws ProducerFencedException { + this.delegate.abortTransaction(); + } + + @Override + public Future send(ProducerRecord producerRecord) { + return send(producerRecord, null); + } + + @Override + public Future send(ProducerRecord producerRecord, Callback callback) { + Span.Builder spanBuilder = tracer.spanBuilder().kind(Span.Kind.PRODUCER).name("kafka.produce") + .tag("kafka.topic", producerRecord.topic()); + Span span = spanBuilder.start(); + this.propagator.inject(span.context(), producerRecord, this.injector); + try (Tracer.SpanInScope spanInScope = tracer.withSpan(span)) { + if (log.isDebugEnabled()) { + log.debug("Created producer span " + span); + } + return this.delegate.send(producerRecord, new KafkaTracingCallback(callback, tracer, span)); + } + } + + @Override + public void flush() { + this.delegate.flush(); + } + + @Override + public List partitionsFor(String s) { + return this.delegate.partitionsFor(s); + } + + @Override + public Map metrics() { + return this.delegate.metrics(); + } + + @Override + public void close() { + this.delegate.close(); + } + + @Override + public void close(Duration duration) { + this.delegate.close(duration); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerFactory.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerFactory.java new file mode 100644 index 000000000..184df69b7 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerFactory.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import org.apache.kafka.clients.producer.Producer; +import reactor.kafka.sender.KafkaSender; +import reactor.kafka.sender.SenderOptions; +import reactor.kafka.sender.internals.ProducerFactory; + +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.propagation.Propagator; + +/** + * This decorates a Reactor Kafka {@link ProducerFactory} to create decorated producers of + * type {@link TracingKafkaProducer}. This can be used by the {@link KafkaSender} factory + * methods to create instrumented senders. + * + * @author Anders Clausen + * @author Flaviu Muresan + * @since 3.0.3 + */ +public class TracingKafkaProducerFactory extends ProducerFactory { + + private final Tracer tracer; + + private final Propagator propagator; + + public TracingKafkaProducerFactory(Tracer tracer, Propagator propagator) { + super(); + this.tracer = tracer; + this.propagator = propagator; + } + + @Override + public Producer createProducer(SenderOptions senderOptions) { + return new TracingKafkaProducer<>(super.createProducer(senderOptions), tracer, propagator, + new TracingKafkaPropagatorSetter()); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorGetter.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorGetter.java new file mode 100644 index 000000000..e09988e5c --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorGetter.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import java.util.Iterator; +import java.util.Optional; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.common.header.Header; + +import org.springframework.cloud.sleuth.propagation.Propagator; + +/** + * Getter extracting the values from the {@link ConsumerRecord} headers for Kafka based + * communication. + * + * @author Anders Clausen + * @author Flaviu Muresan + * @since 3.0.3 + */ +public class TracingKafkaPropagatorGetter implements Propagator.Getter> { + + @Override + public String get(ConsumerRecord carrier, String key) { + return Optional.ofNullable(carrier).map(ConsumerRecord::headers).map(headers -> headers.headers(key)) + .map(Iterable::iterator).filter(Iterator::hasNext).map(Iterator::next).map(Header::value) + .map(String::new).orElse(null); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorSetter.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorSetter.java new file mode 100644 index 000000000..afdb9b955 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorSetter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import org.apache.kafka.clients.producer.ProducerRecord; + +import org.springframework.cloud.sleuth.propagation.Propagator; + +/** + * Setter injecting the values onto the {@link ProducerRecord} headers for Kafka based + * communication. + * + * @author Anders Clausen + * @author Flaviu Muresan + * @since 3.0.3 + */ +public class TracingKafkaPropagatorSetter implements Propagator.Setter> { + + @Override + public void set(ProducerRecord carrier, String key, String value) { + if (carrier != null) { + carrier.headers().add(key, value.getBytes()); + } + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiver.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiver.java new file mode 100644 index 000000000..f855b5595 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiver.java @@ -0,0 +1,112 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import java.util.function.Function; + +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.kafka.receiver.KafkaReceiver; +import reactor.kafka.receiver.ReceiverRecord; +import reactor.kafka.sender.TransactionManager; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.propagation.Propagator; + +/** + * This decorates a reactive {@link KafkaReceiver} and creates and completes a + * {@link Span.Kind#CONSUMER} span for each record received. This span will be a child + * span of the one extracted from the record headers. + * + * @author Anders Clausen + * @author Flaviu Muresan + * @since 3.0.3 + */ +public class TracingKafkaReceiver implements KafkaReceiver { + + private final KafkaReceiver delegate; + + private final Propagator propagator; + + private final Propagator.Getter> extractor; + + public TracingKafkaReceiver(KafkaReceiver receiver, Propagator propagator, + Propagator.Getter> getter) { + this.delegate = receiver; + this.propagator = propagator; + this.extractor = getter; + } + + @Override + public Flux> receive(Integer integer) { + return buildAndFinishSpanOnNextReceiverRecord(this.delegate.receive(integer)); + } + + @Override + public Flux> receive() { + return buildAndFinishSpanOnNextReceiverRecord(this.delegate.receive()); + } + + @Override + public Flux>> receiveAutoAck(Integer integer) { + return this.delegate.receiveAutoAck(integer).map(this::buildAndFinishSpanOnNextConsumerRecord); + } + + @Override + public Flux>> receiveAutoAck() { + return this.delegate.receiveAutoAck().map(this::buildAndFinishSpanOnNextConsumerRecord); + } + + @Override + public Flux> receiveAtmostOnce(Integer integer) { + return this.buildAndFinishSpanOnNextConsumerRecord(this.delegate.receiveAtmostOnce(integer)); + } + + @Override + public Flux> receiveAtmostOnce() { + return this.buildAndFinishSpanOnNextConsumerRecord(this.delegate.receiveAtmostOnce()); + } + + @Override + public Flux>> receiveExactlyOnce(TransactionManager transactionManager) { + return this.delegate.receiveExactlyOnce(transactionManager).map(this::buildAndFinishSpanOnNextConsumerRecord); + } + + @Override + public Flux>> receiveExactlyOnce(TransactionManager transactionManager, Integer integer) { + return this.delegate.receiveExactlyOnce(transactionManager, integer) + .map(this::buildAndFinishSpanOnNextConsumerRecord); + } + + @Override + public Mono doOnConsumer(Function, ? extends T> function) { + return this.delegate.doOnConsumer(function); + } + + private Flux> buildAndFinishSpanOnNextConsumerRecord(Flux> flux) { + return flux.doOnNext(consumerRecord -> KafkaTracingUtils.buildAndFinishSpan(consumerRecord, this.propagator, + this.extractor)); + } + + private Flux> buildAndFinishSpanOnNextReceiverRecord(Flux> flux) { + return flux.doOnNext(consumerRecord -> KafkaTracingUtils.buildAndFinishSpan(consumerRecord, this.propagator, + this.extractor)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingCallbackTest.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingCallbackTest.java new file mode 100644 index 000000000..44fc895c9 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingCallbackTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import org.apache.kafka.clients.producer.Callback; +import org.apache.kafka.clients.producer.RecordMetadata; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; + +@ExtendWith(MockitoExtension.class) +public class KafkaTracingCallbackTest { + + @Mock + Tracer tracer; + + @Mock + Span span; + + @Mock + Callback callback; + + @Test + void should_call_on_completion_on_user_callback_success() { + KafkaTracingCallback tracingCallback = new KafkaTracingCallback(callback, tracer, span); + RecordMetadata recordMetadata = new RecordMetadata(null, 0, 0, 0, 0L, 0, 0); + + tracingCallback.onCompletion(recordMetadata, null); + + Mockito.verify(callback).onCompletion(eq(recordMetadata), isNull()); + } + + @Test + void should_call_on_completion_on_user_callback_error() { + KafkaTracingCallback tracingCallback = new KafkaTracingCallback(callback, tracer, span); + + tracingCallback.onCompletion(null, new RuntimeException()); + + Mockito.verify(callback).onCompletion(isNull(), any(RuntimeException.class)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumerTest.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumerTest.java new file mode 100644 index 000000000..c04c22d72 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumerTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.TopicPartition; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.BDDMockito; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.cloud.sleuth.propagation.Propagator; + +import static org.mockito.ArgumentMatchers.eq; + +@ExtendWith(MockitoExtension.class) +public class TracingKafkaConsumerTest { + + @Mock + KafkaConsumer kafkaConsumer; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + Propagator propagator; + + @Test + void should_delegate_poll_calls() { + Duration pollTimeout = Duration.of(5, ChronoUnit.SECONDS); + ConsumerRecord record = new ConsumerRecord<>("topic", 0, 1, "test-key", "test-value"); + Map>> map = new HashMap<>(); + map.put(new TopicPartition("topic", 0), Collections.singletonList(record)); + ConsumerRecords records = new ConsumerRecords<>(map); + BDDMockito.given(kafkaConsumer.poll(pollTimeout)).willReturn(records); + TracingKafkaConsumer tracingKafkaConsumer = new TracingKafkaConsumer<>(kafkaConsumer, + propagator, new TracingKafkaPropagatorGetter()); + + tracingKafkaConsumer.poll(pollTimeout); + + Mockito.verify(kafkaConsumer).poll(eq(pollTimeout)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerTest.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerTest.java new file mode 100644 index 000000000..dcb3bab8b --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import org.apache.kafka.clients.producer.Callback; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; + +@ExtendWith(MockitoExtension.class) +public class TracingKafkaProducerTest { + + @Mock + KafkaProducer kafkaProducer; + + @Mock + Propagator propagator; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + Tracer tracer; + + @Test + void should_delegate_send_calls() { + ProducerRecord testRecord = new ProducerRecord<>("test", "test"); + Callback callback = (record, ex) -> { + }; + TracingKafkaProducer tracingKafkaProducer = new TracingKafkaProducer<>(kafkaProducer, tracer, + propagator, new TracingKafkaPropagatorSetter()); + + tracingKafkaProducer.send(testRecord, callback); + + Mockito.verify(kafkaProducer).send(eq(testRecord), any()); + } + + @Test + void should_wrap_user_callback_on_send() { + ProducerRecord testRecord = new ProducerRecord<>("test", "test"); + Callback callback = (record, ex) -> { + }; + TracingKafkaProducer tracingKafkaProducer = new TracingKafkaProducer<>(kafkaProducer, tracer, + propagator, new TracingKafkaPropagatorSetter()); + + tracingKafkaProducer.send(testRecord, callback); + + ArgumentCaptor callbackArgument = ArgumentCaptor.forClass(KafkaTracingCallback.class); + Mockito.verify(kafkaProducer).send(any(), callbackArgument.capture()); + BDDAssertions.then(callbackArgument.getValue()).isNotNull(); + BDDAssertions.then(ReflectionTestUtils.getField(callbackArgument.getValue(), "callback")).isEqualTo(callback); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiverTest.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiverTest.java new file mode 100644 index 000000000..6707aeee6 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiverTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import java.util.function.Predicate; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.BDDMockito; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Flux; +import reactor.kafka.receiver.KafkaReceiver; +import reactor.kafka.receiver.ReceiverOffset; +import reactor.kafka.receiver.ReceiverRecord; +import reactor.test.StepVerifier; + +import org.springframework.cloud.sleuth.propagation.Propagator; + +@ExtendWith(MockitoExtension.class) +public class TracingKafkaReceiverTest { + + @Mock + KafkaReceiver kafkaReceiver; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + Propagator propagator; + + @Test + void should_delegate_receive_calls() { + ReceiverOffset receiverOffset = BDDMockito.mock(ReceiverOffset.class); + ConsumerRecord record = new ConsumerRecord<>("topic", 0, 1, "test-key", "test-value"); + ReceiverRecord receiverRecord = new ReceiverRecord<>(record, receiverOffset); + BDDMockito.given(kafkaReceiver.receive()).willReturn(Flux.just(receiverRecord)); + TracingKafkaReceiver tracingKafkaReceiver = new TracingKafkaReceiver<>(kafkaReceiver, + propagator, new TracingKafkaPropagatorGetter()); + + StepVerifier.create(tracingKafkaReceiver.receive()).expectNextMatches(Predicate.isEqual(receiverRecord)) + .verifyComplete(); + + Mockito.verify(kafkaReceiver).receive(); + } + +} diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index 2e21ae75f..621d49fe6 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -44,6 +44,7 @@ spring-cloud-sleuth-instrumentation-feign-tests spring-cloud-sleuth-instrumentation-gateway-tests spring-cloud-sleuth-instrumentation-grpc-tests + spring-cloud-sleuth-instrumentation-kafka-tests spring-cloud-sleuth-instrumentation-lettuce-tests spring-cloud-sleuth-instrumentation-messaging-tests spring-cloud-sleuth-instrumentation-mvc-tests diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/pom.xml new file mode 100644 index 000000000..6faff3f17 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/pom.xml @@ -0,0 +1,97 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-kafka-tests + jar + Spring Cloud Sleuth Brave Kafka Instrumentation Tests + Spring Cloud Sleuth Brave Kafka Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + + + + ${project.groupId} + spring-cloud-sleuth-tests-common + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-test + + + io.projectreactor.kafka + reactor-kafka + true + + + org.testcontainers + testcontainers + + + org.testcontainers + junit-jupiter + + + org.testcontainers + kafka + + + io.zipkin.brave + brave-tests + + + org.awaitility + awaitility + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java new file mode 100644 index 000000000..b19b6efc3 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.kafka; + +import org.springframework.cloud.sleuth.brave.BraveTestTracing; +import org.springframework.cloud.sleuth.test.TestTracingAware; + +public class KafkaProducerTest extends org.springframework.cloud.sleuth.instrument.kafka.KafkaProducerTest { + + BraveTestTracing testTracing; + + @Override + public TestTracingAware tracerTest() { + if (this.testTracing == null) { + this.testTracing = new BraveTestTracing(); + } + return this.testTracing; + } + +} diff --git a/tests/common/pom.xml b/tests/common/pom.xml index f405ccd78..16cb95b52 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -144,6 +144,26 @@ brave-tests true + + io.projectreactor.kafka + reactor-kafka + true + + + org.testcontainers + testcontainers + true + + + org.testcontainers + junit-jupiter + true + + + org.testcontainers + kafka + true + org.springframework.cloud spring-cloud-sleuth-zipkin diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaProducerTest.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaProducerTest.java new file mode 100644 index 000000000..c541eac73 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaProducerTest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.kafka.clients.producer.Callback; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.StringSerializer; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.cloud.sleuth.test.TestTracingAwareSupplier; + +import static org.awaitility.Awaitility.await; + +@Testcontainers +public abstract class KafkaProducerTest implements TestTracingAwareSupplier { + + protected Tracer tracer = tracerTest().tracing().tracer(); + + protected Propagator propagator = tracerTest().tracing().propagator(); + + protected TestSpanHandler spans = tracerTest().handler(); + + protected TracingKafkaProducer kafkaProducer; + + @Container + protected final KafkaContainer kafkaContainer = new KafkaContainer( + DockerImageName.parse("confluentinc/cp-kafka:5.2.1")).withExposedPorts(9093) + .waitingFor(Wait.forListeningPort()); + + @BeforeEach + void setup() { + kafkaContainer.start(); + Map properties = new HashMap<>(); + properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers()); + properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + kafkaProducer = new TracingKafkaProducer<>(new KafkaProducer<>(properties), tracer, propagator, + new TracingKafkaPropagatorSetter()); + } + + @AfterEach + void destroy() { + kafkaContainer.stop(); + } + + @Test + public void should_create_and_finish_producer_span() { + AtomicBoolean acknowledged = new AtomicBoolean(false); + Callback callback = (metadata, ex) -> acknowledged.set(true); + ProducerRecord producerRecord = new ProducerRecord<>("spring-cloud-sleuth-otel-topic", "test", + "test"); + this.kafkaProducer.send(producerRecord, callback); + await().atMost(Duration.ofSeconds(5)).until(acknowledged::get); + + BDDAssertions.then(this.tracer.currentSpan()).isNull(); + BDDAssertions.then(this.spans).isNotEmpty(); + BDDAssertions.then(this.spans.get(0).getKind()).isEqualTo(Span.Kind.PRODUCER); + } + + @Override + public void cleanUpTracing() { + this.spans.clear(); + } + +} From 0f23f9d52fd7bf453debd8ff2846bc19f845f3bc Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 26 Apr 2021 15:19:51 +0200 Subject: [PATCH 08/59] Spring Cloud Deployer feature (#1925) * Spring Cloud Deployer; fixes gh-1905 * Polish --- pom.xml | 912 +++++++++--------- spring-cloud-sleuth-autoconfigure/pom.xml | 905 ++++++++--------- .../TraceDeployerAutoConfiguration.java | 52 + ...itional-spring-configuration-metadata.json | 306 +++--- .../main/resources/META-INF/spring.factories | 69 +- spring-cloud-sleuth-instrumentation/pom.xml | 443 ++++----- .../TraceReactiveCircuitBreaker.java | 69 +- .../instrument/deployer/TraceAppDeployer.java | 268 +++++ .../TraceAppDeployerBeanPostProcessor.java | 55 ++ .../reactor/ReactorHooksHelper.java | 9 +- .../instrument/reactor/ReactorSleuth.java | 134 ++- .../deployer/NoOpCurrentTraceContext.java | 71 ++ .../instrument/deployer/NoOpSpanInScope.java | 34 + .../instrument/deployer/NoOpTraceContext.java | 49 + .../instrument/deployer/SimpleSpan.java | 91 ++ .../instrument/deployer/SimpleTracer.java | 113 +++ .../deployer/TraceAppDeployerTests.java | 144 +++ 17 files changed, 2351 insertions(+), 1373 deletions(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceDeployerAutoConfiguration.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerBeanPostProcessor.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpCurrentTraceContext.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpSpanInScope.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpTraceContext.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java diff --git a/pom.xml b/pom.xml index 7b1064f32..3da2a0c29 100644 --- a/pom.xml +++ b/pom.xml @@ -1,452 +1,460 @@ - - - - - 4.0.0 - - spring-cloud-sleuth - 3.1.0-SNAPSHOT - pom - Spring Cloud Sleuth - Spring Cloud Sleuth - - - org.springframework.cloud - spring-cloud-build - 3.0.3-SNAPSHOT - - - - - - https://github.com/spring-cloud/spring-cloud-sleuth - scm:git:git://github.com/spring-cloud/spring-cloud-sleuth.git - - - scm:git:ssh://git@github.com/spring-cloud/spring-cloud-sleuth.git - - HEAD - - - - spring-cloud-sleuth-dependencies - spring-cloud-sleuth-api - spring-cloud-sleuth-instrumentation - spring-cloud-sleuth-brave - spring-cloud-sleuth-autoconfigure - tests - spring-cloud-sleuth-zipkin - spring-cloud-starter-sleuth - spring-cloud-sleuth-samples - docs - - - - 1.8 - 1.8 - 1.8 - 1.8 - 3.0.3-SNAPSHOT - 3.0.3-SNAPSHOT - 3.0.3-SNAPSHOT - 3.0.3-SNAPSHOT - 2.0.2-SNAPSHOT - 3.1.3-SNAPSHOT - 3.1.3-SNAPSHOT - 3.0.3-SNAPSHOT - 3.0.3-SNAPSHOT - 2.3.2-SNAPSHOT - 5.13.2 - 0.32.0 - 2.3.4.RELEASE - false - 4.9.0 - 4.8.0 - 20.0 - 1.7.1 - 3.3.0 - 3.0.1 - 2.2.0.RELEASE - - - true - false - 3.8.1 - 2.2 - 4.0.3 - 0.21.3 - 0.14.1 - 1.15.3 - - - - - - - maven-compiler-plugin - 3.8.1 - - - default-compile - - true - true - - ${maven.compiler.source} - ${maven.compiler.target} - - - - - default-testCompile - - true - true - - ${maven.compiler.testSource} - ${maven.compiler.testTarget} - - - - - - - maven-enforcer-plugin - 1.3.1 - - - enforce-java - - enforce - - - - - ${maven.compiler.testTarget} - - - - - - - - maven-deploy-plugin - 2.8.2 - - - - - - io.spring.javaformat - spring-javaformat-maven-plugin - - - maven-checkstyle-plugin - - - - - - - - maven-checkstyle-plugin - - - maven-surefire-report-plugin - - - - - - - - org.springframework.cloud - spring-cloud-sleuth-dependencies - ${project.version} - pom - import - - - org.springframework.cloud - spring-cloud-netflix-dependencies - ${spring-cloud-netflix.version} - pom - import - - - org.springframework.cloud - spring-cloud-commons-dependencies - ${spring-cloud-commons.version} - pom - import - - - org.springframework.cloud - spring-cloud-gateway-dependencies - ${spring-cloud-gateway.version} - pom - import - - - org.springframework.cloud - spring-cloud-circuitbreaker-dependencies - ${spring-cloud-circuitbreaker.version} - pom - import - - - org.springframework.cloud - spring-cloud-stream-dependencies - ${spring-cloud-stream.version} - pom - import - - - org.springframework.cloud - spring-cloud-function-dependencies - ${spring-cloud-function.version} - pom - import - - - org.springframework.cloud - spring-cloud-openfeign-dependencies - ${spring-cloud-openfeign.version} - pom - import - - - org.springframework.cloud - spring-cloud-config-dependencies - ${spring-cloud-config.version} - pom - import - - - org.springframework.cloud - spring-cloud-task-dependencies - ${spring-cloud-task.version} - pom - import - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - ${spring-security-boot-autoconfigure.version} - true - - - cglib - cglib-nodep - ${cglib-nodep.version} - - - org.objenesis - objenesis - ${objenesis.version} - - - - com.squareup.okhttp3 - mockwebserver - ${mockwebserver.version} - - - org.springframework.security.oauth - spring-security-oauth2 - ${spring-security-oauth2.version} - - - io.zipkin.aws - brave-propagation-aws - ${brave-propagation-aws.version} - - - org.hamcrest - hamcrest-core - ${hamcrest-core.version} - test - - - org.awaitility - awaitility - ${awaitility.version} - test - - - com.tngtech.archunit - archunit-junit5 - ${archunit-junit5.version} - - - org.testcontainers - testcontainers-bom - ${testcontainers.version} - pom - import - - - - - - - spring - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - true - - - false - - - - - jfrog-snapshots - JFrog Snapshots - https://oss.jfrog.org/oss-snapshot-local/ - - true - - - false - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - false - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - false - - - - - - ide - - false - - - - - maven-compiler-plugin - - ${maven.compiler.testSource} - ${maven.compiler.testTarget} - - - - - - - benchmarks - - false - - - benchmarks - - - - sonar - - - - org.jacoco - jacoco-maven-plugin - - - pre-unit-test - - prepare-agent - - - surefireArgLine - ${project.build.directory}/jacoco.exec - - - - - post-unit-test - test - - report - - - - ${project.build.directory}/jacoco.exec - - - - - - - maven-surefire-plugin - - - ${surefireArgLine} - - - - - - - - + + + + + 4.0.0 + + spring-cloud-sleuth + 3.1.0-SNAPSHOT + pom + Spring Cloud Sleuth + Spring Cloud Sleuth + + + org.springframework.cloud + spring-cloud-build + 3.0.3-SNAPSHOT + + + + + + https://github.com/spring-cloud/spring-cloud-sleuth + scm:git:git://github.com/spring-cloud/spring-cloud-sleuth.git + + + scm:git:ssh://git@github.com/spring-cloud/spring-cloud-sleuth.git + + HEAD + + + + spring-cloud-sleuth-dependencies + spring-cloud-sleuth-api + spring-cloud-sleuth-instrumentation + spring-cloud-sleuth-brave + spring-cloud-sleuth-autoconfigure + tests + spring-cloud-sleuth-zipkin + spring-cloud-starter-sleuth + spring-cloud-sleuth-samples + docs + + + + 1.8 + 1.8 + 1.8 + 1.8 + 3.0.3-SNAPSHOT + 3.0.3-SNAPSHOT + 3.0.3-SNAPSHOT + 3.0.3-SNAPSHOT + 2.0.2-SNAPSHOT + 3.1.3-SNAPSHOT + 3.1.3-SNAPSHOT + 3.0.3-SNAPSHOT + 3.0.3-SNAPSHOT + 2.3.2-SNAPSHOT + 2.5.1 + 5.13.2 + 0.32.0 + 2.3.4.RELEASE + false + 4.9.0 + 4.8.0 + 20.0 + 1.7.1 + 3.3.0 + 3.0.1 + 2.2.0.RELEASE + + + true + false + 3.8.1 + 2.2 + 4.0.3 + 0.21.3 + 0.14.1 + 1.15.3 + + + + + + + maven-compiler-plugin + 3.8.1 + + + default-compile + + true + true + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + default-testCompile + + true + true + + ${maven.compiler.testSource} + ${maven.compiler.testTarget} + + + + + + + maven-enforcer-plugin + 1.3.1 + + + enforce-java + + enforce + + + + + ${maven.compiler.testTarget} + + + + + + + + maven-deploy-plugin + 2.8.2 + + + + + + io.spring.javaformat + spring-javaformat-maven-plugin + + + maven-checkstyle-plugin + + + + + + + + maven-checkstyle-plugin + + + maven-surefire-report-plugin + + + + + + + + org.springframework.cloud + spring-cloud-sleuth-dependencies + ${project.version} + pom + import + + + org.springframework.cloud + spring-cloud-netflix-dependencies + ${spring-cloud-netflix.version} + pom + import + + + org.springframework.cloud + spring-cloud-commons-dependencies + ${spring-cloud-commons.version} + pom + import + + + org.springframework.cloud + spring-cloud-gateway-dependencies + ${spring-cloud-gateway.version} + pom + import + + + org.springframework.cloud + spring-cloud-circuitbreaker-dependencies + ${spring-cloud-circuitbreaker.version} + pom + import + + + org.springframework.cloud + spring-cloud-stream-dependencies + ${spring-cloud-stream.version} + pom + import + + + org.springframework.cloud + spring-cloud-function-dependencies + ${spring-cloud-function.version} + pom + import + + + org.springframework.cloud + spring-cloud-openfeign-dependencies + ${spring-cloud-openfeign.version} + pom + import + + + org.springframework.cloud + spring-cloud-config-dependencies + ${spring-cloud-config.version} + pom + import + + + org.springframework.cloud + spring-cloud-task-dependencies + ${spring-cloud-task.version} + pom + import + + + org.springframework.cloud + spring-cloud-deployer-dependencies + ${spring-cloud-deployer.version} + import + pom + + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + ${spring-security-boot-autoconfigure.version} + true + + + cglib + cglib-nodep + ${cglib-nodep.version} + + + org.objenesis + objenesis + ${objenesis.version} + + + + com.squareup.okhttp3 + mockwebserver + ${mockwebserver.version} + + + org.springframework.security.oauth + spring-security-oauth2 + ${spring-security-oauth2.version} + + + io.zipkin.aws + brave-propagation-aws + ${brave-propagation-aws.version} + + + org.hamcrest + hamcrest-core + ${hamcrest-core.version} + test + + + org.awaitility + awaitility + ${awaitility.version} + test + + + com.tngtech.archunit + archunit-junit5 + ${archunit-junit5.version} + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + + + + + spring + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + false + + + + + jfrog-snapshots + JFrog Snapshots + https://oss.jfrog.org/oss-snapshot-local/ + + true + + + false + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + ide + + false + + + + + maven-compiler-plugin + + ${maven.compiler.testSource} + ${maven.compiler.testTarget} + + + + + + + benchmarks + + false + + + benchmarks + + + + sonar + + + + org.jacoco + jacoco-maven-plugin + + + pre-unit-test + + prepare-agent + + + surefireArgLine + ${project.build.directory}/jacoco.exec + + + + + post-unit-test + test + + report + + + + ${project.build.directory}/jacoco.exec + + + + + + + maven-surefire-plugin + + + ${surefireArgLine} + + + + + + + + diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index ac237bbf7..994f00cd4 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -1,450 +1,455 @@ - - - - - 4.0.0 - - spring-cloud-sleuth-autoconfigure - jar - Spring Cloud Sleuth AutoConfigure - Spring Cloud Sleuth AutoConfigure - - - org.springframework.cloud - spring-cloud-sleuth - 3.1.0-SNAPSHOT - .. - - - - - - org.springframework.cloud - spring-cloud-sleuth-instrumentation - - - org.springframework.boot - spring-boot-starter-web - true - - - io.micrometer - micrometer-core - true - - - io.projectreactor - reactor-core - true - - - org.reactivestreams - reactive-streams - true - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.boot - spring-boot-starter-actuator - true - - - org.springframework.integration - spring-integration-core - true - - - org.springframework.cloud - spring-cloud-config-server - true - - - org.springframework.cloud - spring-cloud-starter-config - true - - - org.springframework.cloud - spring-cloud-function-context - true - - - org.springframework.boot - spring-boot-starter-websocket - true - - - org.springframework.cloud - spring-cloud-stream - - ${spring-cloud-stream.version} - true - - - org.springframework.cloud - spring-cloud-commons - - - org.springframework - spring-context - - - org.springframework.cloud - spring-cloud-context - true - - - org.springframework.cloud - spring-cloud-starter-task - true - - - io.reactivex - rxjava - true - - - io.github.openfeign - feign-okhttp - true - - - org.springframework.cloud - spring-cloud-starter-openfeign - true - - - org.springframework.cloud - spring-cloud-starter-loadbalancer - true - - - org.springframework.cloud - spring-cloud-starter-gateway - true - - - org.aspectj - aspectjrt - - - - org.springframework.boot - spring-boot-starter-quartz - true - - - org.springframework.boot - spring-boot-autoconfigure-processor - true - - - org.springframework.security.oauth - spring-security-oauth2 - true - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - true - - - - - org.springframework.cloud - spring-cloud-sleuth-brave - true - - - io.zipkin.brave - brave - - - io.zipkin.reporter2 - * - - - io.zipkin.zipkin2 - * - - - true - - - io.zipkin.brave - brave-context-slf4j - true - - - io.zipkin.brave - brave-instrumentation-messaging - true - - - io.zipkin.brave - brave-instrumentation-rpc - true - - - io.zipkin.brave - brave-instrumentation-spring-rabbit - true - - - io.zipkin.brave - brave-instrumentation-kafka-clients - true - - - io.zipkin.brave - brave-instrumentation-kafka-streams - true - - - io.zipkin.brave - brave-instrumentation-httpclient - true - - - io.zipkin.brave - brave-instrumentation-httpasyncclient - true - - - io.zipkin.brave - brave-instrumentation-jms - true - - - io.zipkin.brave - brave-instrumentation-mongodb - true - - - io.zipkin.aws - brave-propagation-aws - true - - - javax.jms - javax.jms-api - true - - - io.opentracing.brave - brave-opentracing - true - - - org.apache.httpcomponents - httpasyncclient - true - - - org.springframework - spring-jms - true - - - - io.github.lognet - grpc-spring-boot-starter - true - - - org.springframework.boot - spring-boot-starter - - - - - io.zipkin.brave - brave-instrumentation-grpc - true - - - io.zipkin.reporter2 - zipkin-reporter-metrics-micrometer - - - io.micrometer - micrometer-core - - - true - - - - io.lettuce - lettuce-core - true - - - org.springframework.kafka - spring-kafka - true - - - org.apache.kafka - kafka-streams - true - - - org.springframework.amqp - spring-rabbit - true - - - org.springframework.boot - spring-boot-starter-data-mongodb - true - - - - - org.springframework.cloud - spring-cloud-sleuth-zipkin - true - - - io.zipkin.zipkin2 - zipkin - true - - - io.zipkin.reporter2 - zipkin-reporter - true - - - io.zipkin.reporter2 - zipkin-reporter-brave - true - - - io.zipkin.reporter2 - zipkin-sender-kafka - true - - - - org.apache.kafka - kafka-clients - - - - - io.zipkin.reporter2 - zipkin-sender-activemq-client - true - - - org.apache.activemq - activemq-client - - - - - org.apache.activemq - activemq-client - true - - - io.zipkin.reporter2 - zipkin-sender-amqp-client - true - - - - com.rabbitmq - amqp-client - - - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.awaitility - awaitility - test - - - org.mongodb - mongodb-driver-reactivestreams - test - - - - - io.zipkin.brave - brave-instrumentation-http-tests - test - - - com.squareup.okhttp3 - mockwebserver - test - - - - - com.squareup.okhttp3 - okhttp - - 4.8.0 - test - - - com.tngtech.archunit - archunit-junit5 - test - - - - - - - fast - - false - - - - - maven-surefire-plugin - - 4 - true - -Xmx1024m -XX:MaxPermSize=256m - - - - - - - - + + + + + 4.0.0 + + spring-cloud-sleuth-autoconfigure + jar + Spring Cloud Sleuth AutoConfigure + Spring Cloud Sleuth AutoConfigure + + + org.springframework.cloud + spring-cloud-sleuth + 3.1.0-SNAPSHOT + .. + + + + + + org.springframework.cloud + spring-cloud-sleuth-instrumentation + + + org.springframework.boot + spring-boot-starter-web + true + + + io.micrometer + micrometer-core + true + + + io.projectreactor + reactor-core + true + + + org.reactivestreams + reactive-streams + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.springframework.integration + spring-integration-core + true + + + org.springframework.cloud + spring-cloud-config-server + true + + + org.springframework.cloud + spring-cloud-starter-config + true + + + org.springframework.cloud + spring-cloud-function-context + true + + + org.springframework.boot + spring-boot-starter-websocket + true + + + org.springframework.cloud + spring-cloud-stream + + ${spring-cloud-stream.version} + true + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework + spring-context + + + org.springframework.cloud + spring-cloud-context + true + + + org.springframework.cloud + spring-cloud-starter-task + true + + + org.springframework.cloud + spring-cloud-deployer-spi + true + + + io.reactivex + rxjava + true + + + io.github.openfeign + feign-okhttp + true + + + org.springframework.cloud + spring-cloud-starter-openfeign + true + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + true + + + org.springframework.cloud + spring-cloud-starter-gateway + true + + + org.aspectj + aspectjrt + + + + org.springframework.boot + spring-boot-starter-quartz + true + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springframework.security.oauth + spring-security-oauth2 + true + + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + true + + + + + org.springframework.cloud + spring-cloud-sleuth-brave + true + + + io.zipkin.brave + brave + + + io.zipkin.reporter2 + * + + + io.zipkin.zipkin2 + * + + + true + + + io.zipkin.brave + brave-context-slf4j + true + + + io.zipkin.brave + brave-instrumentation-messaging + true + + + io.zipkin.brave + brave-instrumentation-rpc + true + + + io.zipkin.brave + brave-instrumentation-spring-rabbit + true + + + io.zipkin.brave + brave-instrumentation-kafka-clients + true + + + io.zipkin.brave + brave-instrumentation-kafka-streams + true + + + io.zipkin.brave + brave-instrumentation-httpclient + true + + + io.zipkin.brave + brave-instrumentation-httpasyncclient + true + + + io.zipkin.brave + brave-instrumentation-jms + true + + + io.zipkin.brave + brave-instrumentation-mongodb + true + + + io.zipkin.aws + brave-propagation-aws + true + + + javax.jms + javax.jms-api + true + + + io.opentracing.brave + brave-opentracing + true + + + org.apache.httpcomponents + httpasyncclient + true + + + org.springframework + spring-jms + true + + + + io.github.lognet + grpc-spring-boot-starter + true + + + org.springframework.boot + spring-boot-starter + + + + + io.zipkin.brave + brave-instrumentation-grpc + true + + + io.zipkin.reporter2 + zipkin-reporter-metrics-micrometer + + + io.micrometer + micrometer-core + + + true + + + + io.lettuce + lettuce-core + true + + + org.springframework.kafka + spring-kafka + true + + + org.apache.kafka + kafka-streams + true + + + org.springframework.amqp + spring-rabbit + true + + + org.springframework.boot + spring-boot-starter-data-mongodb + true + + + + + org.springframework.cloud + spring-cloud-sleuth-zipkin + true + + + io.zipkin.zipkin2 + zipkin + true + + + io.zipkin.reporter2 + zipkin-reporter + true + + + io.zipkin.reporter2 + zipkin-reporter-brave + true + + + io.zipkin.reporter2 + zipkin-sender-kafka + true + + + + org.apache.kafka + kafka-clients + + + + + io.zipkin.reporter2 + zipkin-sender-activemq-client + true + + + org.apache.activemq + activemq-client + + + + + org.apache.activemq + activemq-client + true + + + io.zipkin.reporter2 + zipkin-sender-amqp-client + true + + + + com.rabbitmq + amqp-client + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.awaitility + awaitility + test + + + org.mongodb + mongodb-driver-reactivestreams + test + + + + + io.zipkin.brave + brave-instrumentation-http-tests + test + + + com.squareup.okhttp3 + mockwebserver + test + + + + + com.squareup.okhttp3 + okhttp + + 4.8.0 + test + + + com.tngtech.archunit + archunit-junit5 + test + + + + + + + fast + + false + + + + + maven-surefire-plugin + + 4 + true + -Xmx1024m -XX:MaxPermSize=256m + + + + + + + + diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceDeployerAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceDeployerAutoConfiguration.java new file mode 100644 index 000000000..45b7b577c --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceDeployerAutoConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.deployer; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.deployer.spi.app.AppDeployer; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.deployer.TraceAppDeployerBeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for app deployers. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(Tracer.class) +@ConditionalOnProperty(value = "spring.sleuth.deployer.enabled", matchIfMissing = true) +@ConditionalOnClass(AppDeployer.class) +@AutoConfigureAfter(BraveAutoConfiguration.class) +public class TraceDeployerAutoConfiguration { + + @Bean + static TraceAppDeployerBeanPostProcessor traceAppDeployerBeanPostProcessor(BeanFactory beanFactory, + Environment environment) { + return new TraceAppDeployerBeanPostProcessor(beanFactory, environment); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index b2ebb1d5a..ec3dadb1b 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -1,147 +1,159 @@ -{ - "properties": [ - { - "name": "spring.zipkin.kafka.topic", - "type": "java.lang.String", - "description": "Name of the Kafka topic where spans should be sent to Zipkin.", - "defaultValue": "zipkin" - }, - { - "name": "spring.zipkin.rabbitmq.queue", - "type": "java.lang.String", - "description": "Name of the RabbitMQ queue where spans should be sent to Zipkin.", - "defaultValue": "zipkin" - }, - { - "name": "spring.zipkin.rabbitmq.addresses", - "type": "java.lang.String", - "description": "Addresses of the RabbitMQ brokers used to send spans to Zipkin" - }, - { - "name": "spring.zipkin.activemq.queue", - "type": "java.lang.String", - "description": "Name of the ActiveMQ queue where spans should be sent to Zipkin.", - "defaultValue": "zipkin" - }, - { - "name": "spring.zipkin.activemq.message-max-bytes", - "type": "java.lang.String", - "description": "Maximum number of bytes for a given message with spans sent to Zipkin over ActiveMQ.", - "defaultValue": 100000 - }, - { - "name": "spring.sleuth.function.enabled", - "type": "java.lang.Boolean", - "description": "Enable instrumenting of Spring Cloud Function and Spring Cloud Function based projects (e.g. Spring Cloud Stream).", - "defaultValue": true - }, - { - "name": "spring.sleuth.integration.websockets.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing for WebSockets.", - "defaultValue": true - }, - { - "name": "spring.sleuth.async.enabled", - "type": "java.lang.Boolean", - "description": "Enable instrumenting async related components so that the tracing information is passed between threads.", - "defaultValue": true - }, - { - "name": "spring.sleuth.async.configurer.enabled", - "type": "java.lang.Boolean", - "description": "Enable default AsyncConfigurer.", - "defaultValue": true - }, - { - "name": "spring.sleuth.feign.enabled", - "type": "java.lang.Boolean", - "description": "Enable span information propagation when using Feign.", - "defaultValue": true - }, - { - "name": "spring.sleuth.feign.processor.enabled", - "type": "java.lang.Boolean", - "description": "Enable post processor that wraps Feign Context in its tracing representations.", - "defaultValue": true - }, - { - "name": "spring.sleuth.grpc.enabled", - "type": "java.lang.Boolean", - "description": "Enable span information propagation when using GRPC.", - "defaultValue": true - }, - { - "name": "spring.sleuth.messaging.jms.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing of JMS.", - "defaultValue": true - }, - { - "name": "spring.sleuth.messaging.rabbit.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing of RabbitMQ.", - "defaultValue": true - }, - { - "name": "spring.sleuth.messaging.kafka.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing of Kafka.", - "defaultValue": true - }, - { - "name": "spring.sleuth.messaging.kafka.mapper.enabled", - "type": "java.lang.Boolean", - "description": "Enable DefaultKafkaHeaderMapper tracing for Kafka.", - "defaultValue": true - }, - { - "name": "spring.sleuth.quartz.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing for Quartz.", - "defaultValue": true - }, - { - "name": "spring.sleuth.mongodb.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing for MongoDb.", - "defaultValue": true - }, - { - "name": "spring.sleuth.rpc.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing of RPC.", - "defaultValue": true - }, - { - "name": "spring.sleuth.sampler.refresh.enabled", - "type": "java.lang.Boolean", - "description": "Enable refresh scope for sampler.", - "defaultValue": true - }, - { - "name": "spring.sleuth.web.webclient.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing instrumentation for WebClient.", - "defaultValue": true - }, - { - "name": "spring.sleuth.integration.enabled", - "type": "java.lang.Boolean", - "description": "Enable Spring Integration instrumentation.", - "defaultValue": true - }, - { - "name": "spring.sleuth.task.enabled", - "type": "java.lang.Boolean", - "description": "Enable Spring Cloud Task instrumentation.", - "defaultValue": true - }, - { - "name": "spring.sleuth.config.server.enabled", - "type": "java.lang.Boolean", - "description": "Enable Spring Cloud Config Server instrumentation.", - "defaultValue": true - } - ] -} +{ + "properties": [ + { + "name": "spring.zipkin.kafka.topic", + "type": "java.lang.String", + "description": "Name of the Kafka topic where spans should be sent to Zipkin.", + "defaultValue": "zipkin" + }, + { + "name": "spring.zipkin.rabbitmq.queue", + "type": "java.lang.String", + "description": "Name of the RabbitMQ queue where spans should be sent to Zipkin.", + "defaultValue": "zipkin" + }, + { + "name": "spring.zipkin.rabbitmq.addresses", + "type": "java.lang.String", + "description": "Addresses of the RabbitMQ brokers used to send spans to Zipkin" + }, + { + "name": "spring.zipkin.activemq.queue", + "type": "java.lang.String", + "description": "Name of the ActiveMQ queue where spans should be sent to Zipkin.", + "defaultValue": "zipkin" + }, + { + "name": "spring.zipkin.activemq.message-max-bytes", + "type": "java.lang.String", + "description": "Maximum number of bytes for a given message with spans sent to Zipkin over ActiveMQ.", + "defaultValue": 100000 + }, + { + "name": "spring.sleuth.function.enabled", + "type": "java.lang.Boolean", + "description": "Enable instrumenting of Spring Cloud Function and Spring Cloud Function based projects (e.g. Spring Cloud Stream).", + "defaultValue": true + }, + { + "name": "spring.sleuth.integration.websockets.enabled", + "type": "java.lang.Boolean", + "description": "Enable tracing for WebSockets.", + "defaultValue": true + }, + { + "name": "spring.sleuth.async.enabled", + "type": "java.lang.Boolean", + "description": "Enable instrumenting async related components so that the tracing information is passed between threads.", + "defaultValue": true + }, + { + "name": "spring.sleuth.async.configurer.enabled", + "type": "java.lang.Boolean", + "description": "Enable default AsyncConfigurer.", + "defaultValue": true + }, + { + "name": "spring.sleuth.feign.enabled", + "type": "java.lang.Boolean", + "description": "Enable span information propagation when using Feign.", + "defaultValue": true + }, + { + "name": "spring.sleuth.feign.processor.enabled", + "type": "java.lang.Boolean", + "description": "Enable post processor that wraps Feign Context in its tracing representations.", + "defaultValue": true + }, + { + "name": "spring.sleuth.grpc.enabled", + "type": "java.lang.Boolean", + "description": "Enable span information propagation when using GRPC.", + "defaultValue": true + }, + { + "name": "spring.sleuth.messaging.jms.enabled", + "type": "java.lang.Boolean", + "description": "Enable tracing of JMS.", + "defaultValue": true + }, + { + "name": "spring.sleuth.messaging.rabbit.enabled", + "type": "java.lang.Boolean", + "description": "Enable tracing of RabbitMQ.", + "defaultValue": true + }, + { + "name": "spring.sleuth.messaging.kafka.enabled", + "type": "java.lang.Boolean", + "description": "Enable tracing of Kafka.", + "defaultValue": true + }, + { + "name": "spring.sleuth.messaging.kafka.mapper.enabled", + "type": "java.lang.Boolean", + "description": "Enable DefaultKafkaHeaderMapper tracing for Kafka.", + "defaultValue": true + }, + { + "name": "spring.sleuth.quartz.enabled", + "type": "java.lang.Boolean", + "description": "Enable tracing for Quartz.", + "defaultValue": true + }, + { + "name": "spring.sleuth.mongodb.enabled", + "type": "java.lang.Boolean", + "description": "Enable tracing for MongoDb.", + "defaultValue": true + }, + { + "name": "spring.sleuth.rpc.enabled", + "type": "java.lang.Boolean", + "description": "Enable tracing of RPC.", + "defaultValue": true + }, + { + "name": "spring.sleuth.sampler.refresh.enabled", + "type": "java.lang.Boolean", + "description": "Enable refresh scope for sampler.", + "defaultValue": true + }, + { + "name": "spring.sleuth.web.webclient.enabled", + "type": "java.lang.Boolean", + "description": "Enable tracing instrumentation for WebClient.", + "defaultValue": true + }, + { + "name": "spring.sleuth.integration.enabled", + "type": "java.lang.Boolean", + "description": "Enable Spring Integration instrumentation.", + "defaultValue": true + }, + { + "name": "spring.sleuth.task.enabled", + "type": "java.lang.Boolean", + "description": "Enable Spring Cloud Task instrumentation.", + "defaultValue": true + }, + { + "name": "spring.sleuth.deployer.enabled", + "type": "java.lang.Boolean", + "description": "Enable Spring Cloud Deployer instrumentation.", + "defaultValue": true + }, + { + "name": "spring.sleuth.deployer.status-poll-delay", + "type": "java.lang.Long", + "description": "Default poll delay to retrieve the deployed application status.", + "defaultValue": 1000 + }, + { + "name": "spring.sleuth.config.server.enabled", + "type": "java.lang.Boolean", + "description": "Enable Spring Cloud Config Server instrumentation.", + "defaultValue": true + } + ] +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index d2df0f518..d96b177e0 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,34 +1,35 @@ -# Auto Configuration -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncCustomAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncDefaultAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.config.TraceSpringCloudConfigAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.circuitbreaker.TraceCircuitBreakerAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.rxjava.TraceRxJavaAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.quartz.TraceQuartzAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.task.TraceTaskAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.web.TraceWebAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.web.client.TraceWebClientAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.web.client.feign.TraceFeignClientAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.web.client.TraceWebAsyncClientAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.scheduling.TraceSchedulingAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.reactor.TraceReactorAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceFunctionAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringIntegrationAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringMessagingAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceWebSocketAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.brave.instrument.web.client.BraveWebClientAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.brave.instrument.rpc.BraveRpcAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.brave.instrument.grpc.BraveGrpcAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.brave.instrument.messaging.BraveKafkaStreamsAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.brave.instrument.messaging.BraveMessagingAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.brave.instrument.opentracing.BraveOpentracingAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.brave.instrument.redis.BraveRedisAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.brave.instrument.mongodb.BraveMongoDbAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.zipkin2.ZipkinAutoConfiguration -# Environment Post Processor -org.springframework.boot.env.EnvironmentPostProcessor=\ -org.springframework.cloud.sleuth.autoconfig.TraceEnvironmentPostProcessor,\ -org.springframework.cloud.sleuth.autoconfig.instrument.web.client.TraceGatewayEnvironmentPostProcessor \ No newline at end of file +# Auto Configuration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncCustomAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncDefaultAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.config.TraceSpringCloudConfigAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.circuitbreaker.TraceCircuitBreakerAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.deployer.TraceDeployerAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.rxjava.TraceRxJavaAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.quartz.TraceQuartzAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.task.TraceTaskAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.web.TraceWebAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.web.client.TraceWebClientAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.web.client.feign.TraceFeignClientAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.web.client.TraceWebAsyncClientAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.scheduling.TraceSchedulingAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.reactor.TraceReactorAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceFunctionAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringIntegrationAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringMessagingAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceWebSocketAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.brave.instrument.web.client.BraveWebClientAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.brave.instrument.rpc.BraveRpcAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.brave.instrument.grpc.BraveGrpcAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.brave.instrument.messaging.BraveKafkaStreamsAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.brave.instrument.messaging.BraveMessagingAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.brave.instrument.opentracing.BraveOpentracingAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.brave.instrument.redis.BraveRedisAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.brave.instrument.mongodb.BraveMongoDbAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.zipkin2.ZipkinAutoConfiguration +# Environment Post Processor +org.springframework.boot.env.EnvironmentPostProcessor=\ +org.springframework.cloud.sleuth.autoconfig.TraceEnvironmentPostProcessor,\ +org.springframework.cloud.sleuth.autoconfig.instrument.web.client.TraceGatewayEnvironmentPostProcessor diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index 4793fa853..0dfdf55a4 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -1,219 +1,224 @@ - - - - - 4.0.0 - - spring-cloud-sleuth-instrumentation - jar - Spring Cloud Sleuth Instrumentation - Spring Cloud Sleuth Instrumentation - - - org.springframework.cloud - spring-cloud-sleuth - 3.1.0-SNAPSHOT - .. - - - - - org.springframework.cloud - spring-cloud-sleuth-api - - - org.springframework.boot - spring-boot-starter-web - true - - - io.micrometer - micrometer-core - true - - - io.projectreactor - reactor-core - true - - - io.projectreactor.kafka - reactor-kafka - true - - - org.reactivestreams - reactive-streams - true - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.boot - spring-boot-starter-actuator - true - - - org.springframework.integration - spring-integration-core - true - - - org.springframework.cloud - spring-cloud-config-server - true - - - org.springframework.cloud - spring-cloud-starter-config - true - - - org.springframework.cloud - spring-cloud-function-context - true - - - org.springframework.boot - spring-boot-starter-websocket - true - - - org.springframework.cloud - spring-cloud-stream - - ${spring-cloud-stream.version} - true - - - org.springframework.cloud - spring-cloud-commons - - - org.springframework - spring-context - - - org.springframework.cloud - spring-cloud-context - true - - - io.reactivex - rxjava - true - - - io.github.openfeign - feign-okhttp - true - - - org.springframework.cloud - spring-cloud-starter-openfeign - true - - - org.springframework.cloud - spring-cloud-starter-loadbalancer - true - - - org.springframework.cloud - spring-cloud-starter-gateway - true - - - org.springframework.cloud - spring-cloud-starter-task - true - - - org.aspectj - aspectjrt - - - - org.springframework.boot - spring-boot-starter-quartz - true - - - org.springframework.boot - spring-boot-autoconfigure-processor - true - - - org.springframework.security.oauth - spring-security-oauth2 - true - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - true - - - - org.springframework.boot - spring-boot-starter-test - test - - - org.awaitility - awaitility - test - - - com.tngtech.archunit - archunit-junit5 - test - - - io.projectreactor - reactor-test - test - - - - - - fast - - false - - - - - maven-surefire-plugin - - 4 - true - -Xmx1024m -XX:MaxPermSize=256m - - - - - - - - + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation + jar + Spring Cloud Sleuth Instrumentation + Spring Cloud Sleuth Instrumentation + + + org.springframework.cloud + spring-cloud-sleuth + 3.1.0-SNAPSHOT + .. + + + + + org.springframework.cloud + spring-cloud-sleuth-api + + + org.springframework.boot + spring-boot-starter-web + true + + + io.micrometer + micrometer-core + true + + + io.projectreactor + reactor-core + true + + + io.projectreactor.kafka + reactor-kafka + true + + + org.reactivestreams + reactive-streams + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.springframework.integration + spring-integration-core + true + + + org.springframework.cloud + spring-cloud-config-server + true + + + org.springframework.cloud + spring-cloud-starter-config + true + + + org.springframework.cloud + spring-cloud-function-context + true + + + org.springframework.boot + spring-boot-starter-websocket + true + + + org.springframework.cloud + spring-cloud-stream + + ${spring-cloud-stream.version} + true + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework + spring-context + + + org.springframework.cloud + spring-cloud-context + true + + + io.reactivex + rxjava + true + + + io.github.openfeign + feign-okhttp + true + + + org.springframework.cloud + spring-cloud-starter-openfeign + true + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + true + + + org.springframework.cloud + spring-cloud-starter-gateway + true + + + org.springframework.cloud + spring-cloud-starter-task + true + + + org.springframework.cloud + spring-cloud-deployer-spi + true + + + org.aspectj + aspectjrt + + + + org.springframework.boot + spring-boot-starter-quartz + true + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springframework.security.oauth + spring-security-oauth2 + true + + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.awaitility + awaitility + test + + + com.tngtech.archunit + archunit-junit5 + test + + + io.projectreactor + reactor-test + test + + + + + + fast + + false + + + + + maven-surefire-plugin + + 4 + true + -Xmx1024m -XX:MaxPermSize=256m + + + + + + + + diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreaker.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreaker.java index b283db6e8..ecf8cce3c 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreaker.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreaker.java @@ -19,22 +19,16 @@ package org.springframework.cloud.sleuth.instrument.circuitbreaker; import java.util.function.Function; import java.util.function.Supplier; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.util.context.Context; import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker; import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.TraceContext; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; class TraceReactiveCircuitBreaker implements ReactiveCircuitBreaker { - private static final Log log = LogFactory.getLog(TraceReactiveCircuitBreaker.class); - private final ReactiveCircuitBreaker delegate; private final Tracer tracer; @@ -71,68 +65,11 @@ class TraceReactiveCircuitBreaker implements ReactiveCircuitBreaker { } private Mono runAndTraceMono(Supplier> mono) { - return Mono.deferContextual(contextView -> { - Span span = contextView.get(Span.class); - Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); - return mono.get().doOnError(span::error).doFinally(signalType -> { - span.end(); - scope.close(); - }); - }).contextWrite(this::enhanceContext); + return ReactorSleuth.tracedMono(this.tracer, this.currentTraceContext, "function", mono); } private Flux runAndTraceFlux(Supplier> flux) { - return Flux.deferContextual(contextView -> { - Span span = contextView.get(Span.class); - Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); - return flux.get().doOnError(span::error).doFinally(signalType -> { - span.end(); - scope.close(); - }); - }).contextWrite(this::enhanceContext); - } - - private Span spanFromContext(reactor.util.context.Context context) { - TraceContext traceContext = context.getOrDefault(TraceContext.class, null); - Span span = null; - if (traceContext == null) { - span = context.getOrDefault(Span.class, null); - } - if (traceContext == null && span == null) { - span = this.tracer.nextSpan(); - if (log.isDebugEnabled()) { - log.debug("There was no previous span in reactor context, created a new one [" + span + "]"); - } - } - else if (traceContext != null) { - // there was a previous span - we create a child one - try (CurrentTraceContext.Scope scope = this.currentTraceContext.maybeScope(traceContext)) { - if (log.isDebugEnabled()) { - log.debug("Found a trace context in reactor context [" + traceContext + "]"); - } - span = this.tracer.nextSpan(); - if (log.isDebugEnabled()) { - log.debug("Created a child span [" + span + "]"); - } - } - } - else { - if (log.isDebugEnabled()) { - log.debug("Found a span in reactor context [" + span + "]"); - } - span = this.tracer.nextSpan(span); - if (log.isDebugEnabled()) { - log.debug("Created a child span [" + span + "]"); - } - } - // TODO: Better name? - return span.name("function"); - } - - private Context enhanceContext(Context context) { - Span span = spanFromContext(context); - return context.put(Span.class, span).put(TraceContext.class, span.context()).put(Tracer.SpanInScope.class, - this.tracer.withSpan(span)); + return ReactorSleuth.tracedFlux(this.tracer, this.currentTraceContext, "function", flux); } } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java new file mode 100644 index 000000000..4ea4de184 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java @@ -0,0 +1,268 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.deployer; + +import java.time.Duration; +import java.util.Arrays; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.deployer.spi.app.AppDeployer; +import org.springframework.cloud.deployer.spi.app.AppScaleRequest; +import org.springframework.cloud.deployer.spi.app.AppStatus; +import org.springframework.cloud.deployer.spi.app.DeploymentState; +import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; +import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; +import org.springframework.core.env.Environment; + +/** + * Trace representation of an {@link AppDeployer}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceAppDeployer implements AppDeployer { + + private static final Log log = LogFactory.getLog(TraceAppDeployer.class); + + private final AppDeployer delegate; + + private final BeanFactory beanFactory; + + private final Environment environment; + + private Tracer tracer; + + private CurrentTraceContext currentTraceContext; + + private Long pollDelay; + + public TraceAppDeployer(AppDeployer delegate, BeanFactory beanFactory, Environment environment) { + this.delegate = delegate; + this.beanFactory = beanFactory; + this.environment = environment; + } + + @Override + public String deploy(AppDeploymentRequest request) { + Span span = tracer().nextSpan().name("deploy"); + // TODO: Is this secure to pass? + // TODO: Does it make sense? + // if (!request.getCommandlineArguments().isEmpty()) { + // span.tag("commandlineArguments", request.getCommandlineArguments().toString()); + // } + // if (!request.getDeploymentProperties().isEmpty()) { + // span.tag("deploymentProperties", request.getDeploymentProperties().toString()); + // } + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + span.event("start"); + String id = this.delegate.deploy(request); + span.tag("id", id); + registerListener(span, id); + return id; + } + } + + private void registerListener(Span span, String id) { + PreviousAndCurrentStatus previousAndCurrentStatus = new PreviousAndCurrentStatus(span); + // @formatter:off + this.delegate.statusReactive(id) + .map(previousAndCurrentStatus::updateCurrent) + .repeatWhen(repeat -> repeat.flatMap(i -> Mono.delay(Duration.ofMillis(pollDelay())))) + .takeUntil(PreviousAndCurrentStatus::isFinished) + .last() + .doOnNext(PreviousAndCurrentStatus::annotateSpan) + .doOnError(span::error) + // we will close the span in the reactive part + .doFinally(signalType -> span.end()).subscribe(); + // @formatter:on + } + + @Override + public void undeploy(String id) { + Span span = tracer().nextSpan().name("undeploy"); + span.tag("id", id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + span.event("start"); + this.delegate.undeploy(id); + registerListener(span, id); + } + finally { + span.end(); + } + } + + @Override + public AppStatus status(String id) { + Span span = tracer().nextSpan().name("status"); + span.tag("id", id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + return this.delegate.status(id); + } + finally { + span.end(); + } + } + + @Override + public Mono statusReactive(String id) { + return ReactorSleuth.tracedMono(tracer(), currentTraceContext(), "status", + () -> this.delegate.statusReactive(id), span -> span.tag("id", id)); + } + + @Override + public Flux statusesReactive(String... ids) { + return ReactorSleuth.tracedFlux(tracer(), currentTraceContext(), "statuses", + () -> this.delegate.statusesReactive(ids), span -> span.tag("ids", Arrays.toString(ids))); + } + + @Override + public RuntimeEnvironmentInfo environmentInfo() { + return this.delegate.environmentInfo(); + } + + @Override + public String getLog(String id) { + Span span = tracer().nextSpan().name("getLog"); + span.tag("id", id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + return this.delegate.getLog(id); + } + finally { + span.end(); + } + } + + @Override + public void scale(AppScaleRequest appScaleRequest) { + Span span = tracer().nextSpan().name("scale"); + span.tag("deploymentId", appScaleRequest.getDeploymentId()); + span.tag("count", String.valueOf(appScaleRequest.getCount())); + // TODO: Is this secure to pass? + // TODO: Does it make sense? + // if (appScaleRequest.getProperties().isPresent() && + // !appScaleRequest.getProperties().get().isEmpty()) { + // span.tag("properties", appScaleRequest.getProperties().get().toString()); + // } + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + this.delegate.scale(appScaleRequest); + } + finally { + span.end(); + } + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + + private CurrentTraceContext currentTraceContext() { + if (this.currentTraceContext == null) { + this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); + } + return this.currentTraceContext; + } + + private long pollDelay() { + if (this.pollDelay == null) { + this.pollDelay = this.environment.getProperty("spring.sleuth.deployer.status-poll-delay", Long.class, 500L); + } + return this.pollDelay; + } + + private static final class PreviousAndCurrentStatus { + + private final Span span; + + private AppStatus current; + + private AppStatus previous; + + private PreviousAndCurrentStatus(Span span) { + this.span = span; + if (log.isDebugEnabled()) { + log.debug("Current span is [" + span + "]"); + } + } + + private PreviousAndCurrentStatus updateCurrent(AppStatus current) { + if (log.isTraceEnabled()) { + log.trace("State before change: current [" + this.current + "], previous [" + this.previous + "]"); + } + this.previous = this.current; + this.current = current; + if (log.isTraceEnabled()) { + log.trace("State after change: current [" + this.current + "], previous [" + this.previous + "]"); + } + if (statusChanged()) { + annotateSpan(); + } + else if (log.isTraceEnabled()) { + log.trace("State has not changed, will not annotate the span"); + } + return this; + } + + private void annotateSpan() { + String name = this.current.getState().name(); + if (log.isDebugEnabled()) { + log.debug("Will annotate its state with [" + name + "]"); + } + this.span.event(name); + } + + private boolean statusChanged() { + if (this.previous == null && this.current != null) { + if (log.isDebugEnabled()) { + log.debug("Previous is null, current is not null"); + } + return true; + } + else if (this.current == null) { + throw new IllegalStateException("Current state can't be null"); + } + DeploymentState currentState = this.current.getState(); + DeploymentState previousState = this.previous.getState(); + return currentState != previousState; + } + + private boolean isFinished() { + boolean finished = this.current.getState() == DeploymentState.deployed + || this.current.getState() == DeploymentState.undeployed + || this.current.getState() == DeploymentState.failed + || this.current.getState() == DeploymentState.error + || this.current.getState() == DeploymentState.unknown; + if (log.isTraceEnabled()) { + log.trace("Status is finished [" + finished + "]"); + } + return finished; + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerBeanPostProcessor.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerBeanPostProcessor.java new file mode 100644 index 000000000..616ba68df --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerBeanPostProcessor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.deployer; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.deployer.spi.app.AppDeployer; +import org.springframework.core.env.Environment; + +/** + * {@link BeanPostProcessor} to wrap a {@link AppDeployer} instance into its trace + * representation. + * + * @author Marcin Grzejszczak + * @since 2.0.0 + */ +public class TraceAppDeployerBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + private final Environment environment; + + public TraceAppDeployerBeanPostProcessor(BeanFactory beanFactory, Environment environment) { + this.beanFactory = beanFactory; + this.environment = environment; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof AppDeployer && !(bean instanceof TraceAppDeployer)) { + return new TraceAppDeployer((AppDeployer) bean, this.beanFactory, this.environment); + } + return bean; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorHooksHelper.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorHooksHelper.java index dd759fbbd..135a4be6d 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorHooksHelper.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorHooksHelper.java @@ -60,13 +60,13 @@ import org.springframework.util.Assert; * .scan((l, r) -> l + r) // (-) * .doOnNext(it -> { // (-) * //log - * }) + * }) * .doFirst(() -> { // (-) * //log - * }) + * }) * .doFinally(signalType -> { // (-) * //log - * }) + * }) * .subscribeOn(Schedulers.parallel()) //(+) * .subscribe();//(*) * (*) - captures tracing context if it differs from what was captured before at subscription and propagates it. @@ -84,7 +84,7 @@ import org.springframework.util.Assert; * .map(it -> ...) // (+) is SYNC but should add hook as previous Processor/operator does not use hooks * .doOnNext(it -> { // (-) is SYNC no need to wrap * //log - * }) + * }) * .subscribe(); *} */ @@ -93,6 +93,7 @@ final class ReactorHooksHelper { // need a way to determine SYNC sources to not add redundant scope passing decorator // most of reactor-core SYNC sources are marked with SourceProducer interface static final Class sourceProducerClass; + static { Class c; try { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java index 82dd63265..dabadd1a3 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java @@ -17,8 +17,10 @@ package org.springframework.cloud.sleuth.instrument.reactor; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -28,16 +30,20 @@ import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Fuseable; import reactor.core.Scannable; +import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; -import reactor.util.annotation.Nullable; import reactor.util.context.Context; import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.TraceContext; import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.internal.LazyBean; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; /** * Reactive Span pointcuts factories. @@ -314,6 +320,132 @@ public abstract class ReactorSleuth { }; } + /** + * Wraps the given Mono in a trace representation. Retrieves the span from context, + * creates a child span with the given name. + * @param tracer - Tracer bean + * @param currentTraceContext - CurrentTraceContext bean + * @param childSpanName - name of the created child span + * @param supplier - supplier of a {@link Mono} to be wrapped in tracing + * @param - type returned by the Mono + * @param spanCustomizer - customizer for the child span + * @return traced Mono + */ + public static Mono tracedMono(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, + @NonNull String childSpanName, @NonNull Supplier> supplier, + @NonNull Consumer spanCustomizer) { + return Mono.deferContextual(contextView -> { + Span span = contextView.get(Span.class); + spanCustomizer.accept(span); + Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); + return supplier.get().doOnError(span::error).doFinally(signalType -> { + span.end(); + scope.close(); + }); + }).contextWrite(context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); + } + + /** + * Wraps the given Mono in a trace representation. Retrieves the span from context, + * creates a child span with the given name. + * @param tracer - Tracer bean + * @param currentTraceContext - CurrentTraceContext bean + * @param childSpanName - name of the created child span + * @param supplier - supplier of a {@link Mono} to be wrapped in tracing + * @param - type returned by the Mono + * @return traced Mono + */ + public static Mono tracedMono(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, + @NonNull String childSpanName, @NonNull Supplier> supplier) { + return tracedMono(tracer, currentTraceContext, childSpanName, supplier, span -> { + }); + } + + /** + * Wraps the given Flux in a trace representation. Retrieves the span from context, + * creates a child span with the given name. + * @param tracer - Tracer bean + * @param currentTraceContext - CurrentTraceContext bean + * @param childSpanName - name of the created child span + * @param supplier - supplier of a {@link Flux} to be wrapped in tracing + * @param - type returned by the Flux + * @param spanCustomizer - customizer for the child span + * @return traced Flux + */ + public static Flux tracedFlux(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, + @NonNull String childSpanName, @NonNull Supplier> supplier, + @NonNull Consumer spanCustomizer) { + return Flux.deferContextual(contextView -> { + Span span = contextView.get(Span.class); + spanCustomizer.accept(span); + Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); + return supplier.get().doOnError(span::error).doFinally(signalType -> { + span.end(); + scope.close(); + }); + }).contextWrite(context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); + } + + /** + * Wraps the given Flux in a trace representation. Retrieves the span from context, + * creates a child span with the given name. + * @param tracer - Tracer bean + * @param currentTraceContext - CurrentTraceContext bean + * @param childSpanName - name of the created child span + * @param supplier - supplier of a {@link Flux} to be wrapped in tracing + * @param - type returned by the Flux + * @return traced Flux + */ + public static Flux tracedFlux(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, + @NonNull String childSpanName, @NonNull Supplier> supplier) { + return tracedFlux(tracer, currentTraceContext, childSpanName, supplier, span -> { + }); + } + + private static Span spanFromContext(Tracer tracer, CurrentTraceContext currentTraceContext, + reactor.util.context.Context context, String childSpanName) { + TraceContext traceContext = context.getOrDefault(TraceContext.class, null); + Span span = null; + if (traceContext == null) { + span = context.getOrDefault(Span.class, null); + } + if (traceContext == null && span == null) { + span = tracer.nextSpan(); + if (log.isDebugEnabled()) { + log.debug("There was no previous span in reactor context, created a new one [" + span + "]"); + } + } + else if (traceContext != null) { + // there was a previous span - we create a child one + try (CurrentTraceContext.Scope scope = currentTraceContext.maybeScope(traceContext)) { + if (log.isDebugEnabled()) { + log.debug("Found a trace context in reactor context [" + traceContext + "]"); + } + span = tracer.nextSpan(); + if (log.isDebugEnabled()) { + log.debug("Created a child span [" + span + "]"); + } + } + } + else { + if (log.isDebugEnabled()) { + log.debug("Found a span in reactor context [" + span + "]"); + } + span = tracer.nextSpan(span); + if (log.isDebugEnabled()) { + log.debug("Created a child span [" + span + "]"); + } + } + return span.name(childSpanName).start(); + } + + private static Context enhanceContext(Tracer tracer, CurrentTraceContext currentTraceContext, + reactor.util.context.Context context, String childSpanName) { + Span span = spanFromContext(tracer, currentTraceContext, context, childSpanName); + return context.put(Span.class, span).put(TraceContext.class, span.context()).put(Tracer.SpanInScope.class, + tracer.withSpan(span)); + } + } class SleuthContextOperator implements Subscription, CoreSubscriber, Scannable { diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpCurrentTraceContext.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpCurrentTraceContext.java new file mode 100644 index 000000000..394c3eefb --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpCurrentTraceContext.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.deployer; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; + +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.TraceContext; + +/** + * A noop implementation. Does nothing. + * + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +class NoOpCurrentTraceContext implements CurrentTraceContext { + + @Override + public TraceContext context() { + return null; + } + + @Override + public Scope newScope(TraceContext context) { + return () -> { + }; + } + + @Override + public Scope maybeScope(TraceContext context) { + return () -> { + }; + } + + @Override + public Callable wrap(Callable task) { + return task; + } + + @Override + public Runnable wrap(Runnable task) { + return task; + } + + @Override + public Executor wrap(Executor delegate) { + return delegate; + } + + @Override + public ExecutorService wrap(ExecutorService delegate) { + return delegate; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpSpanInScope.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpSpanInScope.java new file mode 100644 index 000000000..d2339918a --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpSpanInScope.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.deployer; + +import org.springframework.cloud.sleuth.Tracer; + +/** + * A noop implementation. Does nothing. + * + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +class NoOpSpanInScope implements Tracer.SpanInScope { + + @Override + public void close() { + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpTraceContext.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpTraceContext.java new file mode 100644 index 000000000..057b4b41f --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpTraceContext.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.deployer; + +import org.springframework.cloud.sleuth.TraceContext; + +/** + * A noop implementation. Does nothing. + * + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +class NoOpTraceContext implements TraceContext { + + @Override + public String traceId() { + return ""; + } + + @Override + public String parentId() { + return ""; + } + + @Override + public String spanId() { + return ""; + } + + @Override + public Boolean sampled() { + return false; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleSpan.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleSpan.java new file mode 100644 index 000000000..86cdfd169 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleSpan.java @@ -0,0 +1,91 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.deployer; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; + +/** + * A noop implementation. Does nothing. + * + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +class SimpleSpan implements Span { + + Map tags = new HashMap<>(); + + boolean started; + + boolean ended; + + @Override + public boolean isNoop() { + return true; + } + + @Override + public TraceContext context() { + return new NoOpTraceContext(); + } + + @Override + public Span start() { + this.started = true; + return this; + } + + @Override + public Span name(String name) { + return this; + } + + @Override + public Span event(String value) { + return this; + } + + @Override + public Span tag(String key, String value) { + this.tags.put(key, value); + return this; + } + + @Override + public Span error(Throwable throwable) { + return this; + } + + @Override + public void end() { + this.ended = true; + } + + @Override + public void abandon() { + + } + + @Override + public Span remoteServiceName(String remoteServiceName) { + return this; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java new file mode 100644 index 000000000..295d2288d --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java @@ -0,0 +1,113 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.deployer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.BDDAssertions; + +import org.springframework.cloud.sleuth.BaggageInScope; +import org.springframework.cloud.sleuth.ScopedSpan; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanCustomizer; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; + +/** + * A noop implementation. Does nothing. + * + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +class SimpleTracer implements Tracer { + + List spans = new ArrayList<>(); + + @Override + public Span nextSpan(Span parent) { + return new SimpleSpan(); + } + + SimpleSpan getOnlySpan() { + BDDAssertions.then(this.spans).hasSize(1); + SimpleSpan span = this.spans.get(0); + BDDAssertions.then(span.started).as("Span must be started").isTrue(); + BDDAssertions.then(span.ended).as("Span must be finished").isTrue(); + return span; + } + + @Override + public SpanInScope withSpan(Span span) { + return new NoOpSpanInScope(); + } + + @Override + public SpanCustomizer currentSpanCustomizer() { + return null; + } + + @Override + public Span currentSpan() { + return new SimpleSpan(); + } + + @Override + public Span nextSpan() { + final SimpleSpan span = new SimpleSpan(); + this.spans.add(span); + return span; + } + + @Override + public ScopedSpan startScopedSpan(String name) { + return null; + } + + @Override + public Span.Builder spanBuilder() { + return null; + } + + @Override + public Map getAllBaggage() { + return new HashMap<>(); + } + + @Override + public BaggageInScope getBaggage(String name) { + return null; + } + + @Override + public BaggageInScope getBaggage(TraceContext traceContext, String name) { + return null; + } + + @Override + public BaggageInScope createBaggage(String name) { + return null; + } + + @Override + public BaggageInScope createBaggage(String name, String value) { + return null; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java new file mode 100644 index 000000000..c6b50b62e --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java @@ -0,0 +1,144 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.deployer; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.cloud.deployer.spi.app.AppDeployer; +import org.springframework.cloud.deployer.spi.app.AppScaleRequest; +import org.springframework.cloud.deployer.spi.app.AppStatus; +import org.springframework.cloud.deployer.spi.core.AppDefinition; +import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; +import org.springframework.core.env.Environment; +import org.springframework.core.io.PathResource; +import org.springframework.mock.env.MockEnvironment; + +class TraceAppDeployerTests { + + SimpleTracer simpleTracer = new SimpleTracer(); + + AppDeployer delegate = BDDMockito.mock(AppDeployer.class); + + TraceAppDeployer traceAppDeployer = new TraceAppDeployer(this.delegate, beanFactory(), environment()); + + @Test + void should_trace_deploy() { + BDDMockito.given(this.delegate.statusReactive(BDDMockito.any())) + .willReturn(Mono.just(AppStatus.of("asd").build())); + + this.traceAppDeployer.deploy(deploymentRequest()); + + BDDAssertions.then(this.simpleTracer.getOnlySpan().tags).isNotEmpty(); + BDDMockito.then(this.delegate).should().deploy(BDDMockito.any()); + } + + @Test + void should_trace_undeploy() { + BDDMockito.given(this.delegate.statusReactive(BDDMockito.any())) + .willReturn(Mono.just(AppStatus.of("asd").build())); + + this.traceAppDeployer.undeploy("asd"); + + BDDAssertions.then(this.simpleTracer.getOnlySpan().tags).isNotEmpty(); + BDDMockito.then(this.delegate).should().undeploy(BDDMockito.any()); + } + + @Test + void should_trace_status() { + this.traceAppDeployer.status("asd"); + + BDDAssertions.then(this.simpleTracer.getOnlySpan().tags).isNotEmpty(); + BDDMockito.then(this.delegate).should().status(BDDMockito.any()); + } + + @Test + void should_trace_status_reactive() { + BDDMockito.given(this.delegate.statusReactive(BDDMockito.any())) + .willReturn(Mono.just(AppStatus.of("asd").build())); + + this.traceAppDeployer.statusReactive("asd").block(); + + BDDAssertions.then(this.simpleTracer.getOnlySpan().tags).isNotEmpty(); + BDDMockito.then(this.delegate).should().statusReactive(BDDMockito.any()); + } + + @Test + void should_trace_statuses_reactive() { + BDDMockito.given(this.delegate.statusesReactive(BDDMockito.any())) + .willReturn(Flux.just(AppStatus.of("asd").build())); + + this.traceAppDeployer.statusesReactive("asd").blockFirst(); + + BDDAssertions.then(this.simpleTracer.getOnlySpan().tags).isNotEmpty(); + BDDMockito.then(this.delegate).should().statusesReactive(BDDMockito.any()); + } + + @Test + void should_trace_log() { + this.traceAppDeployer.getLog("id"); + + BDDAssertions.then(this.simpleTracer.getOnlySpan().tags).isNotEmpty(); + BDDMockito.then(this.delegate).should().getLog(BDDMockito.any()); + } + + @Test + void should_trace_scale() { + this.traceAppDeployer.scale(new AppScaleRequest("asd", 2)); + + BDDAssertions.then(this.simpleTracer.getOnlySpan().tags).isNotEmpty(); + BDDMockito.then(this.delegate).should().scale(BDDMockito.any()); + } + + private AppDeploymentRequest deploymentRequest() { + return new AppDeploymentRequest(new AppDefinition("foo", new HashMap<>()), new PathResource("/"), + deploymentProps(), commandLineArgs()); + } + + private List commandLineArgs() { + return Arrays.asList("foo=bar1", "baz=bar2"); + } + + private Map deploymentProps() { + Map map = new HashMap<>(); + map.put("deployment1", "prop1"); + map.put("deployment2", "prop2"); + return map; + } + + private BeanFactory beanFactory() { + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); + beanFactory.addBean("tracer", this.simpleTracer); + beanFactory.addBean("currentTraceContext", new NoOpCurrentTraceContext()); + return beanFactory; + } + + private Environment environment() { + return new MockEnvironment(); + } + +} From 793c1b675283e7f246859d4d6586c45426e0d505 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 26 Apr 2021 15:29:18 +0200 Subject: [PATCH 09/59] Polished the docs --- docs/src/main/asciidoc/_configprops.adoc | 6 +- docs/src/main/asciidoc/integrations.adoc | 1162 +++++++++-------- ...itional-spring-configuration-metadata.json | 2 +- 3 files changed, 591 insertions(+), 579 deletions(-) diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 48122995d..e2c13e903 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -10,13 +10,16 @@ |spring.sleuth.baggage.remote-fields | | List of fields that are referenced the same in-process as it is on the wire. For example, the field "x-vcap-request-id" would be set as-is including the prefix. |spring.sleuth.baggage.tag-fields | | |spring.sleuth.circuitbreaker.enabled | `true` | Enable Spring Cloud CircuitBreaker instrumentation. +|spring.sleuth.config.server.enabled | `true` | Enable Spring Cloud Config Server instrumentation. +|spring.sleuth.deployer.enabled | `true` | Enable Spring Cloud Deployer instrumentation. +|spring.sleuth.deployer.status-poll-delay | `1000` | Default poll delay to retrieve the deployed application status. |spring.sleuth.enabled | `true` | |spring.sleuth.feign.enabled | `true` | Enable span information propagation when using Feign. |spring.sleuth.feign.processor.enabled | `true` | Enable post processor that wraps Feign Context in its tracing representations. |spring.sleuth.function.enabled | `true` | Enable instrumenting of Spring Cloud Function and Spring Cloud Function based projects (e.g. Spring Cloud Stream). |spring.sleuth.grpc.enabled | `true` | Enable span information propagation when using GRPC. |spring.sleuth.http.enabled | `true` | Enables HTTP support. -|spring.sleuth.integration.enabled | `true` | Enable Spring Integration sleuth instrumentation. +|spring.sleuth.integration.enabled | `true` | Enable Spring Integration instrumentation. |spring.sleuth.integration.patterns | `[!hystrixStreamOutput*, *, !channel*]` | An array of patterns against which channel names will be matched. @see org.springframework.integration.config.GlobalChannelInterceptor#patterns() Defaults to any channel name not matching the Hystrix Stream and functional Stream channel names. |spring.sleuth.integration.websockets.enabled | `true` | Enable tracing for WebSockets. |spring.sleuth.messaging.enabled | `false` | Should messaging be turned on. @@ -49,6 +52,7 @@ |spring.sleuth.span-filter.enabled | `false` | Will turn on the default Sleuth handler mechanism. Might ignore exporting of certain spans; |spring.sleuth.span-filter.span-name-patterns-to-skip | `^catalogWatchTaskScheduler$` | List of span names to ignore. They will not be sent to external systems. |spring.sleuth.supports-join | `true` | True means the tracing system supports sharing a span ID between a client and server. +|spring.sleuth.task.enabled | `true` | Enable Spring Cloud Task instrumentation. |spring.sleuth.trace-id128 | `false` | When true, generate 128-bit trace IDs instead of 64-bit ones. |spring.sleuth.tracer.mode | | Set which tracer implementation should be picked. |spring.sleuth.web.additional-skip-pattern | | Additional pattern for URLs that should be skipped in tracing. This will be appended to the {@link SleuthWebProperties#skipPattern}. diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index a05b772c4..9d8530d57 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -1,577 +1,585 @@ -[[sleuth-integration]] -= Spring Cloud Sleuth customization - -include::_attributes.adoc[] - -In this section, we describe how to customize various parts of Spring Cloud Sleuth. - -[[sleuth-async-integration]] -== Asynchronous Communication - -In this section, we describe how to customize asynchronous communication with Spring Cloud Sleuth. - -[[sleuth-async-annotation-integration]] -=== `@Async` Annotated methods - -This feature is available for all tracer implementations. - -In Spring Cloud Sleuth, we instrument async-related components so that the tracing information is passed between threads. -You can disable this behavior by setting the value of `spring.sleuth.async.enabled` to `false`. - -If you annotate your method with `@Async`, we automatically modify the existing Span as follows: - -* If the method is annotated with `@SpanName`, the value of the annotation is the Span's name. -* If the method is not annotated with `@SpanName`, the Span name is the annotated method name. -* The span is tagged with the method's class name and method name. - -Since we're modifying the existing span, if you want to maintain its original name (e.g. a span created by receiving an HTTP request) -you should wrap your `@Async` annotated method with a `@NewSpan` annotation or create a new span manually. - -[[sleuth-async-scheduled-integration]] -=== `@Scheduled` Annotated Methods - -This feature is available for all tracer implementations. - -In Spring Cloud Sleuth, we instrument scheduled method execution so that the tracing information is passed between threads. -You can disable this behavior by setting the value of `spring.sleuth.scheduled.enabled` to `false`. - -If you annotate your method with `@Scheduled`, we automatically create a new span with the following characteristics: - -* The span name is the annotated method name. -* The span is tagged with the method's class name and method name. - -If you want to skip span creation for some `@Scheduled` annotated classes, you can set the `spring.sleuth.scheduled.skipPattern` with a regular expression that matches the fully qualified name of the `@Scheduled` annotated class. - -[[sleuth-async-executor-service-integration]] -=== Executor, ExecutorService, and ScheduledExecutorService - -This feature is available for all tracer implementations. - -We provide `LazyTraceExecutor`, `TraceableExecutorService`, and `TraceableScheduledExecutorService`. -Those implementations create spans each time a new task is submitted, invoked, or scheduled. - -The following example shows how to pass tracing information with `TraceableExecutorService` when working with `CompletableFuture`: - -[source,java,indent=0] ----- - -include::{common_tests_path}/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceableExecutorServiceTests.java[tags=completablefuture,indent=0] ----- - -IMPORTANT: Sleuth does not work with `parallelStream()` out of the box. -If you want to have the tracing information propagated through the stream, you have to use the approach with `supplyAsync(...)`, as shown earlier. - -If there are beans that implement the `Executor` interface that you would like to exclude from span creation, you can use the `spring.sleuth.async.ignored-beans` -property where you can provide a list of bean names. - -You can disable this behavior by setting the value of `spring.sleuth.async.enabled` to `false`. - -[[sleuth-async-executor-integration]] -==== Customization of Executors - -Sometimes, you need to set up a custom instance of the `AsyncExecutor`. -The following example shows how to set up such a custom `Executor`: - -[source,java,indent=0] ----- -include::{common_tests_path}/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/MultipleAsyncRestTemplateTests.java[tags=custom_executor,indent=0] ----- - -TIP: To ensure that your configuration gets post processed, remember to add the `@Role(BeanDefinition.ROLE_INFRASTRUCTURE)` on your -`@Configuration` class - -[[sleuth-http-client-integration]] -== HTTP Client Integration - -Features from this section can be disabled by setting the `spring.sleuth.web.client.enabled` property with value equal to `false`. - -[[sleuth-http-client-rest-template-integration]] -=== Synchronous Rest Template - -This feature is available for all tracer implementations. - -We inject a `RestTemplate` interceptor to ensure that all the tracing information is passed to the requests. -Each time a call is made, a new Span is created. -It gets closed upon receiving the response. -To block the synchronous `RestTemplate` features, set `spring.sleuth.web.client.enabled` to `false`. - -IMPORTANT: You have to register `RestTemplate` as a bean so that the interceptors get injected. -If you create a `RestTemplate` instance with a `new` keyword, the instrumentation does NOT work. - -[[sleuth-http-client-async-rest-template-integration]] -=== Asynchronous Rest Template - -This feature is available for all tracer implementations. - -IMPORTANT: Starting with Sleuth `2.0.0`, we no longer register a bean of `AsyncRestTemplate` type. -It is up to you to create such a bean. -Then we instrument it. - -To block the `AsyncRestTemplate` features, set `spring.sleuth.web.async.client.enabled` to `false`. -To disable creation of the default `TraceAsyncClientHttpRequestFactoryWrapper`, set `spring.sleuth.web.async.client.factory.enabled` -to `false`. -If you do not want to create `AsyncRestClient` at all, set `spring.sleuth.web.async.client.template.enabled` to `false`. - -[[sleuth-http-client-multiple-async-rest-template-integration]] -==== Multiple Asynchronous Rest Templates - -Sometimes you need to use multiple implementations of the Asynchronous Rest Template. -In the following snippet, you can see an example of how to set up such a custom `AsyncRestTemplate`: - -[source,java,indent=0] ----- -include::{common_tests_path}/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/MultipleAsyncRestTemplateTests.java[tags=custom_async_rest_template,indent=0] ----- - -[[sleuth-http-client-webclient-integration]] -==== `WebClient` - -This feature is available for all tracer implementations. - -We inject a `ExchangeFilterFunction` implementation that creates a span and, through on-success and on-error callbacks, takes care of closing client-side spans. - -To block this feature, set `spring.sleuth.web.client.enabled` to `false`. - -IMPORTANT: You have to register `WebClient` as a bean so that the tracing instrumentation gets applied. -If you create a `WebClient` instance with a `new` keyword, the instrumentation does NOT work. - -[[sleuth-http-client-traverson-integration]] -==== Traverson - -This feature is available for all tracer implementations. - -If you use the https://docs.spring.io/spring-hateoas/docs/current/reference/html/#client.traverson[Traverson] library, you can inject a `RestTemplate` as a bean into your Traverson object. -Since `RestTemplate` is already intercepted, you get full support for tracing in your client. -The following pseudo code shows how to do that: - -[source,java,indent=0] ----- -@Autowired RestTemplate restTemplate; - -Traverson traverson = new Traverson(URI.create("https://some/address"), - MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8).setRestOperations(restTemplate); -// use Traverson ----- - -[[sleuth-http-client-apache-integration]] -==== Apache `HttpClientBuilder` and `HttpAsyncClientBuilder` - -This feature is available for Brave tracer implementation. - -We instrument the `HttpClientBuilder` and `HttpAsyncClientBuilder` so that tracing context gets injected to the sent requests. - -To block these features, set `spring.sleuth.web.client.enabled` to `false`. - -[[sleuth-http-client-netty-integration]] -==== Netty `HttpClient` - -This feature is available for all tracer implementations. - -We instrument the Netty's `HttpClient`. - -To block this feature, set `spring.sleuth.web.client.enabled` to `false`. - -IMPORTANT: You have to register `HttpClient` as a bean so that the instrumentation happens. -If you create a `HttpClient` instance with a `new` keyword, the instrumentation does NOT work. - -[[sleuth-http-client-userinfo-integration]] -==== `UserInfoRestTemplateCustomizer` - -This feature is available for all tracer implementations. - -We instrument the Spring Security's `UserInfoRestTemplateCustomizer`. - -To block this feature, set `spring.sleuth.web.client.enabled` to `false`. - -[[sleuth-http-server-integration]] -== HTTP Server Integration - -Features from this section can be disabled by setting the `spring.sleuth.web.enabled` property with value equal to `false`. - -[[sleuth-http-server-http-filter-integration]] -=== HTTP Filter - -This feature is available for all tracer implementations. - -Through the `TracingFilter`, all sampled incoming requests result in creation of a Span. -You can configure which URIs you would like to skip by setting the `spring.sleuth.web.skipPattern` property. -If you have `ManagementServerProperties` on classpath, its value of `contextPath` gets appended to the provided skip pattern. -If you want to reuse the Sleuth's default skip patterns and just append your own, pass those patterns by using the `spring.sleuth.web.additionalSkipPattern`. - -By default, all the spring boot actuator endpoints are automatically added to the skip pattern. -If you want to disable this behaviour set `spring.sleuth.web.ignore-auto-configured-skip-patterns` -to `true`. - -To change the order of tracing filter registration, please set the -`spring.sleuth.web.filter-order` property. - -To disable the filter that logs uncaught exceptions you can disable the -`spring.sleuth.web.exception-throwing-filter-enabled` property. - -[[sleuth-http-server-handler-interceptor-integration]] -=== HandlerInterceptor - -This feature is available for all tracer implementations. - -Since we want the span names to be precise, we use a `TraceHandlerInterceptor` that either wraps an existing `HandlerInterceptor` or is added directly to the list of existing `HandlerInterceptors`. -The `TraceHandlerInterceptor` adds a special request attribute to the given `HttpServletRequest`. -If the the `TracingFilter` does not see this attribute, it creates a "`fallback`" span, which is an additional span created on the server side so that the trace is presented properly in the UI. -If that happens, there is probably missing instrumentation. -In that case, please file an issue in Spring Cloud Sleuth. - -[[sleuth-http-server-async-integration]] -=== Async Servlet support - -This feature is available for all tracer implementations. - -If your controller returns a `Callable` or a `WebAsyncTask`, Spring Cloud Sleuth continues the existing span instead of creating a new one. - -[[sleuth-http-server-webflux-integration]] -=== WebFlux support - -This feature is available for all tracer implementations. - -Through `TraceWebFilter`, all sampled incoming requests result in creation of a Span. -That Span's name is `http:` + the path to which the request was sent. -For example, if the request was sent to `/this/that`, the name is `http:/this/that`. -You can configure which URIs you would like to skip by using the `spring.sleuth.web.skipPattern` property. -If you have `ManagementServerProperties` on the classpath, its value of `contextPath` gets appended to the provided skip pattern. -If you want to reuse Sleuth's default skip patterns and append your own, pass those patterns by using the `spring.sleuth.web.additionalSkipPattern`. - -In order to achieve best results in terms of performance and context propagation we suggest that you switch the `spring.sleuth.reactor.instrumentation-type` to `MANUAL`. -In order to execute code with the span in scope you can call `WebFluxSleuthOperators.withSpanInScope`. -Example: - -[source,java,indent=0] ------ -include::{project-root}/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/app/webflux/SleuthBenchmarkingSpringWebFluxApp.java[tags=simple_manual,indent=0] ------ - -To change the order of tracing filter registration, please set the -`spring.sleuth.web.filter-order` property. - -[[sleuth-messaging-integration]] -== Messaging - -Features from this section can be disabled by setting the `spring.sleuth.messaging.enabled` property with value equal to `false`. - -[[sleuth-messaging-spring-integration-integration]] -=== Spring Integration - -This feature is available for all tracer implementations. - -Spring Cloud Sleuth integrates with https://projects.spring.io/spring-integration/[Spring Integration]. -It creates spans for publish and subscribe events. -To disable Spring Integration instrumentation, set `spring.sleuth.integration.enabled` to `false`. - -You can provide the `spring.sleuth.integration.patterns` pattern to explicitly provide the names of channels that you want to include for tracing. -By default, all channels but `hystrixStreamOutput` channel are included. - -IMPORTANT: When using the `Executor` to build a Spring Integration `IntegrationFlow`, you must use the untraced version of the `Executor`. -Decorating the Spring Integration Executor Channel with `TraceableExecutorService` causes the spans to be improperly closed. - -If you want to customize the way tracing context is read from and written to message headers, it's enough for you to register beans of types: - -* `Propagator.Setter` - for writing headers to the message -* `Propagator.Getter` - for reading headers from the message - -[[sleuth-messaging-spring-integration-customization]] -==== Spring Integration Customization - -==== Customizing messaging spans - -In order to change the default span names and tags, just register a bean of type `MessageSpanCustomizer`. You can also -override the existing `DefaultMessageSpanCustomizer` to extend the existing behaviour. - -[source,java] ----- -@Component -include::{common_tests_path}/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptorTest.java[tags=message_span_customizer,indent=2] ----- - -[[sleuth-messaging-spring-cloud-function-integration]] -=== Spring Cloud Function and Spring Cloud Stream - -This feature is available for all tracer implementations. - -Spring Cloud Sleuth can instrument Spring Cloud Function. -The way to achieve it is to provide a `Function` or `Consumer` or `Supplier` that takes in a `Message` as a parameter e.g. `Function, Message>`. -If the type is not `Message` then instrumentation will not take place. -Out of the box instrumentation will not take place when dealing with Reactor based streams - e.g. `Function>, Flux>>`. - -Since Spring Cloud Stream reuses Spring Cloud Function, you'll get the instrumentation out of the box. - -You can disable this behavior by setting the value of `spring.sleuth.function.enabled` to `false`. - -In order to work with reactive Stream functions you can leverage the `MessagingSleuthOperators` utility class that allows you to manipulate the input and output messages in order to continue the tracing context and to execute custom code within the tracing context. - -[source,java,indent=0] ------ -include::{project-root}/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/app/stream/SleuthBenchmarkingStreamApplication.java[tags=simple_reactive,indent=0] ------ - -[[sleuth-messaging-spring-rabbitmq-integration]] -=== Spring RabbitMq - -This feature is available for Brave tracer implementation. - -We instrument the `RabbitTemplate` so that tracing headers get injected into the message. - -To block this feature, set `spring.sleuth.messaging.rabbit.enabled` to `false`. - -[[sleuth-messaging-spring-kafka-integration]] -=== Spring Kafka - -This feature is available for Brave tracer implementation. - -We instrument the Spring Kafka's `ProducerFactory` and `ConsumerFactory` -so that tracing headers get injected into the created Spring Kafka's -`Producer` and `Consumer`. - -To block this feature, set `spring.sleuth.messaging.kafka.enabled` to `false`. - -[[sleuth-messaging-spring-kafka-streams-integration]] -=== Spring Kafka Streams - -This feature is available for Brave tracer implementation. - -We instrument the `KafkaStreams` `KafkaClientSupplier` so that tracing headers get injected into the `Producer` and `Consumer`s. A `KafkaStreamsTracing` bean allows for further instrumentation through additional `TransformerSupplier` and -`ProcessorSupplier` methods. - -To block this feature, set `spring.sleuth.messaging.kafka.streams.enabled` to `false`. - -[[sleuth-messaging-spring-jms-integration]] -=== Spring JMS - -This feature is available for Brave tracer implementation. - -We instrument the `JmsTemplate` so that tracing headers get injected into the message. -We also support `@JmsListener` annotated methods on the consumer side. - -To block this feature, set `spring.sleuth.messaging.jms.enabled` to `false`. - -IMPORTANT: We don't support baggage propagation for JMS - -[[sleuth-openfeign-integration]] -== OpenFeign - -This feature is available for all tracer implementations. - -By default, Spring Cloud Sleuth provides integration with Feign through `TraceFeignClientAutoConfiguration`. -You can disable it entirely by setting `spring.sleuth.feign.enabled` to `false`. -If you do so, no Feign-related instrumentation take place. - -Part of Feign instrumentation is done through a `FeignBeanPostProcessor`. -You can disable it by setting `spring.sleuth.feign.processor.enabled` to `false`. -If you set it to `false`, Spring Cloud Sleuth does not instrument any of your custom Feign components. -However, all the default instrumentation is still there. - -[[sleuth-opentracing-integration]] -== OpenTracing - -This feature is available for all tracer implementations. - -Spring Cloud Sleuth is compatible with https://opentracing.io/[OpenTracing]. -If you have OpenTracing on the classpath, we automatically register the OpenTracing `Tracer` bean. -If you wish to disable this, set `spring.sleuth.opentracing.enabled` to `false` - -[[sleuth-quartz-integration]] -== Quartz - -This feature is available for all tracer implementations. - -We instrument quartz jobs by adding Job/Trigger listeners to the Quartz Scheduler. - -To turn off this feature, set the `spring.sleuth.quartz.enabled` property to `false`. - -[[sleuth-reactor-integration]] -== Reactor - -This feature is available for all tracer implementations. - -We have the following modes of instrumenting reactor based applications that can be set via `spring.sleuth.reactor.instrumentation-type` property: - -* `DECORATE_QUEUES` - With the new Reactor https://github.com/reactor/reactor-core/pull/2566[queue wrapping mechanism] (Reactor 3.4.3) we're instrumenting the way threads are switched by Reactor. This should lead to feature parity with `ON_EACH` with low performance impact. -* `DECORATE_ON_EACH` - wraps every Reactor operator in a trace representation. -Passes the tracing context in most cases. -This mode might lead to drastic performance degradation. -* `DECORATE_ON_LAST` - wraps last Reactor operator in a trace representation. -Passes the tracing context in some cases thus accessing MDC context might not work. -This mode might lead to medium performance degradation. -* `MANUAL` - wraps every Reactor in the least invasive way without passing of tracing context. -It's up to the user to do it. - -Current default is `ON_EACH` for backward compatibility reasons, however we encourage the users to migrate to the `MANUAL` instrumentation and profit from `WebFluxSleuthOperators` and `MessagingSleuthOperators`. -The performance improvement can be substantial. -Example: - -[source,java,indent=0] ------ -include::{project-root}/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/app/webflux/SleuthBenchmarkingSpringWebFluxApp.java[tags=simple_manual,indent=0] ------ - -[[sleuth-redis-integration]] -== Redis - -This feature is available for Brave tracer implementation. - -We set `tracing` property to Lettuce `ClientResources` instance to enable Brave tracing built in Lettuce . -To disable Redis support, set the `spring.sleuth.redis.enabled` property to `false`. - -[[sleuth-runnablecallable-integration]] -== Runnable and Callable - -This feature is available for all tracer implementations. - -If you wrap your logic in `Runnable` or `Callable`, you can wrap those classes in their Sleuth representative, as shown in the following example for `Runnable`: - -[source,java,indent=0] ----- -include::{brave_path}/src/test/java/org/springframework/cloud/sleuth/brave/SpringCloudSleuthDocTests.java[tags=trace_runnable,indent=0] ----- - -The following example shows how to do so for `Callable`: - -[source,java,indent=0] ----- -include::{brave_path}/src/test/java/org/springframework/cloud/sleuth/brave/SpringCloudSleuthDocTests.java[tags=trace_callable,indent=0] ----- - -That way, you ensure that a new span is created and closed for each execution. - -[[sleuth-rpc-integration]] -== RPC - -This feature is available for Brave tracer implementation. - -Sleuth automatically configures the `RpcTracing` bean which serves as a foundation for RPC instrumentation such as gRPC or Dubbo. - -If a customization of client / server sampling of the RPC traces is required, just register a bean of type `brave.sampler.SamplerFunction` and name the bean `sleuthRpcClientSampler` for client sampler and -`sleuthRpcServerSampler` for server sampler. - -For your convenience the `@RpcClientSampler` and `@RpcServerSampler` -annotations can be used to inject the proper beans or to reference the bean names via their static String `NAME` fields. - -Ex. -Here's a sampler that traces 100 "GetUserToken" server requests per second. -This doesn't start new traces for requests to the health check service. -Other requests will use the global sampling configuration. - -[source,java,indent=0] ----- -@Configuration(proxyBeanMethods = false) - class Config { -include::{autoconfig_path}/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java[tags=custom_rpc_server_sampler,indent=2] -} ----- - -For more, see https://github.com/openzipkin/brave/tree/master/instrumentation/rpc#sampling-policy - -[[sleuth-rpc-dubbo-integration]] -=== Dubbo RPC support - -Via the integration with Brave, Spring Cloud Sleuth supports https://dubbo.apache.org/[Dubbo]. -It's enough to add the `brave-instrumentation-dubbo` dependency: - -[source,xml,indent=0] ----- - - io.zipkin.brave - brave-instrumentation-dubbo - ----- - -You need to also set a `dubbo.properties` file with the following contents: - -```properties -dubbo.provider.filter=tracing -dubbo.consumer.filter=tracing -``` - -You can read more about Brave - Dubbo integration https://github.com/openzipkin/brave/tree/master/instrumentation/dubbo-rpc[here]. -An example of Spring Cloud Sleuth and Dubbo can be found https://github.com/openzipkin/sleuth-webmvc-example/compare/add-dubbo-tracing[here]. - -[[sleuth-rpc-grpc-integration]] -=== gRPC - -Spring Cloud Sleuth provides instrumentation for https://grpc.io/[gRPC] via the Brave tracer. -You can disable it entirely by setting `spring.sleuth.grpc.enabled` to `false`. - -[[sleuth-rpc-grpc-variant1-integration]] -==== Variant 1 - -[[sleuth-rpc-grpc-variant1-dependencies-integration]] -===== Dependencies - -IMPORTANT: The gRPC integration relies on two external libraries to instrument clients and servers and both of those libraries must be on the class path to enable the instrumentation. - -Maven: - -``` - - io.github.lognet - grpc-spring-boot-starter - - - io.zipkin.brave - brave-instrumentation-grpc - -``` - -Gradle: - -``` - compile("io.github.lognet:grpc-spring-boot-starter") - compile("io.zipkin.brave:brave-instrumentation-grpc") -``` - -[[sleuth-rpc-grpc-variant1-server-integration]] -===== Server Instrumentation - -Spring Cloud Sleuth leverages grpc-spring-boot-starter to register Brave's gRPC server interceptor with all services annotated with `@GRpcService`. - -[[sleuth-rpc-grpc-variant1-client-integration]] -===== Client Instrumentation - -gRPC clients leverage a `ManagedChannelBuilder` to construct a `ManagedChannel` used to communicate to the gRPC server. -The native `ManagedChannelBuilder` provides static methods as entry points for construction of `ManagedChannel` instances, however, this mechanism is outside the influence of the Spring application context. - -IMPORTANT: Spring Cloud Sleuth provides a `SpringAwareManagedChannelBuilder` that can be customized through the Spring application context and injected by gRPC clients. -*This builder must be used when creating `ManagedChannel` instances.* - -Sleuth creates a `TracingManagedChannelBuilderCustomizer` which inject Brave's client interceptor into the `SpringAwareManagedChannelBuilder`. - -[[sleuth-rpc-grpc-variant2-integration]] -==== Variant 2 - -https://github.com/yidongnan/grpc-spring-boot-starter[Grpc Spring Boot Starter] automatically detects the presence of Spring Cloud Sleuth and Brave's instrumentation for gRPC and registers the necessary client and/or server tooling. - -[[sleuth-rxjava-integration]] -== RxJava - -This feature is available for all tracer implementations. - -We registering a custom https://github.com/ReactiveX/RxJava/wiki/Plugins#rxjavaschedulershook[`RxJavaSchedulersHook`] that wraps all `Action0` instances in their Sleuth representative, which is called `TraceAction`. -The hook either starts or continues a span, depending on whether tracing was already going on before the Action was scheduled. -To disable the custom `RxJavaSchedulersHook`, set the `spring.sleuth.rxjava.schedulers.hook.enabled` to `false`. - -You can define a list of regular expressions for thread names for which you do not want spans to be created. -To do so, provide a comma-separated list of regular expressions in the `spring.sleuth.rxjava.schedulers.ignoredthreads` property. - -IMPORTANT: The suggested approach to reactive programming and Sleuth is to use the Reactor support. - -[[sleuth-circuitbreaker-integration]] -== Spring Cloud CircuitBreaker - -This feature is available for all tracer implementations. - -If you have Spring Cloud CircuitBreaker on the classpath, we will wrap the passed command `Supplier` and the fallback `Function` in its trace representations. We will also instrument the reactive implementation of the CircuitBreaker. -In order to disable this instrumentation set `spring.sleuth.circuitbreaker.enabled` to `false`. - -[[sleuth-config-server-integration]] -== Spring Cloud Config Server - -This feature is available for all tracer implementations. - -If you have Spring Cloud Config Server running on the classpath, we will wrap the `EnvironmentRepository` in a span. -In order to disable this instrumentation set `spring.sleuth.config.server.enabled` to `false`. \ No newline at end of file +[[sleuth-integration]] += Spring Cloud Sleuth customization + +include::_attributes.adoc[] + +In this section, we describe how to customize various parts of Spring Cloud Sleuth. + +[[sleuth-async-integration]] +== Asynchronous Communication + +In this section, we describe how to customize asynchronous communication with Spring Cloud Sleuth. + +[[sleuth-async-annotation-integration]] +=== `@Async` Annotated methods + +This feature is available for all tracer implementations. + +In Spring Cloud Sleuth, we instrument async-related components so that the tracing information is passed between threads. +You can disable this behavior by setting the value of `spring.sleuth.async.enabled` to `false`. + +If you annotate your method with `@Async`, we automatically modify the existing Span as follows: + +* If the method is annotated with `@SpanName`, the value of the annotation is the Span's name. +* If the method is not annotated with `@SpanName`, the Span name is the annotated method name. +* The span is tagged with the method's class name and method name. + +Since we're modifying the existing span, if you want to maintain its original name (e.g. a span created by receiving an HTTP request) +you should wrap your `@Async` annotated method with a `@NewSpan` annotation or create a new span manually. + +[[sleuth-async-scheduled-integration]] +=== `@Scheduled` Annotated Methods + +This feature is available for all tracer implementations. + +In Spring Cloud Sleuth, we instrument scheduled method execution so that the tracing information is passed between threads. +You can disable this behavior by setting the value of `spring.sleuth.scheduled.enabled` to `false`. + +If you annotate your method with `@Scheduled`, we automatically create a new span with the following characteristics: + +* The span name is the annotated method name. +* The span is tagged with the method's class name and method name. + +If you want to skip span creation for some `@Scheduled` annotated classes, you can set the `spring.sleuth.scheduled.skipPattern` with a regular expression that matches the fully qualified name of the `@Scheduled` annotated class. + +[[sleuth-async-executor-service-integration]] +=== Executor, ExecutorService, and ScheduledExecutorService + +This feature is available for all tracer implementations. + +We provide `LazyTraceExecutor`, `TraceableExecutorService`, and `TraceableScheduledExecutorService`. +Those implementations create spans each time a new task is submitted, invoked, or scheduled. + +The following example shows how to pass tracing information with `TraceableExecutorService` when working with `CompletableFuture`: + +[source,java,indent=0] +---- + +include::{common_tests_path}/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceableExecutorServiceTests.java[tags=completablefuture,indent=0] +---- + +IMPORTANT: Sleuth does not work with `parallelStream()` out of the box. +If you want to have the tracing information propagated through the stream, you have to use the approach with `supplyAsync(...)`, as shown earlier. + +If there are beans that implement the `Executor` interface that you would like to exclude from span creation, you can use the `spring.sleuth.async.ignored-beans` +property where you can provide a list of bean names. + +You can disable this behavior by setting the value of `spring.sleuth.async.enabled` to `false`. + +[[sleuth-async-executor-integration]] +==== Customization of Executors + +Sometimes, you need to set up a custom instance of the `AsyncExecutor`. +The following example shows how to set up such a custom `Executor`: + +[source,java,indent=0] +---- +include::{common_tests_path}/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/MultipleAsyncRestTemplateTests.java[tags=custom_executor,indent=0] +---- + +TIP: To ensure that your configuration gets post processed, remember to add the `@Role(BeanDefinition.ROLE_INFRASTRUCTURE)` on your +`@Configuration` class + +[[sleuth-http-client-integration]] +== HTTP Client Integration + +Features from this section can be disabled by setting the `spring.sleuth.web.client.enabled` property with value equal to `false`. + +[[sleuth-http-client-rest-template-integration]] +=== Synchronous Rest Template + +This feature is available for all tracer implementations. + +We inject a `RestTemplate` interceptor to ensure that all the tracing information is passed to the requests. +Each time a call is made, a new Span is created. +It gets closed upon receiving the response. +To block the synchronous `RestTemplate` features, set `spring.sleuth.web.client.enabled` to `false`. + +IMPORTANT: You have to register `RestTemplate` as a bean so that the interceptors get injected. +If you create a `RestTemplate` instance with a `new` keyword, the instrumentation does NOT work. + +[[sleuth-http-client-async-rest-template-integration]] +=== Asynchronous Rest Template + +This feature is available for all tracer implementations. + +IMPORTANT: Starting with Sleuth `2.0.0`, we no longer register a bean of `AsyncRestTemplate` type. +It is up to you to create such a bean. +Then we instrument it. + +To block the `AsyncRestTemplate` features, set `spring.sleuth.web.async.client.enabled` to `false`. +To disable creation of the default `TraceAsyncClientHttpRequestFactoryWrapper`, set `spring.sleuth.web.async.client.factory.enabled` +to `false`. +If you do not want to create `AsyncRestClient` at all, set `spring.sleuth.web.async.client.template.enabled` to `false`. + +[[sleuth-http-client-multiple-async-rest-template-integration]] +==== Multiple Asynchronous Rest Templates + +Sometimes you need to use multiple implementations of the Asynchronous Rest Template. +In the following snippet, you can see an example of how to set up such a custom `AsyncRestTemplate`: + +[source,java,indent=0] +---- +include::{common_tests_path}/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/MultipleAsyncRestTemplateTests.java[tags=custom_async_rest_template,indent=0] +---- + +[[sleuth-http-client-webclient-integration]] +==== `WebClient` + +This feature is available for all tracer implementations. + +We inject a `ExchangeFilterFunction` implementation that creates a span and, through on-success and on-error callbacks, takes care of closing client-side spans. + +To block this feature, set `spring.sleuth.web.client.enabled` to `false`. + +IMPORTANT: You have to register `WebClient` as a bean so that the tracing instrumentation gets applied. +If you create a `WebClient` instance with a `new` keyword, the instrumentation does NOT work. + +[[sleuth-http-client-traverson-integration]] +==== Traverson + +This feature is available for all tracer implementations. + +If you use the https://docs.spring.io/spring-hateoas/docs/current/reference/html/#client.traverson[Traverson] library, you can inject a `RestTemplate` as a bean into your Traverson object. +Since `RestTemplate` is already intercepted, you get full support for tracing in your client. +The following pseudo code shows how to do that: + +[source,java,indent=0] +---- +@Autowired RestTemplate restTemplate; + +Traverson traverson = new Traverson(URI.create("https://some/address"), + MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8).setRestOperations(restTemplate); +// use Traverson +---- + +[[sleuth-http-client-apache-integration]] +==== Apache `HttpClientBuilder` and `HttpAsyncClientBuilder` + +This feature is available for Brave tracer implementation. + +We instrument the `HttpClientBuilder` and `HttpAsyncClientBuilder` so that tracing context gets injected to the sent requests. + +To block these features, set `spring.sleuth.web.client.enabled` to `false`. + +[[sleuth-http-client-netty-integration]] +==== Netty `HttpClient` + +This feature is available for all tracer implementations. + +We instrument the Netty's `HttpClient`. + +To block this feature, set `spring.sleuth.web.client.enabled` to `false`. + +IMPORTANT: You have to register `HttpClient` as a bean so that the instrumentation happens. +If you create a `HttpClient` instance with a `new` keyword, the instrumentation does NOT work. + +[[sleuth-http-client-userinfo-integration]] +==== `UserInfoRestTemplateCustomizer` + +This feature is available for all tracer implementations. + +We instrument the Spring Security's `UserInfoRestTemplateCustomizer`. + +To block this feature, set `spring.sleuth.web.client.enabled` to `false`. + +[[sleuth-http-server-integration]] +== HTTP Server Integration + +Features from this section can be disabled by setting the `spring.sleuth.web.enabled` property with value equal to `false`. + +[[sleuth-http-server-http-filter-integration]] +=== HTTP Filter + +This feature is available for all tracer implementations. + +Through the `TracingFilter`, all sampled incoming requests result in creation of a Span. +You can configure which URIs you would like to skip by setting the `spring.sleuth.web.skipPattern` property. +If you have `ManagementServerProperties` on classpath, its value of `contextPath` gets appended to the provided skip pattern. +If you want to reuse the Sleuth's default skip patterns and just append your own, pass those patterns by using the `spring.sleuth.web.additionalSkipPattern`. + +By default, all the spring boot actuator endpoints are automatically added to the skip pattern. +If you want to disable this behaviour set `spring.sleuth.web.ignore-auto-configured-skip-patterns` +to `true`. + +To change the order of tracing filter registration, please set the +`spring.sleuth.web.filter-order` property. + +To disable the filter that logs uncaught exceptions you can disable the +`spring.sleuth.web.exception-throwing-filter-enabled` property. + +[[sleuth-http-server-handler-interceptor-integration]] +=== HandlerInterceptor + +This feature is available for all tracer implementations. + +Since we want the span names to be precise, we use a `TraceHandlerInterceptor` that either wraps an existing `HandlerInterceptor` or is added directly to the list of existing `HandlerInterceptors`. +The `TraceHandlerInterceptor` adds a special request attribute to the given `HttpServletRequest`. +If the the `TracingFilter` does not see this attribute, it creates a "`fallback`" span, which is an additional span created on the server side so that the trace is presented properly in the UI. +If that happens, there is probably missing instrumentation. +In that case, please file an issue in Spring Cloud Sleuth. + +[[sleuth-http-server-async-integration]] +=== Async Servlet support + +This feature is available for all tracer implementations. + +If your controller returns a `Callable` or a `WebAsyncTask`, Spring Cloud Sleuth continues the existing span instead of creating a new one. + +[[sleuth-http-server-webflux-integration]] +=== WebFlux support + +This feature is available for all tracer implementations. + +Through `TraceWebFilter`, all sampled incoming requests result in creation of a Span. +That Span's name is `http:` + the path to which the request was sent. +For example, if the request was sent to `/this/that`, the name is `http:/this/that`. +You can configure which URIs you would like to skip by using the `spring.sleuth.web.skipPattern` property. +If you have `ManagementServerProperties` on the classpath, its value of `contextPath` gets appended to the provided skip pattern. +If you want to reuse Sleuth's default skip patterns and append your own, pass those patterns by using the `spring.sleuth.web.additionalSkipPattern`. + +In order to achieve best results in terms of performance and context propagation we suggest that you switch the `spring.sleuth.reactor.instrumentation-type` to `MANUAL`. +In order to execute code with the span in scope you can call `WebFluxSleuthOperators.withSpanInScope`. +Example: + +[source,java,indent=0] +----- +include::{project-root}/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/app/webflux/SleuthBenchmarkingSpringWebFluxApp.java[tags=simple_manual,indent=0] +----- + +To change the order of tracing filter registration, please set the +`spring.sleuth.web.filter-order` property. + +[[sleuth-messaging-integration]] +== Messaging + +Features from this section can be disabled by setting the `spring.sleuth.messaging.enabled` property with value equal to `false`. + +[[sleuth-messaging-spring-integration-integration]] +=== Spring Integration + +This feature is available for all tracer implementations. + +Spring Cloud Sleuth integrates with https://projects.spring.io/spring-integration/[Spring Integration]. +It creates spans for publish and subscribe events. +To disable Spring Integration instrumentation, set `spring.sleuth.integration.enabled` to `false`. + +You can provide the `spring.sleuth.integration.patterns` pattern to explicitly provide the names of channels that you want to include for tracing. +By default, all channels but `hystrixStreamOutput` channel are included. + +IMPORTANT: When using the `Executor` to build a Spring Integration `IntegrationFlow`, you must use the untraced version of the `Executor`. +Decorating the Spring Integration Executor Channel with `TraceableExecutorService` causes the spans to be improperly closed. + +If you want to customize the way tracing context is read from and written to message headers, it's enough for you to register beans of types: + +* `Propagator.Setter` - for writing headers to the message +* `Propagator.Getter` - for reading headers from the message + +[[sleuth-messaging-spring-integration-customization]] +==== Spring Integration Customization + +==== Customizing messaging spans + +In order to change the default span names and tags, just register a bean of type `MessageSpanCustomizer`. You can also +override the existing `DefaultMessageSpanCustomizer` to extend the existing behaviour. + +[source,java] +---- +@Component +include::{common_tests_path}/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptorTest.java[tags=message_span_customizer,indent=2] +---- + +[[sleuth-messaging-spring-cloud-function-integration]] +=== Spring Cloud Function and Spring Cloud Stream + +This feature is available for all tracer implementations. + +Spring Cloud Sleuth can instrument Spring Cloud Function. +The way to achieve it is to provide a `Function` or `Consumer` or `Supplier` that takes in a `Message` as a parameter e.g. `Function, Message>`. +If the type is not `Message` then instrumentation will not take place. +Out of the box instrumentation will not take place when dealing with Reactor based streams - e.g. `Function>, Flux>>`. + +Since Spring Cloud Stream reuses Spring Cloud Function, you'll get the instrumentation out of the box. + +You can disable this behavior by setting the value of `spring.sleuth.function.enabled` to `false`. + +In order to work with reactive Stream functions you can leverage the `MessagingSleuthOperators` utility class that allows you to manipulate the input and output messages in order to continue the tracing context and to execute custom code within the tracing context. + +[source,java,indent=0] +----- +include::{project-root}/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/app/stream/SleuthBenchmarkingStreamApplication.java[tags=simple_reactive,indent=0] +----- + +[[sleuth-messaging-spring-rabbitmq-integration]] +=== Spring RabbitMq + +This feature is available for Brave tracer implementation. + +We instrument the `RabbitTemplate` so that tracing headers get injected into the message. + +To block this feature, set `spring.sleuth.messaging.rabbit.enabled` to `false`. + +[[sleuth-messaging-spring-kafka-integration]] +=== Spring Kafka + +This feature is available for Brave tracer implementation. + +We instrument the Spring Kafka's `ProducerFactory` and `ConsumerFactory` +so that tracing headers get injected into the created Spring Kafka's +`Producer` and `Consumer`. + +To block this feature, set `spring.sleuth.messaging.kafka.enabled` to `false`. + +[[sleuth-messaging-spring-kafka-streams-integration]] +=== Spring Kafka Streams + +This feature is available for Brave tracer implementation. + +We instrument the `KafkaStreams` `KafkaClientSupplier` so that tracing headers get injected into the `Producer` and `Consumer`s. A `KafkaStreamsTracing` bean allows for further instrumentation through additional `TransformerSupplier` and +`ProcessorSupplier` methods. + +To block this feature, set `spring.sleuth.messaging.kafka.streams.enabled` to `false`. + +[[sleuth-messaging-spring-jms-integration]] +=== Spring JMS + +This feature is available for Brave tracer implementation. + +We instrument the `JmsTemplate` so that tracing headers get injected into the message. +We also support `@JmsListener` annotated methods on the consumer side. + +To block this feature, set `spring.sleuth.messaging.jms.enabled` to `false`. + +IMPORTANT: We don't support baggage propagation for JMS + +[[sleuth-openfeign-integration]] +== OpenFeign + +This feature is available for all tracer implementations. + +By default, Spring Cloud Sleuth provides integration with Feign through `TraceFeignClientAutoConfiguration`. +You can disable it entirely by setting `spring.sleuth.feign.enabled` to `false`. +If you do so, no Feign-related instrumentation take place. + +Part of Feign instrumentation is done through a `FeignBeanPostProcessor`. +You can disable it by setting `spring.sleuth.feign.processor.enabled` to `false`. +If you set it to `false`, Spring Cloud Sleuth does not instrument any of your custom Feign components. +However, all the default instrumentation is still there. + +[[sleuth-opentracing-integration]] +== OpenTracing + +This feature is available for all tracer implementations. + +Spring Cloud Sleuth is compatible with https://opentracing.io/[OpenTracing]. +If you have OpenTracing on the classpath, we automatically register the OpenTracing `Tracer` bean. +If you wish to disable this, set `spring.sleuth.opentracing.enabled` to `false` + +[[sleuth-quartz-integration]] +== Quartz + +This feature is available for all tracer implementations. + +We instrument quartz jobs by adding Job/Trigger listeners to the Quartz Scheduler. + +To turn off this feature, set the `spring.sleuth.quartz.enabled` property to `false`. + +[[sleuth-reactor-integration]] +== Reactor + +This feature is available for all tracer implementations. + +We have the following modes of instrumenting reactor based applications that can be set via `spring.sleuth.reactor.instrumentation-type` property: + +* `DECORATE_QUEUES` - With the new Reactor https://github.com/reactor/reactor-core/pull/2566[queue wrapping mechanism] (Reactor 3.4.3) we're instrumenting the way threads are switched by Reactor. This should lead to feature parity with `ON_EACH` with low performance impact. +* `DECORATE_ON_EACH` - wraps every Reactor operator in a trace representation. +Passes the tracing context in most cases. +This mode might lead to drastic performance degradation. +* `DECORATE_ON_LAST` - wraps last Reactor operator in a trace representation. +Passes the tracing context in some cases thus accessing MDC context might not work. +This mode might lead to medium performance degradation. +* `MANUAL` - wraps every Reactor in the least invasive way without passing of tracing context. +It's up to the user to do it. + +Current default is `ON_EACH` for backward compatibility reasons, however we encourage the users to migrate to the `MANUAL` instrumentation and profit from `WebFluxSleuthOperators` and `MessagingSleuthOperators`. +The performance improvement can be substantial. +Example: + +[source,java,indent=0] +----- +include::{project-root}/benchmarks/src/main/java/org/springframework/cloud/sleuth/benchmarks/app/webflux/SleuthBenchmarkingSpringWebFluxApp.java[tags=simple_manual,indent=0] +----- + +[[sleuth-redis-integration]] +== Redis + +This feature is available for Brave tracer implementation. + +We set `tracing` property to Lettuce `ClientResources` instance to enable Brave tracing built in Lettuce . +To disable Redis support, set the `spring.sleuth.redis.enabled` property to `false`. + +[[sleuth-runnablecallable-integration]] +== Runnable and Callable + +This feature is available for all tracer implementations. + +If you wrap your logic in `Runnable` or `Callable`, you can wrap those classes in their Sleuth representative, as shown in the following example for `Runnable`: + +[source,java,indent=0] +---- +include::{brave_path}/src/test/java/org/springframework/cloud/sleuth/brave/SpringCloudSleuthDocTests.java[tags=trace_runnable,indent=0] +---- + +The following example shows how to do so for `Callable`: + +[source,java,indent=0] +---- +include::{brave_path}/src/test/java/org/springframework/cloud/sleuth/brave/SpringCloudSleuthDocTests.java[tags=trace_callable,indent=0] +---- + +That way, you ensure that a new span is created and closed for each execution. + +[[sleuth-rpc-integration]] +== RPC + +This feature is available for Brave tracer implementation. + +Sleuth automatically configures the `RpcTracing` bean which serves as a foundation for RPC instrumentation such as gRPC or Dubbo. + +If a customization of client / server sampling of the RPC traces is required, just register a bean of type `brave.sampler.SamplerFunction` and name the bean `sleuthRpcClientSampler` for client sampler and +`sleuthRpcServerSampler` for server sampler. + +For your convenience the `@RpcClientSampler` and `@RpcServerSampler` +annotations can be used to inject the proper beans or to reference the bean names via their static String `NAME` fields. + +Ex. +Here's a sampler that traces 100 "GetUserToken" server requests per second. +This doesn't start new traces for requests to the health check service. +Other requests will use the global sampling configuration. + +[source,java,indent=0] +---- +@Configuration(proxyBeanMethods = false) + class Config { +include::{autoconfig_path}/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java[tags=custom_rpc_server_sampler,indent=2] +} +---- + +For more, see https://github.com/openzipkin/brave/tree/master/instrumentation/rpc#sampling-policy + +[[sleuth-rpc-dubbo-integration]] +=== Dubbo RPC support + +Via the integration with Brave, Spring Cloud Sleuth supports https://dubbo.apache.org/[Dubbo]. +It's enough to add the `brave-instrumentation-dubbo` dependency: + +[source,xml,indent=0] +---- + + io.zipkin.brave + brave-instrumentation-dubbo + +---- + +You need to also set a `dubbo.properties` file with the following contents: + +```properties +dubbo.provider.filter=tracing +dubbo.consumer.filter=tracing +``` + +You can read more about Brave - Dubbo integration https://github.com/openzipkin/brave/tree/master/instrumentation/dubbo-rpc[here]. +An example of Spring Cloud Sleuth and Dubbo can be found https://github.com/openzipkin/sleuth-webmvc-example/compare/add-dubbo-tracing[here]. + +[[sleuth-rpc-grpc-integration]] +=== gRPC + +Spring Cloud Sleuth provides instrumentation for https://grpc.io/[gRPC] via the Brave tracer. +You can disable it entirely by setting `spring.sleuth.grpc.enabled` to `false`. + +[[sleuth-rpc-grpc-variant1-integration]] +==== Variant 1 + +[[sleuth-rpc-grpc-variant1-dependencies-integration]] +===== Dependencies + +IMPORTANT: The gRPC integration relies on two external libraries to instrument clients and servers and both of those libraries must be on the class path to enable the instrumentation. + +Maven: + +``` + + io.github.lognet + grpc-spring-boot-starter + + + io.zipkin.brave + brave-instrumentation-grpc + +``` + +Gradle: + +``` + compile("io.github.lognet:grpc-spring-boot-starter") + compile("io.zipkin.brave:brave-instrumentation-grpc") +``` + +[[sleuth-rpc-grpc-variant1-server-integration]] +===== Server Instrumentation + +Spring Cloud Sleuth leverages grpc-spring-boot-starter to register Brave's gRPC server interceptor with all services annotated with `@GRpcService`. + +[[sleuth-rpc-grpc-variant1-client-integration]] +===== Client Instrumentation + +gRPC clients leverage a `ManagedChannelBuilder` to construct a `ManagedChannel` used to communicate to the gRPC server. +The native `ManagedChannelBuilder` provides static methods as entry points for construction of `ManagedChannel` instances, however, this mechanism is outside the influence of the Spring application context. + +IMPORTANT: Spring Cloud Sleuth provides a `SpringAwareManagedChannelBuilder` that can be customized through the Spring application context and injected by gRPC clients. +*This builder must be used when creating `ManagedChannel` instances.* + +Sleuth creates a `TracingManagedChannelBuilderCustomizer` which inject Brave's client interceptor into the `SpringAwareManagedChannelBuilder`. + +[[sleuth-rpc-grpc-variant2-integration]] +==== Variant 2 + +https://github.com/yidongnan/grpc-spring-boot-starter[Grpc Spring Boot Starter] automatically detects the presence of Spring Cloud Sleuth and Brave's instrumentation for gRPC and registers the necessary client and/or server tooling. + +[[sleuth-rxjava-integration]] +== RxJava + +This feature is available for all tracer implementations. + +We registering a custom https://github.com/ReactiveX/RxJava/wiki/Plugins#rxjavaschedulershook[`RxJavaSchedulersHook`] that wraps all `Action0` instances in their Sleuth representative, which is called `TraceAction`. +The hook either starts or continues a span, depending on whether tracing was already going on before the Action was scheduled. +To disable the custom `RxJavaSchedulersHook`, set the `spring.sleuth.rxjava.schedulers.hook.enabled` to `false`. + +You can define a list of regular expressions for thread names for which you do not want spans to be created. +To do so, provide a comma-separated list of regular expressions in the `spring.sleuth.rxjava.schedulers.ignoredthreads` property. + +IMPORTANT: The suggested approach to reactive programming and Sleuth is to use the Reactor support. + +[[sleuth-circuitbreaker-integration]] +== Spring Cloud CircuitBreaker + +This feature is available for all tracer implementations. + +If you have Spring Cloud CircuitBreaker on the classpath, we will wrap the passed command `Supplier` and the fallback `Function` in its trace representations. We will also instrument the reactive implementation of the CircuitBreaker. +In order to disable this instrumentation set `spring.sleuth.circuitbreaker.enabled` to `false`. + +[[sleuth-config-server-integration]] +== Spring Cloud Config Server + +This feature is available for all tracer implementations. + +If you have Spring Cloud Config Server running on the classpath, we will wrap the `EnvironmentRepository` in a span. +In order to disable this instrumentation set `spring.sleuth.config.server.enabled` to `false`. + +[[sleuth-deployer-integration]] +== Spring Cloud Deployer + +This feature is available for all tracer implementations. + +If you have Spring Cloud Deployer running on the classpath, we wrap the `AppDeployer` in a trace representation. We are polling the application for its status at a default interval. You can change that default by setting the `spring.sleuth.deployer.status-poll-delay` property. +In order to disable this instrumentation set `spring.sleuth.deployer.enabled` to `false`. diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index ec3dadb1b..ef1e667b5 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -147,7 +147,7 @@ "name": "spring.sleuth.deployer.status-poll-delay", "type": "java.lang.Long", "description": "Default poll delay to retrieve the deployed application status.", - "defaultValue": 1000 + "defaultValue": 500 }, { "name": "spring.sleuth.config.server.enabled", From ba8da46e2030735359f9f351e5277807540e3341 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 26 Apr 2021 22:58:28 +0200 Subject: [PATCH 10/59] Update TraceAppDeployer.java --- .../instrument/deployer/TraceAppDeployer.java | 516 +++++++++--------- 1 file changed, 258 insertions(+), 258 deletions(-) diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java index 4ea4de184..b236a6057 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java @@ -1,92 +1,92 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.deployer; - -import java.time.Duration; -import java.util.Arrays; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.cloud.deployer.spi.app.AppDeployer; -import org.springframework.cloud.deployer.spi.app.AppScaleRequest; -import org.springframework.cloud.deployer.spi.app.AppStatus; -import org.springframework.cloud.deployer.spi.app.DeploymentState; -import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; -import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; -import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; -import org.springframework.core.env.Environment; - -/** - * Trace representation of an {@link AppDeployer}. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -public class TraceAppDeployer implements AppDeployer { - - private static final Log log = LogFactory.getLog(TraceAppDeployer.class); - - private final AppDeployer delegate; - - private final BeanFactory beanFactory; - - private final Environment environment; - - private Tracer tracer; - - private CurrentTraceContext currentTraceContext; - - private Long pollDelay; - - public TraceAppDeployer(AppDeployer delegate, BeanFactory beanFactory, Environment environment) { - this.delegate = delegate; - this.beanFactory = beanFactory; - this.environment = environment; - } - - @Override - public String deploy(AppDeploymentRequest request) { - Span span = tracer().nextSpan().name("deploy"); - // TODO: Is this secure to pass? - // TODO: Does it make sense? - // if (!request.getCommandlineArguments().isEmpty()) { - // span.tag("commandlineArguments", request.getCommandlineArguments().toString()); - // } - // if (!request.getDeploymentProperties().isEmpty()) { - // span.tag("deploymentProperties", request.getDeploymentProperties().toString()); - // } - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - span.event("start"); - String id = this.delegate.deploy(request); - span.tag("id", id); - registerListener(span, id); - return id; - } - } - - private void registerListener(Span span, String id) { - PreviousAndCurrentStatus previousAndCurrentStatus = new PreviousAndCurrentStatus(span); +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.deployer; + +import java.time.Duration; +import java.util.Arrays; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.deployer.spi.app.AppDeployer; +import org.springframework.cloud.deployer.spi.app.AppScaleRequest; +import org.springframework.cloud.deployer.spi.app.AppStatus; +import org.springframework.cloud.deployer.spi.app.DeploymentState; +import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; +import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; +import org.springframework.core.env.Environment; + +/** + * Trace representation of an {@link AppDeployer}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceAppDeployer implements AppDeployer { + + private static final Log log = LogFactory.getLog(TraceAppDeployer.class); + + private final AppDeployer delegate; + + private final BeanFactory beanFactory; + + private final Environment environment; + + private Tracer tracer; + + private CurrentTraceContext currentTraceContext; + + private Long pollDelay; + + public TraceAppDeployer(AppDeployer delegate, BeanFactory beanFactory, Environment environment) { + this.delegate = delegate; + this.beanFactory = beanFactory; + this.environment = environment; + } + + @Override + public String deploy(AppDeploymentRequest request) { + Span span = tracer().nextSpan().name("deploy"); + // TODO: Is this secure to pass? + // TODO: Does it make sense? + // if (!request.getCommandlineArguments().isEmpty()) { + // span.tag("commandlineArguments", request.getCommandlineArguments().toString()); + // } + // if (!request.getDeploymentProperties().isEmpty()) { + // span.tag("deploymentProperties", request.getDeploymentProperties().toString()); + // } + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + span.event("start"); + String id = this.delegate.deploy(request); + span.tag("deployer.app.id", id); + registerListener(span, id); + return id; + } + } + + private void registerListener(Span span, String id) { + PreviousAndCurrentStatus previousAndCurrentStatus = new PreviousAndCurrentStatus(span); // @formatter:off this.delegate.statusReactive(id) .map(previousAndCurrentStatus::updateCurrent) @@ -97,172 +97,172 @@ public class TraceAppDeployer implements AppDeployer { .doOnError(span::error) // we will close the span in the reactive part .doFinally(signalType -> span.end()).subscribe(); - // @formatter:on - } - - @Override - public void undeploy(String id) { - Span span = tracer().nextSpan().name("undeploy"); - span.tag("id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - span.event("start"); - this.delegate.undeploy(id); - registerListener(span, id); - } - finally { - span.end(); - } - } - - @Override - public AppStatus status(String id) { - Span span = tracer().nextSpan().name("status"); - span.tag("id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - return this.delegate.status(id); - } - finally { - span.end(); - } - } - - @Override - public Mono statusReactive(String id) { - return ReactorSleuth.tracedMono(tracer(), currentTraceContext(), "status", - () -> this.delegate.statusReactive(id), span -> span.tag("id", id)); - } - - @Override - public Flux statusesReactive(String... ids) { - return ReactorSleuth.tracedFlux(tracer(), currentTraceContext(), "statuses", - () -> this.delegate.statusesReactive(ids), span -> span.tag("ids", Arrays.toString(ids))); - } - - @Override - public RuntimeEnvironmentInfo environmentInfo() { - return this.delegate.environmentInfo(); - } - - @Override - public String getLog(String id) { - Span span = tracer().nextSpan().name("getLog"); - span.tag("id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - return this.delegate.getLog(id); - } - finally { - span.end(); - } - } - - @Override - public void scale(AppScaleRequest appScaleRequest) { - Span span = tracer().nextSpan().name("scale"); - span.tag("deploymentId", appScaleRequest.getDeploymentId()); - span.tag("count", String.valueOf(appScaleRequest.getCount())); - // TODO: Is this secure to pass? - // TODO: Does it make sense? - // if (appScaleRequest.getProperties().isPresent() && - // !appScaleRequest.getProperties().get().isEmpty()) { - // span.tag("properties", appScaleRequest.getProperties().get().toString()); - // } - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - this.delegate.scale(appScaleRequest); - } - finally { - span.end(); - } - } - - private Tracer tracer() { - if (this.tracer == null) { - this.tracer = this.beanFactory.getBean(Tracer.class); - } - return this.tracer; - } - - private CurrentTraceContext currentTraceContext() { - if (this.currentTraceContext == null) { - this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); - } - return this.currentTraceContext; - } - - private long pollDelay() { - if (this.pollDelay == null) { - this.pollDelay = this.environment.getProperty("spring.sleuth.deployer.status-poll-delay", Long.class, 500L); - } - return this.pollDelay; - } - - private static final class PreviousAndCurrentStatus { - - private final Span span; - - private AppStatus current; - - private AppStatus previous; - - private PreviousAndCurrentStatus(Span span) { - this.span = span; - if (log.isDebugEnabled()) { - log.debug("Current span is [" + span + "]"); - } - } - - private PreviousAndCurrentStatus updateCurrent(AppStatus current) { - if (log.isTraceEnabled()) { - log.trace("State before change: current [" + this.current + "], previous [" + this.previous + "]"); - } - this.previous = this.current; - this.current = current; - if (log.isTraceEnabled()) { - log.trace("State after change: current [" + this.current + "], previous [" + this.previous + "]"); - } - if (statusChanged()) { - annotateSpan(); - } - else if (log.isTraceEnabled()) { - log.trace("State has not changed, will not annotate the span"); - } - return this; - } - - private void annotateSpan() { - String name = this.current.getState().name(); - if (log.isDebugEnabled()) { - log.debug("Will annotate its state with [" + name + "]"); - } - this.span.event(name); - } - - private boolean statusChanged() { - if (this.previous == null && this.current != null) { - if (log.isDebugEnabled()) { - log.debug("Previous is null, current is not null"); - } - return true; - } - else if (this.current == null) { - throw new IllegalStateException("Current state can't be null"); - } - DeploymentState currentState = this.current.getState(); - DeploymentState previousState = this.previous.getState(); - return currentState != previousState; - } - - private boolean isFinished() { - boolean finished = this.current.getState() == DeploymentState.deployed - || this.current.getState() == DeploymentState.undeployed - || this.current.getState() == DeploymentState.failed - || this.current.getState() == DeploymentState.error - || this.current.getState() == DeploymentState.unknown; - if (log.isTraceEnabled()) { - log.trace("Status is finished [" + finished + "]"); - } - return finished; - } - - } - -} + // @formatter:on + } + + @Override + public void undeploy(String id) { + Span span = tracer().nextSpan().name("undeploy"); + span.tag("deployer.app.id", id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + span.event("start"); + this.delegate.undeploy(id); + registerListener(span, id); + } + finally { + span.end(); + } + } + + @Override + public AppStatus status(String id) { + Span span = tracer().nextSpan().name("status"); + span.tag("deployer.app.id", id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + return this.delegate.status(id); + } + finally { + span.end(); + } + } + + @Override + public Mono statusReactive(String id) { + return ReactorSleuth.tracedMono(tracer(), currentTraceContext(), "status", + () -> this.delegate.statusReactive(id), span -> span.tag("deployer.app.id", id)); + } + + @Override + public Flux statusesReactive(String... ids) { + return ReactorSleuth.tracedFlux(tracer(), currentTraceContext(), "statuses", + () -> this.delegate.statusesReactive(ids), span -> span.tag("deployer.app.ids", Arrays.toString(ids))); + } + + @Override + public RuntimeEnvironmentInfo environmentInfo() { + return this.delegate.environmentInfo(); + } + + @Override + public String getLog(String id) { + Span span = tracer().nextSpan().name("getLog"); + span.tag("deployer.app.id", id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + return this.delegate.getLog(id); + } + finally { + span.end(); + } + } + + @Override + public void scale(AppScaleRequest appScaleRequest) { + Span span = tracer().nextSpan().name("scale"); + span.tag("deployer.scale.deploymentId", appScaleRequest.getDeploymentId()); + span.tag("deployer.scale.count", String.valueOf(appScaleRequest.getCount())); + // TODO: Is this secure to pass? + // TODO: Does it make sense? + // if (appScaleRequest.getProperties().isPresent() && + // !appScaleRequest.getProperties().get().isEmpty()) { + // span.tag("properties", appScaleRequest.getProperties().get().toString()); + // } + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + this.delegate.scale(appScaleRequest); + } + finally { + span.end(); + } + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + + private CurrentTraceContext currentTraceContext() { + if (this.currentTraceContext == null) { + this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); + } + return this.currentTraceContext; + } + + private long pollDelay() { + if (this.pollDelay == null) { + this.pollDelay = this.environment.getProperty("spring.sleuth.deployer.status-poll-delay", Long.class, 500L); + } + return this.pollDelay; + } + + private static final class PreviousAndCurrentStatus { + + private final Span span; + + private AppStatus current; + + private AppStatus previous; + + private PreviousAndCurrentStatus(Span span) { + this.span = span; + if (log.isDebugEnabled()) { + log.debug("Current span is [" + span + "]"); + } + } + + private PreviousAndCurrentStatus updateCurrent(AppStatus current) { + if (log.isTraceEnabled()) { + log.trace("State before change: current [" + this.current + "], previous [" + this.previous + "]"); + } + this.previous = this.current; + this.current = current; + if (log.isTraceEnabled()) { + log.trace("State after change: current [" + this.current + "], previous [" + this.previous + "]"); + } + if (statusChanged()) { + annotateSpan(); + } + else if (log.isTraceEnabled()) { + log.trace("State has not changed, will not annotate the span"); + } + return this; + } + + private void annotateSpan() { + String name = this.current.getState().name(); + if (log.isDebugEnabled()) { + log.debug("Will annotate its state with [" + name + "]"); + } + this.span.event(name); + } + + private boolean statusChanged() { + if (this.previous == null && this.current != null) { + if (log.isDebugEnabled()) { + log.debug("Previous is null, current is not null"); + } + return true; + } + else if (this.current == null) { + throw new IllegalStateException("Current state can't be null"); + } + DeploymentState currentState = this.current.getState(); + DeploymentState previousState = this.previous.getState(); + return currentState != previousState; + } + + private boolean isFinished() { + boolean finished = this.current.getState() == DeploymentState.deployed + || this.current.getState() == DeploymentState.undeployed + || this.current.getState() == DeploymentState.failed + || this.current.getState() == DeploymentState.error + || this.current.getState() == DeploymentState.unknown; + if (log.isTraceEnabled()) { + log.trace("Status is finished [" + finished + "]"); + } + return finished; + } + + } + +} From 216d681b04a2fc228ca5628e4f5c4593d9b33171 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 29 Apr 2021 13:32:58 +0200 Subject: [PATCH 11/59] Adds RSocket instrumentation coauthor @olegdokuka fixes gh-1677 --- docs/src/main/asciidoc/integrations.adoc | 9 + .../cloud/sleuth/SpanAndScope.java | 5 + .../cloud/sleuth/TraceContext.java | 43 ++ .../springframework/cloud/sleuth/Tracer.java | 6 + .../cloud/sleuth/WithThreadLocalSpan.java | 91 ++++ spring-cloud-sleuth-autoconfigure/pom.xml | 5 + .../messaging/SleuthMessagingProperties.java | 34 ++ ...TraceSpringMessagingAutoConfiguration.java | 11 + .../rsocket/SleuthRSocketProperties.java | 43 ++ .../TraceRSocketAutoConfiguration.java | 89 ++++ .../main/resources/META-INF/spring.factories | 1 + .../autoconfig/NoOpTraceContextBuilder.java | 54 ++ .../cloud/sleuth/autoconfig/NoOpTracer.java | 5 + .../bridge/BraveTraceContextBuilder.java | 71 +++ .../sleuth/brave/bridge/BraveTracer.java | 5 + .../sleuth/brave/bridge/W3CPropagation.java | 174 +------ .../bridge/BraveTraceContextBuilderTests.java | 54 ++ .../brave/bridge/W3CPropagationTest.java | 8 +- spring-cloud-sleuth-instrumentation/pom.xml | 10 + .../instrument/async/TraceAsyncAspect.java | 1 + .../messaging/TraceMessageHandler.java | 6 - .../messaging/TraceMessagingAspect.java | 100 ++++ .../messaging/TracingChannelInterceptor.java | 49 +- .../instrument/reactor/ReactorSleuth.java | 75 ++- .../instrument/rsocket/ByteBufGetter.java | 38 ++ .../instrument/rsocket/ByteBufSetter.java | 35 ++ .../rsocket/CompositeMetadataUtils.java | 42 ++ .../instrument/rsocket/PayloadUtils.java | 68 +++ .../TracingRSocketConnectorConfigurer.java | 49 ++ .../TracingRSocketServerCustomizer.java | 49 ++ .../rsocket/TracingRequesterRSocketProxy.java | 168 +++++++ .../rsocket/TracingResponderRSocketProxy.java | 170 +++++++ .../cloud/sleuth/internal/EncodingUtils.java | 322 ++++++++++++ .../instrument/deployer/SimpleTracer.java | 5 + .../sleuth/internal/EncodingUtilsTests.java | 43 ++ tests/brave/pom.xml | 1 + .../pom.xml | 100 ++++ .../instrument/rsocket/TraceRSocketTests.java | 53 ++ .../src/test/resources/logback.xml | 32 ++ tests/common/pom.xml | 465 +++++++++--------- .../config/ConfigServerIntegrationTests.java | 2 +- .../instrument/rsocket/TraceRSocketTests.java | 363 ++++++++++++++ 42 files changed, 2494 insertions(+), 460 deletions(-) create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/WithThreadLocalSpan.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/SleuthRSocketProperties.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTraceContextBuilder.java create mode 100644 spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilder.java create mode 100644 spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilderTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessagingAspect.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufGetter.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufSetter.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/CompositeMetadataUtils.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/PayloadUtils.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketConnectorConfigurer.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketServerCustomizer.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRequesterRSocketProxy.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/EncodingUtils.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/internal/EncodingUtilsTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/pom.xml create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/rsocket/TraceRSocketTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/resources/logback.xml create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 9d8530d57..322c2ff23 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -583,3 +583,12 @@ This feature is available for all tracer implementations. If you have Spring Cloud Deployer running on the classpath, we wrap the `AppDeployer` in a trace representation. We are polling the application for its status at a default interval. You can change that default by setting the `spring.sleuth.deployer.status-poll-delay` property. In order to disable this instrumentation set `spring.sleuth.deployer.enabled` to `false`. + + +[[sleuth-deployer-integration]] +== Spring RSocket + +This feature is available for all tracer implementations. + +If you have Spring RSocket running on the classpath, we wrap the inbound and outbound communication to propagate the tracing context via the metadata. +In order to disable this instrumentation set `spring.sleuth.rsocket.enabled` to `false`. diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java index e18e028d7..807e871c6 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java @@ -41,4 +41,9 @@ public class SpanAndScope { return this.scope; } + @Override + public String toString() { + return "SpanAndScope{" + "span=" + this.span + '}'; + } + } diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/TraceContext.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/TraceContext.java index d9e564bbd..15cb7231e 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/TraceContext.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/TraceContext.java @@ -51,4 +51,47 @@ public interface TraceContext { */ Boolean sampled(); + /** + * Builder for {@link TraceContext}. + * + * @since 3.1.0 + */ + interface Builder { + + /** + * Sets trace id on the trace context. + * @param traceId trace id + * @return this + */ + TraceContext.Builder traceId(String traceId); + + /** + * Sets parent id on the trace context. + * @param parentId parent trace id + * @return this + */ + TraceContext.Builder parentId(String parentId); + + /** + * Sets span id on the trace context. + * @param spanId span id + * @return this + */ + TraceContext.Builder spanId(String spanId); + + /** + * Sets sampled on the trace context. + * @param sampled if span is sampled + * @return this + */ + TraceContext.Builder sampled(Boolean sampled); + + /** + * Builds the trace context. + * @return trace context + */ + TraceContext build(); + + } + } diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Tracer.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Tracer.java index bf05dea69..b5a06b5b9 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Tracer.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Tracer.java @@ -134,6 +134,12 @@ public interface Tracer extends BaggageManager { */ Span.Builder spanBuilder(); + /** + * Builder for {@link TraceContext}. + * @return a trace context builder + */ + TraceContext.Builder traceContextBuilder(); + /** * Allows to customize the current span in scope. * @return current span customizer diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/WithThreadLocalSpan.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/WithThreadLocalSpan.java new file mode 100644 index 000000000..89aec40f8 --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/WithThreadLocalSpan.java @@ -0,0 +1,91 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; + +/** + * Represents a {@link Span} stored in thread local. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface WithThreadLocalSpan { + + /** + * Logger. + */ + Log log = LogFactory.getLog(WithThreadLocalSpan.class); + + /** + * Sets the span in thread local scope. + * @param span span to put in thread local + */ + default void setSpanInScope(Span span) { + getThreadLocalSpan().set(span); + if (log.isDebugEnabled()) { + log.debug("Put span in scope " + span); + } + } + + /** + * Finishes the thread local span. + * @param error potential error to be stored in span + */ + default void finishSpan(@Nullable Throwable error) { + SpanAndScope spanAndScope = takeSpanFromThreadLocal(); + if (spanAndScope == null) { + return; + } + Span span = spanAndScope.getSpan(); + Tracer.SpanInScope scope = spanAndScope.getScope(); + if (span.isNoop()) { + if (log.isDebugEnabled()) { + log.debug("Span " + span + " is noop - will stope the scope"); + } + scope.close(); + return; + } + if (error != null) { // an error occurred, adding error to span + span.error(error); + } + if (log.isDebugEnabled()) { + log.debug("Will finish the span and its corresponding scope " + span); + } + span.end(); + scope.close(); + } + + /** + * Takes a span from thread local and restores the previous one if present. + * @return span from a thread local span + */ + default SpanAndScope takeSpanFromThreadLocal() { + SpanAndScope span = getThreadLocalSpan().get(); + if (log.isDebugEnabled()) { + log.debug("Took span [" + span + "] from thread local"); + } + getThreadLocalSpan().remove(); + return span; + } + + ThreadLocalSpan getThreadLocalSpan(); + +} diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index 994f00cd4..7f314a3b0 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -324,6 +324,11 @@ spring-boot-starter-data-mongodb true + + org.springframework.boot + spring-boot-starter-rsocket + true + diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/SleuthMessagingProperties.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/SleuthMessagingProperties.java index 4a4d88c4a..3175d87ee 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/SleuthMessagingProperties.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/SleuthMessagingProperties.java @@ -17,6 +17,7 @@ package org.springframework.cloud.sleuth.autoconfig.instrument.messaging; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.messaging.handler.annotation.MessageMapping; /** * Properties for messaging. @@ -32,6 +33,11 @@ public class SleuthMessagingProperties { */ private boolean enabled; + /** + * Aspect related properties. + */ + private Aspect aspect = new Aspect(); + /** * Rabbit related properties. */ @@ -55,6 +61,14 @@ public class SleuthMessagingProperties { this.enabled = enabled; } + public Aspect getAspect() { + return this.aspect; + } + + public void setAspect(Aspect aspect) { + this.aspect = aspect; + } + public Rabbit getRabbit() { return this.rabbit; } @@ -79,6 +93,26 @@ public class SleuthMessagingProperties { this.jms = jms; } + /** + * Aspect configuration. + */ + public static class Aspect { + + /** + * Should {@link MessageMapping} wrapping be enabled. + */ + private boolean enabled; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + /** * RabbitMQ configuration. */ diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/TraceSpringMessagingAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/TraceSpringMessagingAutoConfiguration.java index fa596a866..a143bdb75 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/TraceSpringMessagingAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/TraceSpringMessagingAutoConfiguration.java @@ -16,12 +16,16 @@ package org.springframework.cloud.sleuth.autoconfig.instrument.messaging; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.sleuth.SpanNamer; +import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.instrument.messaging.MessageHeaderPropagatorGetter; import org.springframework.cloud.sleuth.instrument.messaging.MessageHeaderPropagatorSetter; +import org.springframework.cloud.sleuth.instrument.messaging.TraceMessagingAspect; import org.springframework.cloud.sleuth.propagation.Propagator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -29,10 +33,17 @@ import org.springframework.messaging.support.MessageHeaderAccessor; @Configuration(proxyBeanMethods = false) @ConditionalOnClass(MessageHeaderAccessor.class) +@ConditionalOnBean(Tracer.class) @ConditionalOnProperty(value = "spring.sleuth.messaging.enabled", matchIfMissing = true) @EnableConfigurationProperties({ SleuthIntegrationMessagingProperties.class, SleuthMessagingProperties.class }) class TraceSpringMessagingAutoConfiguration { + @Bean + @ConditionalOnProperty(value = "spring.sleuth.messaging.aspect.enabled", matchIfMissing = true) + TraceMessagingAspect traceMessagingAspect(Tracer tracer, SpanNamer spanNamer) { + return new TraceMessagingAspect(tracer, spanNamer); + } + @Bean @ConditionalOnMissingBean Propagator.Setter traceMessagePropagationSetter() { diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/SleuthRSocketProperties.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/SleuthRSocketProperties.java new file mode 100644 index 000000000..7dd0b0776 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/SleuthRSocketProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.rsocket; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Sleuth RSocket settings. + * + * @author Oleh Dokuka + * @since 3.1.0 + */ +@ConfigurationProperties("spring.sleuth.rsocket") +public class SleuthRSocketProperties { + + /** + * When true enables instrumentation for rsocket. + */ + private boolean enabled = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java new file mode 100644 index 000000000..584f76199 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.rsocket; + +import java.util.List; + +import io.rsocket.RSocket; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.rsocket.server.RSocketServerCustomizer; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.brave.propagation.PropagationType; +import org.springframework.cloud.sleuth.instrument.rsocket.TracingRSocketConnectorConfigurer; +import org.springframework.cloud.sleuth.instrument.rsocket.TracingRSocketServerCustomizer; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.messaging.rsocket.RSocketConnectorConfigurer; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketRequester.Builder; +import org.springframework.messaging.rsocket.RSocketStrategies; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(Tracer.class) +@ConditionalOnProperty(value = "spring.sleuth.rsocket.enabled", matchIfMissing = true) +@ConditionalOnClass({ RSocket.class, RSocketStrategies.class }) +@AutoConfigureAfter(BraveAutoConfiguration.class) +@AutoConfigureBefore({ RSocketRequesterAutoConfiguration.class, RSocketServerAutoConfiguration.class }) +@EnableConfigurationProperties(SleuthRSocketProperties.class) +public class TraceRSocketAutoConfiguration { + + // We're using text instead of objects cause we can have same properties from Brave / + // OTel + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + Builder rSocketRequesterBuilder(RSocketStrategies strategies, + ObjectProvider connectorConfigurerProvider) { + // TODO: should be in spring boot + final Builder builder = RSocketRequester.builder().rsocketStrategies(strategies); + connectorConfigurerProvider.forEach(builder::rsocketConnector); + return builder; + } + + private boolean containsZipkinPropagationType(List types) { + return types.contains(PropagationType.B3); + } + + @Bean + RSocketConnectorConfigurer tracingRSocketConnectorConfigurer(Propagator propagator, Tracer tracer, + @Value("${spring.sleuth.propagation.type:B3}") List types) { + return new TracingRSocketConnectorConfigurer(propagator, tracer, containsZipkinPropagationType(types)); + } + + // We're using text instead of objects cause we can have same properties from Brave / + // OTel + @Bean + RSocketServerCustomizer tracingRSocketServerCustomizer(Propagator propagator, Tracer tracer, + @Value("${spring.sleuth.propagation.type:B3}") List types) { + return new TracingRSocketServerCustomizer(propagator, tracer, containsZipkinPropagationType(types)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index d96b177e0..91d031660 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -19,6 +19,7 @@ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceFunctionAu org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringIntegrationAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringMessagingAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceWebSocketAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.rsocket.TraceRSocketAutoConfiguration, \ org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.brave.instrument.web.client.BraveWebClientAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.brave.instrument.rpc.BraveRpcAutoConfiguration,\ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTraceContextBuilder.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTraceContextBuilder.java new file mode 100644 index 000000000..c0b17da52 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTraceContextBuilder.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig; + +import org.springframework.cloud.sleuth.TraceContext; + +/** + * A noop implementation. Does nothing. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +class NoOpTraceContextBuilder implements TraceContext.Builder { + + @Override + public TraceContext.Builder traceId(String traceId) { + return this; + } + + @Override + public TraceContext.Builder parentId(String traceId) { + return this; + } + + @Override + public TraceContext.Builder spanId(String spanId) { + return null; + } + + @Override + public TraceContext.Builder sampled(Boolean sampled) { + return this; + } + + @Override + public TraceContext build() { + return new NoOpTraceContext(); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTracer.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTracer.java index 95a8f281e..08d4d99ea 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTracer.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/NoOpTracer.java @@ -69,6 +69,11 @@ class NoOpTracer implements Tracer { return new NoOpSpanBuilder(); } + @Override + public TraceContext.Builder traceContextBuilder() { + return new NoOpTraceContextBuilder(); + } + @Override public Map getAllBaggage() { return new HashMap<>(); diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilder.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilder.java new file mode 100644 index 000000000..0c19ac5b8 --- /dev/null +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilder.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.bridge; + +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.internal.EncodingUtils; + +/** + * Brave implementation of a {@link TraceContext.Builder}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +class BraveTraceContextBuilder implements TraceContext.Builder { + + brave.propagation.TraceContext.Builder delegate = brave.propagation.TraceContext.newBuilder(); + + @Override + public TraceContext.Builder traceId(String traceId) { + long[] fromString = EncodingUtils.fromString(traceId); + if (fromString.length == 2) { + this.delegate.traceIdHigh(fromString[0]); + this.delegate.traceId(fromString[1]); + } + else { + this.delegate.traceId(fromString[0]); + } + return this; + } + + @Override + public TraceContext.Builder parentId(String traceId) { + long[] fromString = EncodingUtils.fromString(traceId); + this.delegate.parentId(fromString[fromString.length == 2 ? 1 : 0]); + return this; + } + + @Override + public TraceContext.Builder spanId(String spanId) { + long[] fromString = EncodingUtils.fromString(spanId); + this.delegate.spanId(fromString[fromString.length == 2 ? 1 : 0]); + return this; + } + + @Override + public TraceContext.Builder sampled(Boolean sampled) { + this.delegate.sampled(sampled); + return this; + } + + @Override + public TraceContext build() { + brave.propagation.TraceContext context = this.delegate.build(); + return BraveTraceContext.fromBrave(context); + } + +} diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTracer.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTracer.java index 5eaa63958..258a5c53b 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTracer.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTracer.java @@ -90,6 +90,11 @@ public class BraveTracer implements Tracer { return new BraveSpanBuilder(this.tracer); } + @Override + public TraceContext.Builder traceContextBuilder() { + return new BraveTraceContextBuilder(); + } + @Override public Map getAllBaggage() { return this.braveBaggageManager.getAllBaggage(); diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagation.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagation.java index 0931b52e2..4f8a87207 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagation.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagation.java @@ -37,6 +37,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.sleuth.BaggageInScope; +import org.springframework.cloud.sleuth.internal.EncodingUtils; import static java.util.Collections.singletonList; @@ -219,12 +220,12 @@ class W3CPropagation extends Propagation.Factory implements Propagation private static boolean isTraceIdValid(CharSequence traceId) { return (traceId.length() == TRACE_ID_HEX_SIZE) && !INVALID_TRACE_ID.contentEquals(traceId) - && BigendianEncoding.isValidBase16String(traceId); + && EncodingUtils.isValidBase16String(traceId); } private static boolean isSpanIdValid(String spanId) { return (spanId.length() == SPAN_ID_HEX_SIZE) && !INVALID_SPAN_ID.equals(spanId) - && BigendianEncoding.isValidBase16String(spanId); + && EncodingUtils.isValidBase16String(spanId); } private static TraceContext extractContextFromTraceParent(String traceparent) { @@ -257,10 +258,10 @@ class W3CPropagation extends Propagation.Factory implements Propagation String traceIdLow = traceId.substring(traceId.length() / 2); byte isSampled = TraceFlags.byteFromHex(traceparent, TRACE_OPTION_OFFSET); return TraceContext.newBuilder().shared(true) - .traceIdHigh(BigendianEncoding.longFromBase16String(traceIdHigh)) - .traceId(BigendianEncoding.longFromBase16String(traceIdLow)) - .spanId(BigendianEncoding.longFromBase16String(spanId)) - .sampled(isSampled == TraceFlags.IS_SAMPLED).build(); + .traceIdHigh(EncodingUtils.longFromBase16String(traceIdHigh)) + .traceId(EncodingUtils.longFromBase16String(traceIdLow)) + .spanId(EncodingUtils.longFromBase16String(spanId)).sampled(isSampled == TraceFlags.IS_SAMPLED) + .build(); } return null; } @@ -421,41 +422,6 @@ final class TemporaryBuffers { } -/** - * Taken from OpenTelemetry API. - */ -final class Utils { - - private Utils() { - - } - - /** - * Throws an {@link IllegalArgumentException} if the argument is false. This method is - * similar to {@code Preconditions.checkArgument(boolean, Object)} from Guava. - * @param isValid whether the argument check passed. - * @param errorMessage the message to use for the exception. - */ - static void checkArgument(boolean isValid, String errorMessage) { - if (!isValid) { - throw new IllegalArgumentException(errorMessage); - } - } - - /** - * Throws an {@link IllegalStateException} if the argument is false. This method is - * similar to {@code Preconditions.checkState(boolean, Object)} from Guava. - * @param isValid whether the state check passed. - * @param errorMessage the message to use for the exception. - */ - static void checkState(boolean isValid, String errorMessage) { - if (!isValid) { - throw new IllegalStateException(String.valueOf(errorMessage)); - } - } - -} - /** * Taken from OpenTelemetry API. */ @@ -469,131 +435,7 @@ final class TraceFlags { /** Extract the byte representation of the flags from a hex-representation. */ static byte byteFromHex(CharSequence src, int srcOffset) { - return BigendianEncoding.byteFromBase16String(src, srcOffset); - } - -} - -/** - * Taken from OpenTelemetry API. - */ -final class BigendianEncoding { - - private BigendianEncoding() { - } - - static final int LONG_BYTES = Long.SIZE / Byte.SIZE; - - static final int BYTE_BASE16 = 2; - - static final int LONG_BASE16 = BYTE_BASE16 * LONG_BYTES; - - private static final String ALPHABET = "0123456789abcdef"; - - private static final int ASCII_CHARACTERS = 128; - - private static final char[] ENCODING = buildEncodingArray(); - - private static final byte[] DECODING = buildDecodingArray(); - - private static char[] buildEncodingArray() { - char[] encoding = new char[512]; - for (int i = 0; i < 256; ++i) { - encoding[i] = ALPHABET.charAt(i >>> 4); - encoding[i | 0x100] = ALPHABET.charAt(i & 0xF); - } - return encoding; - } - - private static byte[] buildDecodingArray() { - byte[] decoding = new byte[ASCII_CHARACTERS]; - Arrays.fill(decoding, (byte) -1); - for (int i = 0; i < ALPHABET.length(); i++) { - char c = ALPHABET.charAt(i); - decoding[c] = (byte) i; - } - return decoding; - } - - /** - * Returns the {@code long} value whose base16 representation is stored in the first - * 16 chars of {@code chars} starting from the {@code offset}. - * @param chars the base16 representation of the {@code long}. - */ - static long longFromBase16String(CharSequence chars) { - return longFromBase16String(chars, 0); - } - - /** - * Returns the {@code long} value whose base16 representation is stored in the first - * 16 chars of {@code chars} starting from the {@code offset}. - * @param chars the base16 representation of the {@code long}. - */ - static long longFromBase16String(CharSequence chars, int offset) { - Utils.checkArgument(chars.length() >= offset + LONG_BASE16, "chars too small"); - return (decodeByte(chars.charAt(offset), chars.charAt(offset + 1)) & 0xFFL) << 56 - | (decodeByte(chars.charAt(offset + 2), chars.charAt(offset + 3)) & 0xFFL) << 48 - | (decodeByte(chars.charAt(offset + 4), chars.charAt(offset + 5)) & 0xFFL) << 40 - | (decodeByte(chars.charAt(offset + 6), chars.charAt(offset + 7)) & 0xFFL) << 32 - | (decodeByte(chars.charAt(offset + 8), chars.charAt(offset + 9)) & 0xFFL) << 24 - | (decodeByte(chars.charAt(offset + 10), chars.charAt(offset + 11)) & 0xFFL) << 16 - | (decodeByte(chars.charAt(offset + 12), chars.charAt(offset + 13)) & 0xFFL) << 8 - | (decodeByte(chars.charAt(offset + 14), chars.charAt(offset + 15)) & 0xFFL); - } - - /** - * Decodes the specified two character sequence, and returns the resulting - * {@code byte}. - * @param chars the character sequence to be decoded. - * @param offset the starting offset in the {@code CharSequence}. - * @return the resulting {@code byte} - * @throws IllegalArgumentException if the input is not a valid encoded string - * according to this encoding. - */ - static byte byteFromBase16String(CharSequence chars, int offset) { - Utils.checkArgument(chars.length() >= offset + 2, "chars too small"); - return decodeByte(chars.charAt(offset), chars.charAt(offset + 1)); - } - - private static byte decodeByte(char hi, char lo) { - Utils.checkArgument(lo < ASCII_CHARACTERS && DECODING[lo] != -1, "invalid character " + lo); - Utils.checkArgument(hi < ASCII_CHARACTERS && DECODING[hi] != -1, "invalid character " + hi); - int decoded = DECODING[hi] << 4 | DECODING[lo]; - return (byte) decoded; - } - - /** - * Returns the {@code long} value whose big-endian representation is stored in the - * first 8 bytes of {@code bytes} starting from the {@code offset}. - * @param bytes the byte array representation of the {@code long}. - * @param offset the starting offset in the byte array. - * @return the {@code long} value whose big-endian representation is given. - * @throws IllegalArgumentException if {@code bytes} has fewer than 8 elements. - */ - static long longFromByteArray(byte[] bytes, int offset) { - Utils.checkArgument(bytes.length >= offset + LONG_BYTES, "array too small"); - return (bytes[offset] & 0xFFL) << 56 | (bytes[offset + 1] & 0xFFL) << 48 | (bytes[offset + 2] & 0xFFL) << 40 - | (bytes[offset + 3] & 0xFFL) << 32 | (bytes[offset + 4] & 0xFFL) << 24 - | (bytes[offset + 5] & 0xFFL) << 16 | (bytes[offset + 6] & 0xFFL) << 8 | (bytes[offset + 7] & 0xFFL); - } - - static boolean isValidBase16String(CharSequence value) { - for (int i = 0; i < value.length(); i++) { - char b = value.charAt(i); - // 48..57 && 97..102 are valid - if (!isDigit(b) && !isLowercaseHexCharacter(b)) { - return false; - } - } - return true; - } - - private static boolean isLowercaseHexCharacter(char b) { - return 97 <= b && b <= 102; - } - - private static boolean isDigit(char b) { - return 48 <= b && b <= 57; + return EncodingUtils.byteFromBase16String(src, srcOffset); } } diff --git a/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilderTests.java b/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilderTests.java new file mode 100644 index 000000000..570d6e70a --- /dev/null +++ b/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/BraveTraceContextBuilderTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.bridge; + +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.sleuth.TraceContext; + +import static org.assertj.core.api.BDDAssertions.then; + +class BraveTraceContextBuilderTests { + + @Test + void should_set_trace_context_for_64_bit() { + BraveTraceContextBuilder builder = new BraveTraceContextBuilder(); + + TraceContext traceContext = builder.parentId("7c6239a5ad0a4287").spanId("caff89f7f0f229dd") + .traceId("596e1787feb11040").sampled(true).build(); + + then(traceContext.parentId()).isEqualTo("7c6239a5ad0a4287"); + then(traceContext.spanId()).isEqualTo("caff89f7f0f229dd"); + then(traceContext.traceId()).isEqualTo("596e1787feb11040"); + then(traceContext.sampled()).isTrue(); + } + + @Test + void should_set_trace_context_for_128_bit() { + BraveTraceContextBuilder builder = new BraveTraceContextBuilder(); + + TraceContext traceContext = builder.parentId("00000000000000007c6239a5ad0a4287") + .spanId("0000000000000000caff89f7f0f229dd").traceId("596e1787feb11040caff89f7f0f229dd").sampled(true) + .build(); + + then(traceContext.parentId()).isEqualTo("7c6239a5ad0a4287"); + then(traceContext.spanId()).isEqualTo("caff89f7f0f229dd"); + then(traceContext.traceId()).isEqualTo("596e1787feb11040caff89f7f0f229dd"); + then(traceContext.sampled()).isTrue(); + } + +} diff --git a/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagationTest.java b/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagationTest.java index 265e0e76b..7d56613da 100644 --- a/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagationTest.java +++ b/spring-cloud-sleuth-brave/src/test/java/org/springframework/cloud/sleuth/brave/bridge/W3CPropagationTest.java @@ -27,6 +27,8 @@ import brave.propagation.TraceContext; import brave.propagation.TraceContextOrSamplingFlags; import org.junit.jupiter.api.Test; +import org.springframework.cloud.sleuth.internal.EncodingUtils; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.springframework.cloud.sleuth.brave.bridge.W3CPropagation.TRACE_PARENT; @@ -68,9 +70,9 @@ class W3CPropagationTest { private TraceContext.Builder sampledTraceContext(String traceIdHigh, String traceId, String spanId) { return TraceContext.newBuilder().sampled(SAMPLED_TRACE_OPTIONS) - .traceIdHigh(BigendianEncoding.longFromBase16String(traceIdHigh)) - .traceId(BigendianEncoding.longFromBase16String(traceId)) - .spanId(BigendianEncoding.longFromBase16String(spanId)); + .traceIdHigh(EncodingUtils.longFromBase16String(traceIdHigh)) + .traceId(EncodingUtils.longFromBase16String(traceId)) + .spanId(EncodingUtils.longFromBase16String(spanId)); } @Test diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index 0dfdf55a4..3b2d48a01 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -42,6 +42,11 @@ spring-boot-starter-web true + + org.springframework.boot + spring-boot-starter-rsocket + true + io.micrometer micrometer-core @@ -52,6 +57,11 @@ reactor-core true + + io.rsocket + rsocket-core + true + io.projectreactor.kafka reactor-kafka diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java index 0d11b88f9..6be996c8d 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java @@ -62,6 +62,7 @@ public class TraceAsyncAspect { } span = span.name(spanName); try (Tracer.SpanInScope ws = this.tracer.withSpan(span.start())) { + // TODO: Make this less generic span.tag(CLASS_KEY, pjp.getTarget().getClass().getSimpleName()); span.tag(METHOD_KEY, pjp.getSignature().getName()); return pjp.proceed(); diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessageHandler.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessageHandler.java index 9c3a0cc91..52d02ffb3 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessageHandler.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessageHandler.java @@ -191,12 +191,6 @@ class TraceMessageHandler { } } - private void addTags(Span result, String destinationName) { - if (StringUtils.hasText(destinationName)) { - result.tag("channel", SpanNameUtil.shorten(destinationName)); - } - } - /** * Called either when message got received and processed or message got sent. * @param span - span that corresponds to the given operation diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessagingAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessagingAspect.java new file mode 100644 index 000000000..fda6d71bd --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessagingAspect.java @@ -0,0 +1,100 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.messaging; + +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanNamer; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.internal.SpanNameUtil; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.util.ReflectionUtils; + +/** + * Aspect that wraps {@link MessageMapping} annotated methods in a tracing representation. + * + * TODO: Document that for client side responders declare them as beans + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@SuppressWarnings("ArgNamesWarningsInspection") +@Aspect +public class TraceMessagingAspect { + + private static final Log log = org.apache.commons.logging.LogFactory.getLog(TraceMessagingAspect.class); + + static final String MESSAGING_CONTROLLER_CLASS_KEY = "messaging.controller.class"; + + static final String MESSAGING_CONTROLLER_METHOD_KEY = "messaging.controller.method"; + + private final Tracer tracer; + + private final SpanNamer spanNamer; + + public TraceMessagingAspect(Tracer tracer, SpanNamer spanNamer) { + this.tracer = tracer; + this.spanNamer = spanNamer; + } + + @Pointcut("@within(org.springframework.messaging.handler.annotation.MessageMapping)") + private void anyMessageMappingAnnotated() { + } // NOSONAR + + @Around("anyMessageMappingAnnotated()") + @SuppressWarnings("unchecked") + public Object addTags(ProceedingJoinPoint pjp) throws Throwable { + Object object = pjp.proceed(); + String methodName = pjp.getSignature().getName(); + String className = pjp.getTarget().getClass().getName(); + Span currentSpan = currentSpan(pjp); + currentSpan.tag(MESSAGING_CONTROLLER_CLASS_KEY, className); + currentSpan.tag(MESSAGING_CONTROLLER_METHOD_KEY, methodName); + return object; + } + + private Span currentSpan(ProceedingJoinPoint pjp) { + Span currentSpan = this.tracer.currentSpan(); + if (currentSpan == null) { + if (log.isDebugEnabled()) { + log.debug("No span found - will create a new one"); + } + currentSpan = this.tracer.nextSpan().name(name(pjp)).start(); + } + return currentSpan; + } + + private String name(ProceedingJoinPoint pjp) { + return this.spanNamer.name(getMethod(pjp, pjp.getTarget()), + SpanNameUtil.toLowerHyphen(pjp.getSignature().getName())); + } + + private Method getMethod(ProceedingJoinPoint pjp, Object object) { + MethodSignature signature = (MethodSignature) pjp.getSignature(); + Method method = signature.getMethod(); + return ReflectionUtils.findMethod(object.getClass(), method.getName(), method.getParameterTypes()); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java index ed9d3780d..9ee309081 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java @@ -26,9 +26,9 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeansException; import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.SpanAndScope; import org.springframework.cloud.sleuth.ThreadLocalSpan; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.WithThreadLocalSpan; import org.springframework.cloud.sleuth.propagation.Propagator; import org.springframework.cloud.stream.binder.BinderType; import org.springframework.cloud.stream.binder.BinderTypeRegistry; @@ -58,7 +58,7 @@ import org.springframework.util.StringUtils; * @since 3.0.0 */ public final class TracingChannelInterceptor extends ChannelInterceptorAdapter - implements ExecutorChannelInterceptor, ApplicationContextAware { + implements ExecutorChannelInterceptor, ApplicationContextAware, WithThreadLocalSpan { /** * Name of the class in Spring Cloud Stream that is a direct channel. @@ -163,13 +163,6 @@ public final class TracingChannelInterceptor extends ChannelInterceptorAdapter return outputMessage; } - private void setSpanInScope(Span span) { - this.threadLocalSpan.set(span); - if (log.isDebugEnabled()) { - log.debug("Put span in scope " + span); - } - } - private String toRemoteServiceName(MessageHeaderAccessor headers) { for (String key : headers.getMessageHeaders().keySet()) { String remoteServiceName = this.remoteServiceNameMapper.apply(key); @@ -357,41 +350,9 @@ public final class TracingChannelInterceptor extends ChannelInterceptorAdapter finishSpan(ex); } - void finishSpan(Exception error) { - SpanAndScope spanAndScope = getSpanFromThreadLocal(); - if (spanAndScope == null) { - return; - } - Span span = spanAndScope.getSpan(); - Tracer.SpanInScope scope = spanAndScope.getScope(); - if (span.isNoop()) { - if (log.isDebugEnabled()) { - log.debug("Span " + span + " is noop - will stope the scope"); - } - scope.close(); - return; - } - if (error != null) { // an error occurred, adding error to span - String message = error.getMessage(); - if (message == null) { - message = error.getClass().getSimpleName(); - } - span.tag("error", message); - } - if (log.isDebugEnabled()) { - log.debug("Will finish the and its corresponding scope " + span); - } - span.end(); - scope.close(); - } - - private SpanAndScope getSpanFromThreadLocal() { - SpanAndScope span = this.threadLocalSpan.get(); - if (log.isDebugEnabled()) { - log.debug("Took span [" + span + "] from thread local"); - } - this.threadLocalSpan.remove(); - return span; + @Override + public ThreadLocalSpan getThreadLocalSpan() { + return this.threadLocalSpan; } private MessageHeaderAccessor mutableHeaderAccessor(Message message) { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java index dabadd1a3..02d870027 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java @@ -334,15 +334,26 @@ public abstract class ReactorSleuth { public static Mono tracedMono(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, @NonNull String childSpanName, @NonNull Supplier> supplier, @NonNull Consumer spanCustomizer) { + return runMonoSupplierInScope(supplier, spanCustomizer).contextWrite( + context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); + } + + private static Mono runMonoSupplierInScope(Supplier> supplier, Consumer spanCustomizer) { return Mono.deferContextual(contextView -> { Span span = contextView.get(Span.class); spanCustomizer.accept(span); Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); - return supplier.get().doOnError(span::error).doFinally(signalType -> { - span.end(); - scope.close(); - }); - }).contextWrite(context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); + // @formatter:off + return supplier.get() + // TODO: Fix me when this is resolved in Reactor +// .doOnSubscribe(__ -> scope.close()) + .doOnError(span::error) + .doFinally(signalType -> { + span.end(); + scope.close(); + }); + // @formatter:on + }); } /** @@ -361,6 +372,20 @@ public abstract class ReactorSleuth { }); } + /** + * Wraps the given Mono in a trace representation. Puts the provided span to context. + * @param tracer - Tracer bean + * @param span - span to put in context + * @param supplier - supplier of a {@link Mono} to be wrapped in tracing + * @param - type returned by the Mono + * @return traced Mono + */ + public static Mono tracedMono(@NonNull Tracer tracer, @NonNull Span span, + @NonNull Supplier> supplier) { + return runMonoSupplierInScope(supplier, span1 -> { + }).contextWrite(context -> ReactorSleuth.putSpanInScope(tracer, context, span)); + } + /** * Wraps the given Flux in a trace representation. Retrieves the span from context, * creates a child span with the given name. @@ -375,15 +400,41 @@ public abstract class ReactorSleuth { public static Flux tracedFlux(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, @NonNull String childSpanName, @NonNull Supplier> supplier, @NonNull Consumer spanCustomizer) { + return runFluxSupplierInScope(supplier, spanCustomizer).contextWrite( + context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); + } + + /** + * Wraps the given Flux in a trace representation. Retrieves the span from context, + * creates a child span with the given name. + * @param tracer - Tracer bean + * @param span - span to put in context + * @param supplier - supplier of a {@link Flux} to be wrapped in tracing + * @param - type returned by the Flux + * @return traced Flux + */ + public static Flux tracedFlux(@NonNull Tracer tracer, @NonNull Span span, + @NonNull Supplier> supplier) { + return runFluxSupplierInScope(supplier, span1 -> { + }).contextWrite(context -> ReactorSleuth.putSpanInScope(tracer, context, span)); + } + + private static Flux runFluxSupplierInScope(Supplier> supplier, Consumer spanCustomizer) { return Flux.deferContextual(contextView -> { Span span = contextView.get(Span.class); spanCustomizer.accept(span); Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); - return supplier.get().doOnError(span::error).doFinally(signalType -> { - span.end(); - scope.close(); - }); - }).contextWrite(context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); + // @formatter:off + return supplier.get() + // TODO: Fix me when this is resolved in Reactor +// .doOnSubscribe(__ -> scope.close()) + .doOnError(span::error) + .doFinally(signalType -> { + span.end(); + scope.close(); + }); + // @formatter:on + }); } /** @@ -442,6 +493,10 @@ public abstract class ReactorSleuth { private static Context enhanceContext(Tracer tracer, CurrentTraceContext currentTraceContext, reactor.util.context.Context context, String childSpanName) { Span span = spanFromContext(tracer, currentTraceContext, context, childSpanName); + return putSpanInScope(tracer, context, span); + } + + private static Context putSpanInScope(Tracer tracer, Context context, Span span) { return context.put(Span.class, span).put(TraceContext.class, span.context()).put(Tracer.SpanInScope.class, tracer.withSpan(span)); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufGetter.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufGetter.java new file mode 100644 index 000000000..167af1865 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufGetter.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import io.netty.buffer.ByteBuf; +import io.netty.util.CharsetUtil; +import io.rsocket.metadata.CompositeMetadata; + +import org.springframework.cloud.sleuth.propagation.Propagator; + +class ByteBufGetter implements Propagator.Getter { + + @Override + public String get(ByteBuf carrier, String key) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(carrier, false); + for (CompositeMetadata.Entry entry : compositeMetadata) { + if (key.equals(entry.getMimeType())) { + return entry.getContent().toString(CharsetUtil.UTF_8); + } + } + return null; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufSetter.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufSetter.java new file mode 100644 index 000000000..e44a77de7 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/ByteBufSetter.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.metadata.CompositeMetadataCodec; + +import org.springframework.cloud.sleuth.propagation.Propagator; + +class ByteBufSetter implements Propagator.Setter { + + @Override + public void set(CompositeByteBuf carrier, String key, String value) { + final ByteBufAllocator alloc = carrier.alloc(); + CompositeMetadataCodec.encodeAndAddMetadataWithCompression(carrier, alloc, key, + ByteBufUtil.writeUtf8(alloc, value)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/CompositeMetadataUtils.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/CompositeMetadataUtils.java new file mode 100644 index 000000000..5bc960a3e --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/CompositeMetadataUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import io.netty.buffer.ByteBuf; +import io.rsocket.metadata.CompositeMetadata; + +import org.springframework.lang.Nullable; + +final class CompositeMetadataUtils { + + private CompositeMetadataUtils() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + @Nullable + static ByteBuf extract(ByteBuf metadata, String key) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(metadata, false); + for (CompositeMetadata.Entry entry : compositeMetadata) { + final String entryKey = entry.getMimeType(); + if (key.equals(entryKey)) { + return entry.getContent(); + } + } + return null; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/PayloadUtils.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/PayloadUtils.java new file mode 100644 index 000000000..17363acc4 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/PayloadUtils.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import java.util.HashSet; +import java.util.Set; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.Payload; +import io.rsocket.metadata.CompositeMetadata; +import io.rsocket.metadata.CompositeMetadata.Entry; +import io.rsocket.metadata.CompositeMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.ByteBufPayload; +import io.rsocket.util.DefaultPayload; + +final class PayloadUtils { + + private PayloadUtils() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + static Payload cleanTracingMetadata(Payload payload, Set fields) { + Set fieldsWithDefaultZipkin = new HashSet<>(fields); + fieldsWithDefaultZipkin.add(WellKnownMimeType.MESSAGE_RSOCKET_TRACING_ZIPKIN.getString()); + final CompositeMetadata entries = new CompositeMetadata(payload.metadata(), true); + final CompositeByteBuf metadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + for (Entry entry : entries) { + if (!fieldsWithDefaultZipkin.contains(entry.getMimeType())) { + CompositeMetadataCodec.encodeAndAddMetadataWithCompression(metadata, ByteBufAllocator.DEFAULT, + entry.getMimeType(), entry.getContent()); + } + } + return payload(payload, metadata); + } + + private static Payload payload(Payload payload, CompositeByteBuf metadata) { + final Payload newPayload; + try { + if (payload instanceof ByteBufPayload) { + newPayload = ByteBufPayload.create(payload.data().retain(), metadata.retain()); + } + else { + newPayload = DefaultPayload.create(payload.data().retain(), metadata.retain()); + } + } + finally { + payload.release(); + } + return newPayload; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketConnectorConfigurer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketConnectorConfigurer.java new file mode 100644 index 000000000..b172c0640 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketConnectorConfigurer.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import io.rsocket.core.RSocketConnector; +import io.rsocket.plugins.RSocketInterceptor; + +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.messaging.rsocket.RSocketConnectorConfigurer; + +public class TracingRSocketConnectorConfigurer implements RSocketConnectorConfigurer { + + private final Propagator propagator; + + private final Tracer tracer; + + private final boolean isZipkinPropagationEnabled; + + public TracingRSocketConnectorConfigurer(Propagator propagator, Tracer tracer, boolean isZipkinPropagationEnabled) { + this.propagator = propagator; + this.tracer = tracer; + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public void configure(RSocketConnector rSocketConnector) { + rSocketConnector.interceptors(ir -> ir + .forResponder((RSocketInterceptor) rSocket -> new TracingResponderRSocketProxy(rSocket, this.propagator, + new ByteBufGetter(), this.tracer, this.isZipkinPropagationEnabled)) + .forRequester((RSocketInterceptor) rSocket -> new TracingRequesterRSocketProxy(rSocket, this.propagator, + new ByteBufSetter(), this.tracer, this.isZipkinPropagationEnabled))); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketServerCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketServerCustomizer.java new file mode 100644 index 000000000..809671eec --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRSocketServerCustomizer.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import io.rsocket.core.RSocketServer; +import io.rsocket.plugins.RSocketInterceptor; + +import org.springframework.boot.rsocket.server.RSocketServerCustomizer; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.propagation.Propagator; + +public class TracingRSocketServerCustomizer implements RSocketServerCustomizer { + + private final Propagator propagator; + + private final Tracer tracer; + + private final boolean isZipkinPropagationEnabled; + + public TracingRSocketServerCustomizer(Propagator propagator, Tracer tracer, boolean isZipkinPropagationEnabled) { + this.propagator = propagator; + this.tracer = tracer; + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public void customize(RSocketServer rSocketServer) { + rSocketServer.interceptors(ir -> ir + .forResponder((RSocketInterceptor) rSocket -> new TracingResponderRSocketProxy(rSocket, propagator, + new ByteBufGetter(), this.tracer, this.isZipkinPropagationEnabled)) + .forRequester((RSocketInterceptor) rSocket -> new TracingRequesterRSocketProxy(rSocket, propagator, + new ByteBufSetter(), tracer, isZipkinPropagationEnabled))); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRequesterRSocketProxy.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRequesterRSocketProxy.java new file mode 100644 index 000000000..31d0b9a96 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRequesterRSocketProxy.java @@ -0,0 +1,168 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.function.Function; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TracingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.RSocketProxy; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.internal.EncodingUtils; +import org.springframework.cloud.sleuth.propagation.Propagator; + +/** + * Tracing representation of a {@link RSocketProxy} for the requester. + * + * @author Marcin Grzejszczak + * @author Oleh Dokuka + * @since 3.1.0 + */ +public class TracingRequesterRSocketProxy extends RSocketProxy { + + private static final Log log = LogFactory.getLog(TracingRequesterRSocketProxy.class); + + private final Propagator propagator; + + private final Propagator.Setter setter; + + private final Tracer tracer; + + private final boolean isZipkinPropagationEnabled; + + public TracingRequesterRSocketProxy(RSocket source, Propagator propagator, + Propagator.Setter setter, Tracer tracer, boolean isZipkinPropagationEnabled) { + super(source); + this.propagator = propagator; + this.setter = setter; + this.tracer = tracer; + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public Mono fireAndForget(Payload payload) { + return setSpan(super::fireAndForget, payload, FrameType.REQUEST_FNF); + } + + @Override + public Mono requestResponse(Payload payload) { + return setSpan(super::requestResponse, payload, FrameType.REQUEST_RESPONSE); + } + + Mono setSpan(Function> input, Payload payload, FrameType frameType) { + return Mono.deferContextual(contextView -> { + Span.Builder spanBuilder = spanBuilder(contextView); + ByteBuf extracted = CompositeMetadataUtils.extract(payload.sliceMetadata(), + WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); + // TODO: do sth about extracted == null, log that tracing can't be used or sth + final RoutingMetadata routingMetadata = new RoutingMetadata(extracted); + final Iterator iterator = routingMetadata.iterator(); + String route = iterator.next(); + Span span = spanBuilder.kind(Span.Kind.PRODUCER).name(frameType.name() + " " + route).start(); + span.tag("rsocket.route", route); + span.tag("rsocket.request-type", frameType.name()); + if (log.isDebugEnabled()) { + log.debug("Extracted result from context or thread local " + span); + } + final Payload newPayload = PayloadUtils.cleanTracingMetadata(payload, new HashSet<>(propagator.fields())); + TraceContext traceContext = span.context(); + if (this.isZipkinPropagationEnabled) { + injectDefaultZipkinRSocketHeaders(newPayload, traceContext); + } + this.propagator.inject(traceContext, (CompositeByteBuf) newPayload.metadata(), this.setter); + return input.apply(newPayload).doOnError(span::error).doFinally(signalType -> span.end()); + }); + } + + private void injectDefaultZipkinRSocketHeaders(Payload newPayload, TraceContext traceContext) { + TracingMetadataCodec.Flags flags = traceContext.sampled() == null ? TracingMetadataCodec.Flags.UNDECIDED + : traceContext.sampled() ? TracingMetadataCodec.Flags.SAMPLE : TracingMetadataCodec.Flags.NOT_SAMPLE; + String traceId = traceContext.traceId(); + long[] traceIds = EncodingUtils.fromString(traceId); + long[] spanId = EncodingUtils.fromString(traceContext.spanId()); + long[] parentSpanId = EncodingUtils.fromString(traceContext.parentId()); + boolean isTraceId128Bit = traceIds.length == 2; + if (isTraceId128Bit) { + TracingMetadataCodec.encode128(newPayload.metadata().alloc(), traceIds[0], traceIds[1], spanId[0], + EncodingUtils.fromString(traceContext.parentId())[0], flags); + } + else { + TracingMetadataCodec.encode64(newPayload.metadata().alloc(), traceIds[0], spanId[0], parentSpanId[0], + flags); + } + } + + private Span.Builder spanBuilder(ContextView contextView) { + Span.Builder spanBuilder = this.tracer.spanBuilder(); + if (contextView.hasKey(TraceContext.class)) { + spanBuilder = spanBuilder.setParent(contextView.get(TraceContext.class)); + } + else if (this.tracer.currentSpan() != null) { + spanBuilder = spanBuilder.setParent(this.tracer.currentSpan().context()); + } + return spanBuilder; + } + + @Override + public Flux requestStream(Payload payload) { + return Flux.deferContextual(contextView -> setSpan(super::requestStream, payload, contextView)); + } + + @Override + public Flux requestChannel(Publisher inbound) { + return Flux.from(inbound).switchOnFirst((firstSignal, flux) -> { + final Payload firstPayload = firstSignal.get(); + if (firstPayload != null) { + return setSpan(p -> super.requestChannel(flux.skip(1).startWith(p)), firstPayload, + firstSignal.getContextView()); + } + return flux; + }); + } + + Flux setSpan(Function> input, Payload payload, ContextView contextView) { + Span.Builder spanBuilder = spanBuilder(contextView); + final RoutingMetadata routingMetadata = new RoutingMetadata(CompositeMetadataUtils + .extract(payload.sliceMetadata(), WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString())); + final Iterator iterator = routingMetadata.iterator(); + Span span = spanBuilder.kind(Span.Kind.PRODUCER).name(iterator.next()).start(); + if (log.isDebugEnabled()) { + log.debug("Extracted result from context or thread local " + span); + } + final Payload newPayload = PayloadUtils.cleanTracingMetadata(payload, new HashSet<>(propagator.fields())); + this.propagator.inject(span.context(), (CompositeByteBuf) newPayload.metadata(), this.setter); + return input.apply(newPayload).doOnError(span::error).doFinally(signalType -> span.end()); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java new file mode 100644 index 000000000..b75a4aff9 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java @@ -0,0 +1,170 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import java.util.HashSet; +import java.util.Iterator; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.frame.FrameType; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TracingMetadata; +import io.rsocket.metadata.TracingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.util.RSocketProxy; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.ThreadLocalSpan; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.WithThreadLocalSpan; +import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; +import org.springframework.cloud.sleuth.internal.EncodingUtils; +import org.springframework.cloud.sleuth.propagation.Propagator; + +/** + * Tracing representation of a {@link RSocketProxy} for the responder. + * + * @author Marcin Grzejszczak + * @author Oleh Dokuka + * @since 3.1.0 + */ +public class TracingResponderRSocketProxy extends RSocketProxy implements WithThreadLocalSpan { + + private static final Log log = LogFactory.getLog(TracingResponderRSocketProxy.class); + + private final Propagator propagator; + + private final Propagator.Getter getter; + + private final Tracer tracer; + + private final ThreadLocalSpan threadLocalSpan; + + private final boolean isZipkinPropagationEnabled; + + public TracingResponderRSocketProxy(RSocket source, Propagator propagator, Propagator.Getter getter, + Tracer tracer, boolean isZipkinPropagationEnabled) { + super(source); + this.propagator = propagator; + this.getter = getter; + this.tracer = tracer; + this.threadLocalSpan = new ThreadLocalSpan(tracer); + this.isZipkinPropagationEnabled = isZipkinPropagationEnabled; + } + + @Override + public Mono fireAndForget(Payload payload) { + // called on Netty EventLoop + // there can't be trace context in thread local here + Span handle = consumerSpanBuilder(payload.sliceMetadata(), FrameType.REQUEST_FNF); + if (log.isDebugEnabled()) { + log.debug("Created consumer span " + handle); + } + final Payload newPayload = PayloadUtils.cleanTracingMetadata(payload, new HashSet<>(propagator.fields())); + return ReactorSleuth.tracedMono(this.tracer, handle, () -> super.fireAndForget(newPayload)); + } + + @Override + public Mono requestResponse(Payload payload) { + Span handle = consumerSpanBuilder(payload.sliceMetadata(), FrameType.REQUEST_RESPONSE); + if (log.isDebugEnabled()) { + log.debug("Created consumer span " + handle); + } + final Payload newPayload = PayloadUtils.cleanTracingMetadata(payload, new HashSet<>(propagator.fields())); + return ReactorSleuth.tracedMono(this.tracer, handle, () -> super.requestResponse(newPayload)); + } + + @Override + public Flux requestStream(Payload payload) { + Span handle = consumerSpanBuilder(payload.sliceMetadata(), FrameType.REQUEST_STREAM); + if (log.isDebugEnabled()) { + log.debug("Created consumer span " + handle); + } + final Payload newPayload = PayloadUtils.cleanTracingMetadata(payload, new HashSet<>(propagator.fields())); + return ReactorSleuth.tracedFlux(this.tracer, handle, () -> super.requestStream(newPayload)); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads).switchOnFirst((firstSignal, flux) -> { + final Payload firstPayload = firstSignal.get(); + if (firstPayload != null) { + Span handle = consumerSpanBuilder(firstPayload.sliceMetadata(), FrameType.REQUEST_CHANNEL); + if (handle == null) { + return super.requestChannel(flux); + } + if (log.isDebugEnabled()) { + log.debug("Created consumer span " + handle); + } + final Payload newPayload = PayloadUtils.cleanTracingMetadata(firstPayload, + new HashSet<>(propagator.fields())); + return ReactorSleuth.tracedFlux(this.tracer, handle, + () -> super.requestChannel(flux.skip(1).startWith(newPayload))); + } + return flux; + }); + } + + private Span consumerSpanBuilder(ByteBuf headers, FrameType requestType) { + Span.Builder consumerSpanBuilder = consumerSpanBuilder(headers); + if (log.isDebugEnabled()) { + log.debug("Extracted result from headers " + consumerSpanBuilder); + } + final ByteBuf extract = CompositeMetadataUtils.extract(headers, + WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()); + String name = "handle"; + if (extract != null) { + final RoutingMetadata routingMetadata = new RoutingMetadata(extract); + final Iterator iterator = routingMetadata.iterator(); + name = requestType.name() + " " + iterator.next(); + } + return consumerSpanBuilder.kind(Span.Kind.CONSUMER).name(name).start(); + } + + private Span.Builder consumerSpanBuilder(ByteBuf headers) { + if (this.isZipkinPropagationEnabled) { + ByteBuf extract = CompositeMetadataUtils.extract(headers, + WellKnownMimeType.MESSAGE_RSOCKET_TRACING_ZIPKIN.getString()); + if (extract != null) { + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(extract); + Span.Builder builder = this.tracer.spanBuilder(); + TraceContext.Builder parentBuilder = this.tracer.traceContextBuilder() + .sampled(tracingMetadata.isSampled()).traceId(EncodingUtils.fromLong(tracingMetadata.traceId())) + .parentId(EncodingUtils.fromLong(tracingMetadata.parentId())); + return builder.setParent(parentBuilder.build()); + } + else { + return this.propagator.extract(headers, this.getter); + } + } + return this.propagator.extract(headers, this.getter); + } + + @Override + public ThreadLocalSpan getThreadLocalSpan() { + return this.threadLocalSpan; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/EncodingUtils.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/EncodingUtils.java new file mode 100644 index 000000000..f1fcf6f82 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/EncodingUtils.java @@ -0,0 +1,322 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.internal; + +import java.util.Arrays; + +import org.springframework.util.Assert; + +/** + * Adopted from OpenTelemetry API. + * + * @since 3.1.0 + */ +public final class EncodingUtils { + + private static final ThreadLocal charBuffer = new ThreadLocal(); + + private EncodingUtils() { + } + + static final int LONG_BYTES = Long.SIZE / Byte.SIZE; + + static final int BYTE_BASE16 = 2; + + static final int LONG_BASE16 = BYTE_BASE16 * LONG_BYTES; + + private static final String ALPHABET = "0123456789abcdef"; + + private static final int ASCII_CHARACTERS = 128; + + private static final char[] ENCODING = buildEncodingArray(); + + private static final byte[] DECODING = buildDecodingArray(); + + private static char[] buildEncodingArray() { + char[] encoding = new char[512]; + for (int i = 0; i < 256; ++i) { + encoding[i] = ALPHABET.charAt(i >>> 4); + encoding[i | 0x100] = ALPHABET.charAt(i & 0xF); + } + return encoding; + } + + private static byte[] buildDecodingArray() { + byte[] decoding = new byte[ASCII_CHARACTERS]; + Arrays.fill(decoding, (byte) -1); + for (int i = 0; i < ALPHABET.length(); i++) { + char c = ALPHABET.charAt(i); + decoding[c] = (byte) i; + } + return decoding; + } + + /** + * Returns the {@code long} value. + * @param chars the base8 or base16 representation of the {@code long}. + * @return long array from string. Either contains high and low or just low trace id + */ + public static long[] fromString(CharSequence chars) { + if (chars == null || chars.length() == 0) { + return new long[] { 0 }; + } + if (chars.length() == 32) { + long high = HexCodec.lenientLowerHexToUnsignedLong(chars, 0, 16); + long low = HexCodec.lenientLowerHexToUnsignedLong(chars, 16, 32); + return new long[] { high, low }; + } + return new long[] { HexCodec.lenientLowerHexToUnsignedLong(chars, 0, 16) }; + } + + /** + * Returns the {@code long} value whose base16 representation is stored in the first + * 16 chars of {@code chars} starting from the {@code offset}. + * @param chars the base16 representation of the {@code long}. + * @return long value from string + */ + public static long longFromBase16String(CharSequence chars) { + return longFromBase16String(chars, 0); + } + + /** + * Returns the {@code long} value whose base16 representation is stored in the first + * 16 chars of {@code chars} starting from the {@code offset}. + * @param chars the base16 representation of the {@code long}. + */ + static long longFromBase16String(CharSequence chars, int offset) { + Assert.isTrue(chars.length() >= offset + LONG_BASE16, "chars too small"); + return (decodeByte(chars.charAt(offset), chars.charAt(offset + 1)) & 0xFFL) << 56 + | (decodeByte(chars.charAt(offset + 2), chars.charAt(offset + 3)) & 0xFFL) << 48 + | (decodeByte(chars.charAt(offset + 4), chars.charAt(offset + 5)) & 0xFFL) << 40 + | (decodeByte(chars.charAt(offset + 6), chars.charAt(offset + 7)) & 0xFFL) << 32 + | (decodeByte(chars.charAt(offset + 8), chars.charAt(offset + 9)) & 0xFFL) << 24 + | (decodeByte(chars.charAt(offset + 10), chars.charAt(offset + 11)) & 0xFFL) << 16 + | (decodeByte(chars.charAt(offset + 12), chars.charAt(offset + 13)) & 0xFFL) << 8 + | (decodeByte(chars.charAt(offset + 14), chars.charAt(offset + 15)) & 0xFFL); + } + + /** + * Decodes the specified two character sequence, and returns the resulting + * {@code byte}. + * @param chars the character sequence to be decoded. + * @param offset the starting offset in the {@code CharSequence}. + * @return the resulting {@code byte} + * @throws IllegalArgumentException if the input is not a valid encoded string + * according to this encoding. + */ + public static byte byteFromBase16String(CharSequence chars, int offset) { + Assert.isTrue(chars.length() >= offset + 2, "chars too small"); + return decodeByte(chars.charAt(offset), chars.charAt(offset + 1)); + } + + private static byte decodeByte(char hi, char lo) { + Assert.isTrue(lo < ASCII_CHARACTERS && DECODING[lo] != -1, "invalid character " + lo); + Assert.isTrue(hi < ASCII_CHARACTERS && DECODING[hi] != -1, "invalid character " + hi); + int decoded = DECODING[hi] << 4 | DECODING[lo]; + return (byte) decoded; + } + + /** + * Checks if string is valid base16. + * @param value to check + * @return {@code true} if valid base16 string + */ + public static boolean isValidBase16String(CharSequence value) { + for (int i = 0; i < value.length(); i++) { + char b = value.charAt(i); + // 48..57 && 97..102 are valid + if (!isDigit(b) && !isLowercaseHexCharacter(b)) { + return false; + } + } + return true; + } + + /** + * Converts long into string. + * @param id 64 bit + * @return string representation of the long + */ + public static String fromLong(long id) { + return fromLongs(0, id); + } + + /** + * Converts longs into string. + * @param idHigh - trace id high part + * @param idLow - trace id low part + * @return string representation of the long + */ + public static String fromLongs(long idHigh, long idLow) { + if (idHigh == 0L) { + return HexCodec.toLowerHex(idLow); + } + else { + char[] chars = getTemporaryBuffer(); + longToBase16String(idHigh, chars, 0); + longToBase16String(idLow, chars, 16); + return new String(chars); + } + } + + public static void longToBase16String(long value, char[] dest, int destOffset) { + byteToBase16((byte) ((int) (value >> 56 & 255L)), dest, destOffset); + byteToBase16((byte) ((int) (value >> 48 & 255L)), dest, destOffset + 2); + byteToBase16((byte) ((int) (value >> 40 & 255L)), dest, destOffset + 4); + byteToBase16((byte) ((int) (value >> 32 & 255L)), dest, destOffset + 6); + byteToBase16((byte) ((int) (value >> 24 & 255L)), dest, destOffset + 8); + byteToBase16((byte) ((int) (value >> 16 & 255L)), dest, destOffset + 10); + byteToBase16((byte) ((int) (value >> 8 & 255L)), dest, destOffset + 12); + byteToBase16((byte) ((int) (value & 255L)), dest, destOffset + 14); + } + + public static void byteToBase16(byte value, char[] dest, int destOffset) { + int b = value & 255; + dest[destOffset] = ENCODING[b]; + dest[destOffset + 1] = ENCODING[b | 256]; + } + + private static char[] getTemporaryBuffer() { + char[] chars = charBuffer.get(); + if (chars == null) { + chars = new char[32]; + charBuffer.set(chars); + } + return chars; + } + + private static boolean isLowercaseHexCharacter(char b) { + return 97 <= b && b <= 102; + } + + private static boolean isDigit(char b) { + return 48 <= b && b <= 57; + } + +} + +// taken from brave.internal.codec.HexCodec +final class HexCodec { + + private HexCodec() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + /** + * Parses a 16 character lower-hex string with no prefix into an unsigned long, + * starting at the specified index. + * + * This reads a trace context a sequence potentially larger than the format. The + * use-case is reducing garbage, by re-using the input {@code value} across multiple + * parse operations. + * @param value the sequence that contains a lower-hex encoded unsigned long. + * @param beginIndex the inclusive begin index: {@linkplain CharSequence#charAt(int) + * index} of the first lower-hex character representing the unsigned long. + */ + static long lowerHexToUnsignedLong(CharSequence value, int beginIndex) { + int endIndex = Math.min(beginIndex + 16, value.length()); + long result = lenientLowerHexToUnsignedLong(value, beginIndex, endIndex); + if (result == 0) { + throw isntLowerHexLong(value); + } + return result; + } + + /** + * Like {@link #lowerHexToUnsignedLong(CharSequence, int)}, but returns zero on + * invalid input. + * @param value the sequence that contains a lower-hex encoded unsigned long. + * @param beginIndex the inclusive begin index: {@linkplain CharSequence#charAt(int) + * index} of the first lower-hex character representing the unsigned long. + * @param endIndex the exclusive end index: {@linkplain CharSequence#charAt(int) + * index} after the last lower-hex character representing the unsigned long. + */ + static long lenientLowerHexToUnsignedLong(CharSequence value, int beginIndex, int endIndex) { + long result = 0; + int pos = beginIndex; + while (pos < endIndex) { + char c = value.charAt(pos++); + result <<= 4; + if (c >= '0' && c <= '9') { + result |= c - '0'; + } + else if (c >= 'a' && c <= 'f') { + result |= c - 'a' + 10; + } + else { + return 0; + } + } + return result; + } + + static NumberFormatException isntLowerHexLong(CharSequence lowerHex) { + throw new NumberFormatException(lowerHex + " should be a 1 to 32 character lower-hex string with no prefix"); + } + + /** Inspired by {@code okio.Buffer.writeLong}. */ + static String toLowerHex(long v) { + char[] data = RecyclableBuffers.parseBuffer(); + writeHexLong(data, 0, v); + return new String(data, 0, 16); + } + + /** Inspired by {@code okio.Buffer.writeLong}. */ + static void writeHexLong(char[] data, int pos, long v) { + writeHexByte(data, pos + 0, (byte) ((v >>> 56L) & 0xff)); + writeHexByte(data, pos + 2, (byte) ((v >>> 48L) & 0xff)); + writeHexByte(data, pos + 4, (byte) ((v >>> 40L) & 0xff)); + writeHexByte(data, pos + 6, (byte) ((v >>> 32L) & 0xff)); + writeHexByte(data, pos + 8, (byte) ((v >>> 24L) & 0xff)); + writeHexByte(data, pos + 10, (byte) ((v >>> 16L) & 0xff)); + writeHexByte(data, pos + 12, (byte) ((v >>> 8L) & 0xff)); + writeHexByte(data, pos + 14, (byte) (v & 0xff)); + } + + static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + static void writeHexByte(char[] data, int pos, byte b) { + data[pos + 0] = HEX_DIGITS[(b >> 4) & 0xf]; + data[pos + 1] = HEX_DIGITS[b & 0xf]; + } + +} + +// taken from brave +final class RecyclableBuffers { + + private RecyclableBuffers() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + private static final ThreadLocal PARSE_BUFFER = new ThreadLocal<>(); + + /** + * Returns a {@link ThreadLocal} reused {@code char[]} for use when decoding bytes + * into an ID hex string. The buffer should be immediately copied into a + * {@link String} after decoding within the same method. + */ + static char[] parseBuffer() { + char[] idBuffer = PARSE_BUFFER.get(); + if (idBuffer == null) { + idBuffer = new char[32 + 1 + 16 + 3 + 16]; // traceid128-spanid-1-parentid + PARSE_BUFFER.set(idBuffer); + } + return idBuffer; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java index 295d2288d..0b56fc984 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java @@ -85,6 +85,11 @@ class SimpleTracer implements Tracer { return null; } + @Override + public TraceContext.Builder traceContextBuilder() { + return null; + } + @Override public Map getAllBaggage() { return new HashMap<>(); diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/internal/EncodingUtilsTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/internal/EncodingUtilsTests.java new file mode 100644 index 000000000..3472e237f --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/internal/EncodingUtilsTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.internal; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +class EncodingUtilsTests { + + @Test + void should_convert_back_and_forth_with_64bits() { + long[] fromString = EncodingUtils.fromString("7c6239a5ad0a4287"); + BDDAssertions.then(fromString).hasSize(1); + + String fromLong = EncodingUtils.fromLong(fromString[0]); + + BDDAssertions.then(fromLong).isEqualTo("7c6239a5ad0a4287"); + } + + @Test + void should_convert_back_and_forth_with_128bits() { + long[] fromString = EncodingUtils.fromString("596e1787feb110407c6239a5ad0a4287"); + BDDAssertions.then(fromString).hasSize(2); + String fromLong = EncodingUtils.fromLongs(fromString[0], fromString[1]); + + BDDAssertions.then(fromLong).isEqualTo("596e1787feb110407c6239a5ad0a4287"); + } + +} diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index 621d49fe6..b742ec657 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -54,6 +54,7 @@ spring-cloud-sleuth-instrumentation-scheduling-tests spring-cloud-sleuth-instrumentation-task-tests spring-cloud-sleuth-instrumentation-webflux-tests + spring-cloud-sleuth-instrumentation-rsocket-tests spring-cloud-sleuth-zipkin-tests diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/pom.xml new file mode 100644 index 000000000..5df34188c --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/pom.xml @@ -0,0 +1,100 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-rsocket-tests + jar + Spring Cloud Sleuth Brave RSocket Instrumentation Tests + Spring Cloud Sleuth Brave RSocket Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + + + + ${project.groupId} + spring-cloud-sleuth-tests-common + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + org.springframework.boot + spring-boot-starter-rsocket + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-test + + + io.zipkin.brave + brave-tests + + + org.awaitility + awaitility + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/rsocket/TraceRSocketTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/rsocket/TraceRSocketTests.java new file mode 100644 index 000000000..4cd869a93 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/rsocket/TraceRSocketTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.rsocket; + +import brave.sampler.Sampler; + +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +public class TraceRSocketTests extends org.springframework.cloud.sleuth.instrument.rsocket.TraceRSocketTests { + + @Override + protected Class testConfiguration() { + return Config.class; + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + org.springframework.cloud.sleuth.test.TestSpanHandler testSpanHandlerSupplier( + brave.test.TestSpanHandler testSpanHandler) { + return new BraveTestSpanHandler(testSpanHandler); + } + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + brave.test.TestSpanHandler braveTestSpanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/resources/logback.xml b/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/resources/logback.xml new file mode 100644 index 000000000..c5c54bb21 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-rsocket-tests/src/test/resources/logback.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + diff --git a/tests/common/pom.xml b/tests/common/pom.xml index 16cb95b52..7c1fb98de 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -1,230 +1,235 @@ - - - - - 4.0.0 - - spring-cloud-sleuth-tests-common - jar - Spring Cloud Sleuth Tests Common - Spring Cloud Sleuth Tests Common - - - org.springframework.cloud - spring-cloud-sleuth-tests - 3.1.0-SNAPSHOT - .. - - - - - org.springframework.cloud - spring-cloud-sleuth-instrumentation - - - org.springframework.boot - spring-boot-starter-test - compile - - - org.awaitility - awaitility - compile - - - com.squareup.okhttp3 - mockwebserver - true - - - org.springframework.integration - spring-integration-core - true - - - org.springframework.boot - spring-boot-starter-websocket - true - - - org.springframework.boot - spring-boot-starter-actuator - true - - - org.springframework.boot - spring-boot-starter-web - true - - - org.springframework.boot - spring-boot-starter-webflux - true - - - org.springframework.cloud - spring-cloud-starter-gateway - true - - - org.springframework.cloud - spring-cloud-starter-task - true - - - org.springframework.cloud - spring-cloud-starter-circuitbreaker-resilience4j - true - - - org.springframework.cloud - spring-cloud-starter-circuitbreaker-reactor-resilience4j - true - - - org.springframework.boot - spring-boot-starter-quartz - true - - - org.springframework.cloud - spring-cloud-starter-openfeign - true - - - io.github.openfeign - feign-okhttp - true - - - org.springframework.cloud - spring-cloud-starter-loadbalancer - true - - - org.springframework.cloud - spring-cloud-config-server - true - - - org.apache.httpcomponents - httpclient - true - - - org.springframework.cloud - spring-cloud-sleuth-autoconfigure - true - - - org.springframework.cloud - spring-cloud-sleuth-brave - true - - - io.zipkin.brave - brave-tests - true - - - io.projectreactor.kafka - reactor-kafka - true - - - org.testcontainers - testcontainers - true - - - org.testcontainers - junit-jupiter - true - - - org.testcontainers - kafka - true - - - org.springframework.cloud - spring-cloud-sleuth-zipkin - true - - - io.zipkin.zipkin2 - zipkin - true - - - io.zipkin.reporter2 - zipkin-reporter - true - - - io.zipkin.reporter2 - zipkin-reporter-brave - true - - - io.zipkin.reporter2 - zipkin-sender-kafka - true - - - - org.apache.kafka - kafka-clients - - - - - io.zipkin.reporter2 - zipkin-sender-activemq-client - true - - - org.apache.activemq - activemq-client - - - - - org.apache.activemq - activemq-client - true - - - io.zipkin.reporter2 - zipkin-sender-amqp-client - true - - - - com.rabbitmq - amqp-client - - - - - - - + + + + + 4.0.0 + + spring-cloud-sleuth-tests-common + jar + Spring Cloud Sleuth Tests Common + Spring Cloud Sleuth Tests Common + + + org.springframework.cloud + spring-cloud-sleuth-tests + 3.1.0-SNAPSHOT + .. + + + + + org.springframework.cloud + spring-cloud-sleuth-instrumentation + + + org.springframework.boot + spring-boot-starter-test + compile + + + org.awaitility + awaitility + compile + + + com.squareup.okhttp3 + mockwebserver + true + + + org.springframework.integration + spring-integration-core + true + + + org.springframework.boot + spring-boot-starter-websocket + true + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.springframework.boot + spring-boot-starter-web + true + + + org.springframework.boot + spring-boot-starter-webflux + true + + + org.springframework.cloud + spring-cloud-starter-gateway + true + + + org.springframework.cloud + spring-cloud-starter-task + true + + + org.springframework.cloud + spring-cloud-starter-circuitbreaker-resilience4j + true + + + org.springframework.cloud + spring-cloud-starter-circuitbreaker-reactor-resilience4j + true + + + org.springframework.boot + spring-boot-starter-quartz + true + + + org.springframework.cloud + spring-cloud-starter-openfeign + true + + + io.github.openfeign + feign-okhttp + true + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + true + + + org.springframework.cloud + spring-cloud-config-server + true + + + org.apache.httpcomponents + httpclient + true + + + org.springframework.cloud + spring-cloud-sleuth-autoconfigure + true + + + org.springframework.cloud + spring-cloud-sleuth-brave + true + + + io.zipkin.brave + brave-tests + true + + + io.projectreactor.kafka + reactor-kafka + true + + + org.testcontainers + testcontainers + true + + + org.testcontainers + junit-jupiter + true + + + org.testcontainers + kafka + true + + + org.springframework.boot + spring-boot-starter-rsocket + true + + + org.springframework.cloud + spring-cloud-sleuth-zipkin + true + + + io.zipkin.zipkin2 + zipkin + true + + + io.zipkin.reporter2 + zipkin-reporter + true + + + io.zipkin.reporter2 + zipkin-reporter-brave + true + + + io.zipkin.reporter2 + zipkin-sender-kafka + true + + + + org.apache.kafka + kafka-clients + + + + + io.zipkin.reporter2 + zipkin-sender-activemq-client + true + + + org.apache.activemq + activemq-client + + + + + org.apache.activemq + activemq-client + true + + + io.zipkin.reporter2 + zipkin-sender-amqp-client + true + + + + com.rabbitmq + amqp-client + + + + + + + diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/config/ConfigServerIntegrationTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/config/ConfigServerIntegrationTests.java index e27a43140..8a954219e 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/config/ConfigServerIntegrationTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/config/ConfigServerIntegrationTests.java @@ -88,7 +88,7 @@ public abstract class ConfigServerIntegrationTests { void call(int port) { log.info("Sending request"); - String result = new RestTemplate().getForObject("http://localhost:" + port + "/master/application.yml", + String result = new RestTemplate().getForObject("http://localhost:" + port + "/foo/default/main", String.class); log.info("Got [\n" + result + "\n]"); } diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java new file mode 100644 index 000000000..556b4ff54 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java @@ -0,0 +1,363 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import java.net.URI; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; + +import brave.Span; +import brave.Tracer; +import brave.test.TestSpanHandler; +import io.rsocket.frame.FrameType; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketRequester.Builder; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.stereotype.Controller; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.BDDAssertions.then; + +public abstract class TraceRSocketTests { + + public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; + + @Test + public void should_instrument_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); + + final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); + + FrameType receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST FNF + whenNonSampledRequestFnfIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenNonSampledRequestResponseIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenNonSampledRequestStreamIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenNonSampledRequestChannelIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + @Test + public void should_instrument_requester_and_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + + final org.springframework.cloud.sleuth.Tracer tracer = context + .getBean(org.springframework.cloud.sleuth.Tracer.class); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + + final Builder rsocketRequesterBuilder = context.getBean(Builder.class); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) + .doFinally(signalType -> nextSpanFnf.end()).block(); + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) + .doFinally(signalType -> nextSpanRR.end()).block(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) + .doFinally(signalType -> nextSpanRS.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) + .doFinally(signalType -> nextSpanRC.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + protected abstract Class testConfiguration(); + + private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { + then(spans).hasSize(1); + // TODO: Preferred option would be : [api.c2.{name}] + then(spans.get(0).name()).isEqualTo(frameType.name() + " " + path); + } + + private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { + return requester.route(path).send(); + } + + private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveMono(String.class); + } + + private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveFlux(String.class); + } + + private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { + return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); + } + + private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { + requester.route("api.c2.fnf").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).send().block(); + } + + private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { + requester.route("api.c2.rr").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveMono(String.class).block(); + } + + private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { + requester.route("api.c2.rs").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveFlux(String.class).blockLast(); + } + + private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { + requester.route("api.c2.rc").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); + } + + private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { + // then(spans).isEmpty(); // FIXME: does not work for request case + then(controller2.getSpan()).isNotNull(); + then(controller2.getSpan().context().traceIdString()).isEqualTo(expectedTraceId); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + static class MyConfig { + + @Bean + TestController controller(Tracer tracer) { + return new TestController(tracer); + } + + } + + @Controller + @MessageMapping("api.c2") + static class TestController { + + final Tracer tracer; + + Span span; + + ContextView interceptedContext; + + BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); + + TestController(Tracer tracer) { + this.tracer = tracer; + } + + BlockingQueue getReceivedFrames() { + return this.receivedFrames; + } + + Span getSpan() { + return this.span; + } + + void reset() { + this.span = null; + } + + @MessageMapping("fnf") + Mono testFnf() { + + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_FNF); + return Mono.empty(); + }); + } + + @MessageMapping("rr") + Mono testRR() { + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_RESPONSE); + return Mono.just("response"); + }); + } + + @MessageMapping("rs") + Flux testRS() { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_STREAM); + return Flux.just("stream"); + }); + } + + @MessageMapping("rc") + Flux testRC(@Payload Flux inbound) { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_CHANNEL); + return inbound; + }); + } + + } + +} From a6bf9a328a951fef9b750a430fba5e9d876dfb7f Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 29 Apr 2021 14:08:49 +0200 Subject: [PATCH 12/59] Fixed the messaging aspect --- .../instrument/deployer/TraceAppDeployer.java | 516 ++++++------- .../messaging/TraceMessagingAspect.java | 2 +- .../instrument/rsocket/TraceRSocketTests.java | 730 +++++++++--------- 3 files changed, 626 insertions(+), 622 deletions(-) diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java index b236a6057..b4f2febe5 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java @@ -1,92 +1,92 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.deployer; - -import java.time.Duration; -import java.util.Arrays; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.cloud.deployer.spi.app.AppDeployer; -import org.springframework.cloud.deployer.spi.app.AppScaleRequest; -import org.springframework.cloud.deployer.spi.app.AppStatus; -import org.springframework.cloud.deployer.spi.app.DeploymentState; -import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; -import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; -import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; -import org.springframework.core.env.Environment; - -/** - * Trace representation of an {@link AppDeployer}. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -public class TraceAppDeployer implements AppDeployer { - - private static final Log log = LogFactory.getLog(TraceAppDeployer.class); - - private final AppDeployer delegate; - - private final BeanFactory beanFactory; - - private final Environment environment; - - private Tracer tracer; - - private CurrentTraceContext currentTraceContext; - - private Long pollDelay; - - public TraceAppDeployer(AppDeployer delegate, BeanFactory beanFactory, Environment environment) { - this.delegate = delegate; - this.beanFactory = beanFactory; - this.environment = environment; - } - - @Override - public String deploy(AppDeploymentRequest request) { - Span span = tracer().nextSpan().name("deploy"); - // TODO: Is this secure to pass? - // TODO: Does it make sense? - // if (!request.getCommandlineArguments().isEmpty()) { - // span.tag("commandlineArguments", request.getCommandlineArguments().toString()); - // } - // if (!request.getDeploymentProperties().isEmpty()) { - // span.tag("deploymentProperties", request.getDeploymentProperties().toString()); - // } - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - span.event("start"); - String id = this.delegate.deploy(request); - span.tag("deployer.app.id", id); - registerListener(span, id); - return id; - } - } - - private void registerListener(Span span, String id) { - PreviousAndCurrentStatus previousAndCurrentStatus = new PreviousAndCurrentStatus(span); +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.deployer; + +import java.time.Duration; +import java.util.Arrays; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.deployer.spi.app.AppDeployer; +import org.springframework.cloud.deployer.spi.app.AppScaleRequest; +import org.springframework.cloud.deployer.spi.app.AppStatus; +import org.springframework.cloud.deployer.spi.app.DeploymentState; +import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; +import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; +import org.springframework.core.env.Environment; + +/** + * Trace representation of an {@link AppDeployer}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceAppDeployer implements AppDeployer { + + private static final Log log = LogFactory.getLog(TraceAppDeployer.class); + + private final AppDeployer delegate; + + private final BeanFactory beanFactory; + + private final Environment environment; + + private Tracer tracer; + + private CurrentTraceContext currentTraceContext; + + private Long pollDelay; + + public TraceAppDeployer(AppDeployer delegate, BeanFactory beanFactory, Environment environment) { + this.delegate = delegate; + this.beanFactory = beanFactory; + this.environment = environment; + } + + @Override + public String deploy(AppDeploymentRequest request) { + Span span = tracer().nextSpan().name("deploy"); + // TODO: Is this secure to pass? + // TODO: Does it make sense? + // if (!request.getCommandlineArguments().isEmpty()) { + // span.tag("commandlineArguments", request.getCommandlineArguments().toString()); + // } + // if (!request.getDeploymentProperties().isEmpty()) { + // span.tag("deploymentProperties", request.getDeploymentProperties().toString()); + // } + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + span.event("start"); + String id = this.delegate.deploy(request); + span.tag("deployer.app.id", id); + registerListener(span, id); + return id; + } + } + + private void registerListener(Span span, String id) { + PreviousAndCurrentStatus previousAndCurrentStatus = new PreviousAndCurrentStatus(span); // @formatter:off this.delegate.statusReactive(id) .map(previousAndCurrentStatus::updateCurrent) @@ -97,172 +97,172 @@ public class TraceAppDeployer implements AppDeployer { .doOnError(span::error) // we will close the span in the reactive part .doFinally(signalType -> span.end()).subscribe(); - // @formatter:on - } - - @Override - public void undeploy(String id) { - Span span = tracer().nextSpan().name("undeploy"); - span.tag("deployer.app.id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - span.event("start"); - this.delegate.undeploy(id); - registerListener(span, id); - } - finally { - span.end(); - } - } - - @Override - public AppStatus status(String id) { - Span span = tracer().nextSpan().name("status"); - span.tag("deployer.app.id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - return this.delegate.status(id); - } - finally { - span.end(); - } - } - - @Override - public Mono statusReactive(String id) { - return ReactorSleuth.tracedMono(tracer(), currentTraceContext(), "status", - () -> this.delegate.statusReactive(id), span -> span.tag("deployer.app.id", id)); - } - - @Override - public Flux statusesReactive(String... ids) { - return ReactorSleuth.tracedFlux(tracer(), currentTraceContext(), "statuses", - () -> this.delegate.statusesReactive(ids), span -> span.tag("deployer.app.ids", Arrays.toString(ids))); - } - - @Override - public RuntimeEnvironmentInfo environmentInfo() { - return this.delegate.environmentInfo(); - } - - @Override - public String getLog(String id) { - Span span = tracer().nextSpan().name("getLog"); - span.tag("deployer.app.id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - return this.delegate.getLog(id); - } - finally { - span.end(); - } - } - - @Override - public void scale(AppScaleRequest appScaleRequest) { - Span span = tracer().nextSpan().name("scale"); - span.tag("deployer.scale.deploymentId", appScaleRequest.getDeploymentId()); - span.tag("deployer.scale.count", String.valueOf(appScaleRequest.getCount())); - // TODO: Is this secure to pass? - // TODO: Does it make sense? - // if (appScaleRequest.getProperties().isPresent() && - // !appScaleRequest.getProperties().get().isEmpty()) { - // span.tag("properties", appScaleRequest.getProperties().get().toString()); - // } - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - this.delegate.scale(appScaleRequest); - } - finally { - span.end(); - } - } - - private Tracer tracer() { - if (this.tracer == null) { - this.tracer = this.beanFactory.getBean(Tracer.class); - } - return this.tracer; - } - - private CurrentTraceContext currentTraceContext() { - if (this.currentTraceContext == null) { - this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); - } - return this.currentTraceContext; - } - - private long pollDelay() { - if (this.pollDelay == null) { - this.pollDelay = this.environment.getProperty("spring.sleuth.deployer.status-poll-delay", Long.class, 500L); - } - return this.pollDelay; - } - - private static final class PreviousAndCurrentStatus { - - private final Span span; - - private AppStatus current; - - private AppStatus previous; - - private PreviousAndCurrentStatus(Span span) { - this.span = span; - if (log.isDebugEnabled()) { - log.debug("Current span is [" + span + "]"); - } - } - - private PreviousAndCurrentStatus updateCurrent(AppStatus current) { - if (log.isTraceEnabled()) { - log.trace("State before change: current [" + this.current + "], previous [" + this.previous + "]"); - } - this.previous = this.current; - this.current = current; - if (log.isTraceEnabled()) { - log.trace("State after change: current [" + this.current + "], previous [" + this.previous + "]"); - } - if (statusChanged()) { - annotateSpan(); - } - else if (log.isTraceEnabled()) { - log.trace("State has not changed, will not annotate the span"); - } - return this; - } - - private void annotateSpan() { - String name = this.current.getState().name(); - if (log.isDebugEnabled()) { - log.debug("Will annotate its state with [" + name + "]"); - } - this.span.event(name); - } - - private boolean statusChanged() { - if (this.previous == null && this.current != null) { - if (log.isDebugEnabled()) { - log.debug("Previous is null, current is not null"); - } - return true; - } - else if (this.current == null) { - throw new IllegalStateException("Current state can't be null"); - } - DeploymentState currentState = this.current.getState(); - DeploymentState previousState = this.previous.getState(); - return currentState != previousState; - } - - private boolean isFinished() { - boolean finished = this.current.getState() == DeploymentState.deployed - || this.current.getState() == DeploymentState.undeployed - || this.current.getState() == DeploymentState.failed - || this.current.getState() == DeploymentState.error - || this.current.getState() == DeploymentState.unknown; - if (log.isTraceEnabled()) { - log.trace("Status is finished [" + finished + "]"); - } - return finished; - } - - } - -} + // @formatter:on + } + + @Override + public void undeploy(String id) { + Span span = tracer().nextSpan().name("undeploy"); + span.tag("deployer.app.id", id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + span.event("start"); + this.delegate.undeploy(id); + registerListener(span, id); + } + finally { + span.end(); + } + } + + @Override + public AppStatus status(String id) { + Span span = tracer().nextSpan().name("status"); + span.tag("deployer.app.id", id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + return this.delegate.status(id); + } + finally { + span.end(); + } + } + + @Override + public Mono statusReactive(String id) { + return ReactorSleuth.tracedMono(tracer(), currentTraceContext(), "status", + () -> this.delegate.statusReactive(id), span -> span.tag("deployer.app.id", id)); + } + + @Override + public Flux statusesReactive(String... ids) { + return ReactorSleuth.tracedFlux(tracer(), currentTraceContext(), "statuses", + () -> this.delegate.statusesReactive(ids), span -> span.tag("deployer.app.ids", Arrays.toString(ids))); + } + + @Override + public RuntimeEnvironmentInfo environmentInfo() { + return this.delegate.environmentInfo(); + } + + @Override + public String getLog(String id) { + Span span = tracer().nextSpan().name("getLog"); + span.tag("deployer.app.id", id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + return this.delegate.getLog(id); + } + finally { + span.end(); + } + } + + @Override + public void scale(AppScaleRequest appScaleRequest) { + Span span = tracer().nextSpan().name("scale"); + span.tag("deployer.scale.deploymentId", appScaleRequest.getDeploymentId()); + span.tag("deployer.scale.count", String.valueOf(appScaleRequest.getCount())); + // TODO: Is this secure to pass? + // TODO: Does it make sense? + // if (appScaleRequest.getProperties().isPresent() && + // !appScaleRequest.getProperties().get().isEmpty()) { + // span.tag("properties", appScaleRequest.getProperties().get().toString()); + // } + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + this.delegate.scale(appScaleRequest); + } + finally { + span.end(); + } + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + + private CurrentTraceContext currentTraceContext() { + if (this.currentTraceContext == null) { + this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); + } + return this.currentTraceContext; + } + + private long pollDelay() { + if (this.pollDelay == null) { + this.pollDelay = this.environment.getProperty("spring.sleuth.deployer.status-poll-delay", Long.class, 500L); + } + return this.pollDelay; + } + + private static final class PreviousAndCurrentStatus { + + private final Span span; + + private AppStatus current; + + private AppStatus previous; + + private PreviousAndCurrentStatus(Span span) { + this.span = span; + if (log.isDebugEnabled()) { + log.debug("Current span is [" + span + "]"); + } + } + + private PreviousAndCurrentStatus updateCurrent(AppStatus current) { + if (log.isTraceEnabled()) { + log.trace("State before change: current [" + this.current + "], previous [" + this.previous + "]"); + } + this.previous = this.current; + this.current = current; + if (log.isTraceEnabled()) { + log.trace("State after change: current [" + this.current + "], previous [" + this.previous + "]"); + } + if (statusChanged()) { + annotateSpan(); + } + else if (log.isTraceEnabled()) { + log.trace("State has not changed, will not annotate the span"); + } + return this; + } + + private void annotateSpan() { + String name = this.current.getState().name(); + if (log.isDebugEnabled()) { + log.debug("Will annotate its state with [" + name + "]"); + } + this.span.event(name); + } + + private boolean statusChanged() { + if (this.previous == null && this.current != null) { + if (log.isDebugEnabled()) { + log.debug("Previous is null, current is not null"); + } + return true; + } + else if (this.current == null) { + throw new IllegalStateException("Current state can't be null"); + } + DeploymentState currentState = this.current.getState(); + DeploymentState previousState = this.previous.getState(); + return currentState != previousState; + } + + private boolean isFinished() { + boolean finished = this.current.getState() == DeploymentState.deployed + || this.current.getState() == DeploymentState.undeployed + || this.current.getState() == DeploymentState.failed + || this.current.getState() == DeploymentState.error + || this.current.getState() == DeploymentState.unknown; + if (log.isTraceEnabled()) { + log.trace("Status is finished [" + finished + "]"); + } + return finished; + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessagingAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessagingAspect.java index fda6d71bd..a2b262bc0 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessagingAspect.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessagingAspect.java @@ -59,7 +59,7 @@ public class TraceMessagingAspect { this.spanNamer = spanNamer; } - @Pointcut("@within(org.springframework.messaging.handler.annotation.MessageMapping)") + @Pointcut("@annotation(org.springframework.messaging.handler.annotation.MessageMapping)") private void anyMessageMappingAnnotated() { } // NOSONAR diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java index 556b4ff54..43a1f9f97 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java @@ -1,363 +1,367 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.rsocket; - -import java.net.URI; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingDeque; - -import brave.Span; -import brave.Tracer; -import brave.test.TestSpanHandler; -import io.rsocket.frame.FrameType; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.context.ContextView; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.sleuth.TraceContext; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.rsocket.RSocketRequester; -import org.springframework.messaging.rsocket.RSocketRequester.Builder; -import org.springframework.messaging.rsocket.RSocketStrategies; -import org.springframework.stereotype.Controller; -import org.springframework.util.MimeType; - -import static org.assertj.core.api.BDDAssertions.then; - -public abstract class TraceRSocketTests { - - public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; - - @Test - public void should_instrument_responder() throws Exception { - // setup - ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) - .web(WebApplicationType.REACTIVE) - .properties("server.port=0", "spring.rsocket.server.transport=websocket", - "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", - "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", - "management.security.enabled=false") - .run(); - final TestSpanHandler spans = context.getBean(TestSpanHandler.class); - final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); - final TestController controller2 = context.getBean(TestController.class); - final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); - - final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); - - final RSocketRequester rSocketRequester = rsocketRequesterBuilder - .websocket(URI.create("ws://localhost:" + port + "/rsocket")); - - // REQUEST FNF - whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); - - FrameType receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST FNF - whenNonSampledRequestFnfIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - whenNonSampledRequestResponseIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - whenNonSampledRequestStreamIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - whenNonSampledRequestChannelIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); - spans.clear(); - controller2.reset(); - - // cleanup - context.close(); - } - - @Test - public void should_instrument_requester_and_responder() throws Exception { - // setup - ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) - .web(WebApplicationType.REACTIVE) - .properties("server.port=0", "spring.rsocket.server.transport=websocket", - "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", - "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", - "management.security.enabled=false") - .run(); - - final org.springframework.cloud.sleuth.Tracer tracer = context - .getBean(org.springframework.cloud.sleuth.Tracer.class); - final TestSpanHandler spans = context.getBean(TestSpanHandler.class); - final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); - final TestController controller2 = context.getBean(TestController.class); - - final Builder rsocketRequesterBuilder = context.getBean(Builder.class); - - final RSocketRequester rSocketRequester = rsocketRequesterBuilder - .websocket(URI.create("ws://localhost:" + port + "/rsocket")); - - // REQUEST FNF - final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); - whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) - .doFinally(signalType -> nextSpanFnf.end()).block(); - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); - whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) - .doFinally(signalType -> nextSpanRR.end()).block(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); - whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) - .doFinally(signalType -> nextSpanRS.end()).blockLast(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); - whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) - .doFinally(signalType -> nextSpanRC.end()).blockLast(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); - spans.clear(); - controller2.reset(); - - // cleanup - context.close(); - } - - protected abstract Class testConfiguration(); - - private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { - then(spans).hasSize(1); - // TODO: Preferred option would be : [api.c2.{name}] - then(spans.get(0).name()).isEqualTo(frameType.name() + " " + path); - } - - private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { - return requester.route(path).send(); - } - - private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { - return requester.route(path).retrieveMono(String.class); - } - - private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { - return requester.route(path).retrieveFlux(String.class); - } - - private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { - return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); - } - - private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { - requester.route("api.c2.fnf").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).send().block(); - } - - private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { - requester.route("api.c2.rr").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).retrieveMono(String.class).block(); - } - - private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { - requester.route("api.c2.rs").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).retrieveFlux(String.class).blockLast(); - } - - private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { - requester.route("api.c2.rc").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); - } - - private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { - // then(spans).isEmpty(); // FIXME: does not work for request case - then(controller2.getSpan()).isNotNull(); - then(controller2.getSpan().context().traceIdString()).isEqualTo(expectedTraceId); - } - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration - static class MyConfig { - - @Bean - TestController controller(Tracer tracer) { - return new TestController(tracer); - } - - } - - @Controller - @MessageMapping("api.c2") - static class TestController { - - final Tracer tracer; - - Span span; - - ContextView interceptedContext; - - BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); - - TestController(Tracer tracer) { - this.tracer = tracer; - } - - BlockingQueue getReceivedFrames() { - return this.receivedFrames; - } - - Span getSpan() { - return this.span; - } - - void reset() { - this.span = null; - } - - @MessageMapping("fnf") - Mono testFnf() { - - this.span = this.tracer.currentSpan(); - - return Mono.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_FNF); - return Mono.empty(); - }); - } - - @MessageMapping("rr") - Mono testRR() { - this.span = this.tracer.currentSpan(); - - return Mono.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_RESPONSE); - return Mono.just("response"); - }); - } - - @MessageMapping("rs") - Flux testRS() { - this.span = this.tracer.currentSpan(); - - return Flux.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_STREAM); - return Flux.just("stream"); - }); - } - - @MessageMapping("rc") - Flux testRC(@Payload Flux inbound) { - this.span = this.tracer.currentSpan(); - - return Flux.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_CHANNEL); - return inbound; - }); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import java.net.URI; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; + +import io.rsocket.frame.FrameType; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketRequester.Builder; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.stereotype.Controller; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.BDDAssertions.then; + +public abstract class TraceRSocketTests { + + public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; + + @Test + public void should_instrument_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); + + final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); + + FrameType receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST FNF + whenNonSampledRequestFnfIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenNonSampledRequestResponseIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenNonSampledRequestStreamIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenNonSampledRequestChannelIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + @Test + public void should_instrument_requester_and_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + + final org.springframework.cloud.sleuth.Tracer tracer = context + .getBean(org.springframework.cloud.sleuth.Tracer.class); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + + final Builder rsocketRequesterBuilder = context.getBean(Builder.class); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) + .doFinally(signalType -> nextSpanFnf.end()).block(); + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) + .doFinally(signalType -> nextSpanRR.end()).block(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) + .doFinally(signalType -> nextSpanRS.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) + .doFinally(signalType -> nextSpanRC.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + protected abstract Class testConfiguration(); + + private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { + then(spans).hasSize(1); + // TODO: Preferred option would be : [api.c2.{name}] + FinishedSpan span = spans.get(0); + then(span.getName()).isEqualTo(frameType.name() + " " + path); + then(span.getTags()).containsEntry("messaging.controller.class", "org.springframework.cloud.sleuth.instrument.rsocket.TraceRSocketTests$TestController"); + then(span.getTags()).containsKey("messaging.controller.method"); + } + + private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { + return requester.route(path).send(); + } + + private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveMono(String.class); + } + + private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveFlux(String.class); + } + + private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { + return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); + } + + private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { + requester.route("api.c2.fnf").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).send().block(); + } + + private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { + requester.route("api.c2.rr").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveMono(String.class).block(); + } + + private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { + requester.route("api.c2.rs").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveFlux(String.class).blockLast(); + } + + private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { + requester.route("api.c2.rc").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); + } + + private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { + // then(spans).isEmpty(); // FIXME: does not work for request case + then(controller2.getSpan()).isNotNull(); + then(controller2.getSpan().context().traceId()).isEqualTo(expectedTraceId); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + static class MyConfig { + + @Bean + TestController controller(Tracer tracer) { + return new TestController(tracer); + } + + } + + @Controller + @MessageMapping("api.c2") + static class TestController { + + final Tracer tracer; + + Span span; + + ContextView interceptedContext; + + BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); + + TestController(Tracer tracer) { + this.tracer = tracer; + } + + BlockingQueue getReceivedFrames() { + return this.receivedFrames; + } + + Span getSpan() { + return this.span; + } + + void reset() { + this.span = null; + } + + @MessageMapping("fnf") + Mono testFnf() { + + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_FNF); + return Mono.empty(); + }); + } + + @MessageMapping("rr") + Mono testRR() { + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_RESPONSE); + return Mono.just("response"); + }); + } + + @MessageMapping("rs") + Flux testRS() { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_STREAM); + return Flux.just("stream"); + }); + } + + @MessageMapping("rc") + Flux testRC(@Payload Flux inbound) { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_CHANNEL); + return inbound; + }); + } + + } + +} From fe0579f9cd9058382f622d846295584cdc4045c1 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 29 Apr 2021 17:04:23 +0200 Subject: [PATCH 13/59] Use string instead of brave class --- .../TraceRSocketAutoConfiguration.java | 178 +++++++++--------- 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java index 584f76199..85520486a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java @@ -1,89 +1,89 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.rsocket; - -import java.util.List; - -import io.rsocket.RSocket; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration; -import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.rsocket.server.RSocketServerCustomizer; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.brave.propagation.PropagationType; -import org.springframework.cloud.sleuth.instrument.rsocket.TracingRSocketConnectorConfigurer; -import org.springframework.cloud.sleuth.instrument.rsocket.TracingRSocketServerCustomizer; -import org.springframework.cloud.sleuth.propagation.Propagator; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Scope; -import org.springframework.messaging.rsocket.RSocketConnectorConfigurer; -import org.springframework.messaging.rsocket.RSocketRequester; -import org.springframework.messaging.rsocket.RSocketRequester.Builder; -import org.springframework.messaging.rsocket.RSocketStrategies; - -@Configuration(proxyBeanMethods = false) -@ConditionalOnBean(Tracer.class) -@ConditionalOnProperty(value = "spring.sleuth.rsocket.enabled", matchIfMissing = true) -@ConditionalOnClass({ RSocket.class, RSocketStrategies.class }) -@AutoConfigureAfter(BraveAutoConfiguration.class) -@AutoConfigureBefore({ RSocketRequesterAutoConfiguration.class, RSocketServerAutoConfiguration.class }) -@EnableConfigurationProperties(SleuthRSocketProperties.class) -public class TraceRSocketAutoConfiguration { - - // We're using text instead of objects cause we can have same properties from Brave / - // OTel - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - Builder rSocketRequesterBuilder(RSocketStrategies strategies, - ObjectProvider connectorConfigurerProvider) { - // TODO: should be in spring boot - final Builder builder = RSocketRequester.builder().rsocketStrategies(strategies); - connectorConfigurerProvider.forEach(builder::rsocketConnector); - return builder; - } - - private boolean containsZipkinPropagationType(List types) { - return types.contains(PropagationType.B3); - } - - @Bean - RSocketConnectorConfigurer tracingRSocketConnectorConfigurer(Propagator propagator, Tracer tracer, - @Value("${spring.sleuth.propagation.type:B3}") List types) { - return new TracingRSocketConnectorConfigurer(propagator, tracer, containsZipkinPropagationType(types)); - } - - // We're using text instead of objects cause we can have same properties from Brave / - // OTel - @Bean - RSocketServerCustomizer tracingRSocketServerCustomizer(Propagator propagator, Tracer tracer, - @Value("${spring.sleuth.propagation.type:B3}") List types) { - return new TracingRSocketServerCustomizer(propagator, tracer, containsZipkinPropagationType(types)); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.rsocket; + +import java.util.List; +import java.util.Locale; + +import io.rsocket.RSocket; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.rsocket.server.RSocketServerCustomizer; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.rsocket.TracingRSocketConnectorConfigurer; +import org.springframework.cloud.sleuth.instrument.rsocket.TracingRSocketServerCustomizer; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.messaging.rsocket.RSocketConnectorConfigurer; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketRequester.Builder; +import org.springframework.messaging.rsocket.RSocketStrategies; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(Tracer.class) +@ConditionalOnProperty(value = "spring.sleuth.rsocket.enabled", matchIfMissing = true) +@ConditionalOnClass({ RSocket.class, RSocketStrategies.class }) +@AutoConfigureAfter(BraveAutoConfiguration.class) +@AutoConfigureBefore({ RSocketRequesterAutoConfiguration.class, RSocketServerAutoConfiguration.class }) +@EnableConfigurationProperties(SleuthRSocketProperties.class) +public class TraceRSocketAutoConfiguration { + + // We're using text instead of objects cause we can have same properties from Brave / + // OTel + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + Builder rSocketRequesterBuilder(RSocketStrategies strategies, + ObjectProvider connectorConfigurerProvider) { + // TODO: should be in spring boot + final Builder builder = RSocketRequester.builder().rsocketStrategies(strategies); + connectorConfigurerProvider.forEach(builder::rsocketConnector); + return builder; + } + + private boolean containsZipkinPropagationType(List types) { + return types.stream().anyMatch(s -> s.toLowerCase(Locale.ROOT).contains("b3")); + } + + @Bean + RSocketConnectorConfigurer tracingRSocketConnectorConfigurer(Propagator propagator, Tracer tracer, + @Value("${spring.sleuth.propagation.type:B3}") List types) { + return new TracingRSocketConnectorConfigurer(propagator, tracer, containsZipkinPropagationType(types)); + } + + // We're using text instead of objects cause we can have same properties from Brave / + // OTel + @Bean + RSocketServerCustomizer tracingRSocketServerCustomizer(Propagator propagator, Tracer tracer, + @Value("${spring.sleuth.propagation.type:B3}") List types) { + return new TracingRSocketServerCustomizer(propagator, tracer, containsZipkinPropagationType(types)); + } + +} From a35929b06dd61a1c6dee292b209e30985ac72f26 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 29 Apr 2021 17:05:42 +0200 Subject: [PATCH 14/59] Use equality instead of contains --- .../instrument/rsocket/TraceRSocketAutoConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java index 85520486a..a1584823b 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java @@ -69,7 +69,7 @@ public class TraceRSocketAutoConfiguration { } private boolean containsZipkinPropagationType(List types) { - return types.stream().anyMatch(s -> s.toLowerCase(Locale.ROOT).contains("b3")); + return types.stream().anyMatch(s -> s.equalsIgnoreCase("b3")); } @Bean From 302e718fff140995fb0e67bdead64591eec3f158 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 29 Apr 2021 17:09:53 +0200 Subject: [PATCH 15/59] Make RSocket tests configurable for OTel --- .../TraceRSocketAutoConfiguration.java | 1 - .../instrument/rsocket/TraceRSocketTests.java | 743 +++++++++--------- 2 files changed, 376 insertions(+), 368 deletions(-) diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java index a1584823b..cd56303f9 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java @@ -17,7 +17,6 @@ package org.springframework.cloud.sleuth.autoconfig.instrument.rsocket; import java.util.List; -import java.util.Locale; import io.rsocket.RSocket; diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java index 43a1f9f97..0a2a2411f 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java @@ -1,367 +1,376 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.rsocket; - -import java.net.URI; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingDeque; - -import io.rsocket.frame.FrameType; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.context.ContextView; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.TraceContext; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.exporter.FinishedSpan; -import org.springframework.cloud.sleuth.test.TestSpanHandler; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.rsocket.RSocketRequester; -import org.springframework.messaging.rsocket.RSocketRequester.Builder; -import org.springframework.messaging.rsocket.RSocketStrategies; -import org.springframework.stereotype.Controller; -import org.springframework.util.MimeType; - -import static org.assertj.core.api.BDDAssertions.then; - -public abstract class TraceRSocketTests { - - public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; - - @Test - public void should_instrument_responder() throws Exception { - // setup - ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) - .web(WebApplicationType.REACTIVE) - .properties("server.port=0", "spring.rsocket.server.transport=websocket", - "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", - "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", - "management.security.enabled=false") - .run(); - final TestSpanHandler spans = context.getBean(TestSpanHandler.class); - final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); - final TestController controller2 = context.getBean(TestController.class); - final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); - - final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); - - final RSocketRequester rSocketRequester = rsocketRequesterBuilder - .websocket(URI.create("ws://localhost:" + port + "/rsocket")); - - // REQUEST FNF - whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); - - FrameType receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST FNF - whenNonSampledRequestFnfIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - whenNonSampledRequestResponseIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - whenNonSampledRequestStreamIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - whenNonSampledRequestChannelIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, EXPECTED_TRACE_ID); - spans.clear(); - controller2.reset(); - - // cleanup - context.close(); - } - - @Test - public void should_instrument_requester_and_responder() throws Exception { - // setup - ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) - .web(WebApplicationType.REACTIVE) - .properties("server.port=0", "spring.rsocket.server.transport=websocket", - "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", - "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", - "management.security.enabled=false") - .run(); - - final org.springframework.cloud.sleuth.Tracer tracer = context - .getBean(org.springframework.cloud.sleuth.Tracer.class); - final TestSpanHandler spans = context.getBean(TestSpanHandler.class); - final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); - final TestController controller2 = context.getBean(TestController.class); - - final Builder rsocketRequesterBuilder = context.getBean(Builder.class); - - final RSocketRequester rSocketRequester = rsocketRequesterBuilder - .websocket(URI.create("ws://localhost:" + port + "/rsocket")); - - // REQUEST FNF - final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); - whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) - .doFinally(signalType -> nextSpanFnf.end()).block(); - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); - whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) - .doFinally(signalType -> nextSpanRR.end()).block(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); - whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) - .doFinally(signalType -> nextSpanRS.end()).blockLast(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); - whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) - .doFinally(signalType -> nextSpanRC.end()).blockLast(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); - spans.clear(); - controller2.reset(); - - // cleanup - context.close(); - } - - protected abstract Class testConfiguration(); - - private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { - then(spans).hasSize(1); - // TODO: Preferred option would be : [api.c2.{name}] - FinishedSpan span = spans.get(0); - then(span.getName()).isEqualTo(frameType.name() + " " + path); - then(span.getTags()).containsEntry("messaging.controller.class", "org.springframework.cloud.sleuth.instrument.rsocket.TraceRSocketTests$TestController"); - then(span.getTags()).containsKey("messaging.controller.method"); - } - - private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { - return requester.route(path).send(); - } - - private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { - return requester.route(path).retrieveMono(String.class); - } - - private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { - return requester.route(path).retrieveFlux(String.class); - } - - private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { - return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); - } - - private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { - requester.route("api.c2.fnf").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).send().block(); - } - - private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { - requester.route("api.c2.rr").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).retrieveMono(String.class).block(); - } - - private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { - requester.route("api.c2.rs").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).retrieveFlux(String.class).blockLast(); - } - - private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { - requester.route("api.c2.rc").metadata(EXPECTED_TRACE_ID + "-" + EXPECTED_TRACE_ID + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); - } - - private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { - // then(spans).isEmpty(); // FIXME: does not work for request case - then(controller2.getSpan()).isNotNull(); - then(controller2.getSpan().context().traceId()).isEqualTo(expectedTraceId); - } - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration - static class MyConfig { - - @Bean - TestController controller(Tracer tracer) { - return new TestController(tracer); - } - - } - - @Controller - @MessageMapping("api.c2") - static class TestController { - - final Tracer tracer; - - Span span; - - ContextView interceptedContext; - - BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); - - TestController(Tracer tracer) { - this.tracer = tracer; - } - - BlockingQueue getReceivedFrames() { - return this.receivedFrames; - } - - Span getSpan() { - return this.span; - } - - void reset() { - this.span = null; - } - - @MessageMapping("fnf") - Mono testFnf() { - - this.span = this.tracer.currentSpan(); - - return Mono.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_FNF); - return Mono.empty(); - }); - } - - @MessageMapping("rr") - Mono testRR() { - this.span = this.tracer.currentSpan(); - - return Mono.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_RESPONSE); - return Mono.just("response"); - }); - } - - @MessageMapping("rs") - Flux testRS() { - this.span = this.tracer.currentSpan(); - - return Flux.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_STREAM); - return Flux.just("stream"); - }); - } - - @MessageMapping("rc") - Flux testRC(@Payload Flux inbound) { - this.span = this.tracer.currentSpan(); - - return Flux.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_CHANNEL); - return inbound; - }); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import java.net.URI; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; + +import io.rsocket.frame.FrameType; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketRequester.Builder; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.stereotype.Controller; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.BDDAssertions.then; + +public abstract class TraceRSocketTests { + + public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; + + @Test + public void should_instrument_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); + + final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); + + FrameType receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST FNF + whenNonSampledRequestFnfIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenNonSampledRequestResponseIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenNonSampledRequestStreamIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenNonSampledRequestChannelIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + protected String expectedTraceId() { + return EXPECTED_TRACE_ID; + } + + protected String expectedSpanId() { + return EXPECTED_TRACE_ID; + } + + @Test + public void should_instrument_requester_and_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + + final org.springframework.cloud.sleuth.Tracer tracer = context + .getBean(org.springframework.cloud.sleuth.Tracer.class); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + + final Builder rsocketRequesterBuilder = context.getBean(Builder.class); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) + .doFinally(signalType -> nextSpanFnf.end()).block(); + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) + .doFinally(signalType -> nextSpanRR.end()).block(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) + .doFinally(signalType -> nextSpanRS.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) + .doFinally(signalType -> nextSpanRC.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + protected abstract Class testConfiguration(); + + private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { + then(spans).hasSize(1); + // TODO: Preferred option would be : [api.c2.{name}] + FinishedSpan span = spans.get(0); + then(span.getName()).isEqualTo(frameType.name() + " " + path); + then(span.getTags()).containsEntry("messaging.controller.class", + "org.springframework.cloud.sleuth.instrument.rsocket.TraceRSocketTests$TestController"); + then(span.getTags()).containsKey("messaging.controller.method"); + } + + private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { + return requester.route(path).send(); + } + + private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveMono(String.class); + } + + private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveFlux(String.class); + } + + private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { + return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); + } + + private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { + requester.route("api.c2.fnf").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).send().block(); + } + + private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { + requester.route("api.c2.rr").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveMono(String.class).block(); + } + + private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { + requester.route("api.c2.rs").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveFlux(String.class).blockLast(); + } + + private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { + requester.route("api.c2.rc").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); + } + + private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { + // then(spans).isEmpty(); // FIXME: does not work for request case + then(controller2.getSpan()).isNotNull(); + then(controller2.getSpan().context().traceId()).isEqualTo(expectedTraceId); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + static class MyConfig { + + @Bean + TestController controller(Tracer tracer) { + return new TestController(tracer); + } + + } + + @Controller + @MessageMapping("api.c2") + static class TestController { + + final Tracer tracer; + + Span span; + + ContextView interceptedContext; + + BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); + + TestController(Tracer tracer) { + this.tracer = tracer; + } + + BlockingQueue getReceivedFrames() { + return this.receivedFrames; + } + + Span getSpan() { + return this.span; + } + + void reset() { + this.span = null; + } + + @MessageMapping("fnf") + Mono testFnf() { + + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_FNF); + return Mono.empty(); + }); + } + + @MessageMapping("rr") + Mono testRR() { + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_RESPONSE); + return Mono.just("response"); + }); + } + + @MessageMapping("rs") + Flux testRS() { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_STREAM); + return Flux.just("stream"); + }); + } + + @MessageMapping("rc") + Flux testRC(@Payload Flux inbound) { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_CHANNEL); + return inbound; + }); + } + + } + +} From fc0a719d12517c62a0b21a095ea7705c29ec28a9 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 30 Apr 2021 14:12:16 +0200 Subject: [PATCH 16/59] Added batch instrumentation fixes gh-1904 --- docs/src/main/asciidoc/integrations.adoc | 11 +- spring-cloud-sleuth-autoconfigure/pom.xml | 10 + .../batch/TraceBatchAutoConfiguration.java | 57 ++++++ ...aceJobBuilderFactoryBeanPostProcessor.java | 47 +++++ ...ceStepBuilderFactoryBeanPostProcessor.java | 47 +++++ .../TraceRSocketAutoConfiguration.java | 176 +++++++++--------- ...itional-spring-configuration-metadata.json | 6 + .../main/resources/META-INF/spring.factories | 1 + spring-cloud-sleuth-instrumentation/pom.xml | 5 + .../batch/TraceJobBuilderFactory.java | 56 ++++++ .../batch/TraceJobExecutionListener.java | 68 +++++++ .../batch/TraceStepBuilderFactory.java | 56 ++++++ .../batch/TraceStepExecutionListener.java | 72 +++++++ tests/brave/pom.xml | 151 +++++++-------- .../pom.xml | 90 +++++++++ .../batch/BatchIntegrationTests.java | 54 ++++++ .../src/test/resources/application.yml | 5 + tests/common/pom.xml | 5 + .../batch/BatchIntegrationTests.java | 102 ++++++++++ .../reactor/sample/FlatMapTests.java | 2 +- 20 files changed, 855 insertions(+), 166 deletions(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceBatchAutoConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceJobBuilderFactoryBeanPostProcessor.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceStepBuilderFactoryBeanPostProcessor.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceJobBuilderFactory.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceJobExecutionListener.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceStepBuilderFactory.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceStepExecutionListener.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-batch-tests/pom.xml create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-batch-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/batch/BatchIntegrationTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-batch-tests/src/test/resources/application.yml create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/batch/BatchIntegrationTests.java diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 322c2ff23..8cfa25c70 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -584,11 +584,18 @@ This feature is available for all tracer implementations. If you have Spring Cloud Deployer running on the classpath, we wrap the `AppDeployer` in a trace representation. We are polling the application for its status at a default interval. You can change that default by setting the `spring.sleuth.deployer.status-poll-delay` property. In order to disable this instrumentation set `spring.sleuth.deployer.enabled` to `false`. - -[[sleuth-deployer-integration]] +[[sleuth-rsocket-integration]] == Spring RSocket This feature is available for all tracer implementations. If you have Spring RSocket running on the classpath, we wrap the inbound and outbound communication to propagate the tracing context via the metadata. In order to disable this instrumentation set `spring.sleuth.rsocket.enabled` to `false`. + +[[sleuth-batch-integration]] +== Spring Batch + +This feature is available for all tracer implementations. + +If you have Spring RSocket running on the classpath, we wrap the inbound and outbound communication to propagate the tracing context via the metadata. +In order to disable this instrumentation set `spring.sleuth.batch.enabled` to `false`. diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index 7f314a3b0..1c74bedd6 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -93,6 +93,11 @@ spring-boot-starter-websocket true + + org.springframework.boot + spring-boot-starter-batch + true + org.springframework.cloud spring-cloud-stream @@ -433,6 +438,11 @@ archunit-junit5 test + + com.h2database + h2 + test + diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceBatchAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceBatchAutoConfiguration.java new file mode 100644 index 000000000..8527d90e5 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceBatchAutoConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.batch; + +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Spring Batch. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(JobBuilderFactory.class) +@ConditionalOnBean(Tracer.class) +@ConditionalOnProperty(value = "spring.sleuth.batch.enabled", matchIfMissing = true) +@AutoConfigureAfter(BraveAutoConfiguration.class) +@AutoConfigureBefore(BatchAutoConfiguration.class) +public class TraceBatchAutoConfiguration { + + @Bean + static TraceJobBuilderFactoryBeanPostProcessor traceJobBuilderFactoryBeanPostProcessor(BeanFactory beanFactory) { + return new TraceJobBuilderFactoryBeanPostProcessor(beanFactory); + } + + @Bean + static TraceStepBuilderFactoryBeanPostProcessor traceStepBuilderFactoryBeanPostProcessor(BeanFactory beanFactory) { + return new TraceStepBuilderFactoryBeanPostProcessor(beanFactory); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceJobBuilderFactoryBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceJobBuilderFactoryBeanPostProcessor.java new file mode 100644 index 000000000..14d25dce9 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceJobBuilderFactoryBeanPostProcessor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.batch; + +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.sleuth.instrument.batch.TraceJobBuilderFactory; + +/** + * Bean post processor for {@link JobBuilderFactory}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceJobBuilderFactoryBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + public TraceJobBuilderFactoryBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof JobBuilderFactory && !(bean instanceof TraceJobBuilderFactory)) { + return new TraceJobBuilderFactory(this.beanFactory, (JobBuilderFactory) bean); + } + return bean; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceStepBuilderFactoryBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceStepBuilderFactoryBeanPostProcessor.java new file mode 100644 index 000000000..cb36096b7 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceStepBuilderFactoryBeanPostProcessor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.batch; + +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.sleuth.instrument.batch.TraceStepBuilderFactory; + +/** + * Bean post processor for {@link StepBuilderFactory}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceStepBuilderFactoryBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + public TraceStepBuilderFactoryBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof StepBuilderFactory && !(bean instanceof TraceStepBuilderFactory)) { + return new TraceStepBuilderFactory(this.beanFactory, (StepBuilderFactory) bean); + } + return bean; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java index cd56303f9..b92d88cc4 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.java @@ -1,88 +1,88 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.rsocket; - -import java.util.List; - -import io.rsocket.RSocket; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration; -import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.rsocket.server.RSocketServerCustomizer; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.rsocket.TracingRSocketConnectorConfigurer; -import org.springframework.cloud.sleuth.instrument.rsocket.TracingRSocketServerCustomizer; -import org.springframework.cloud.sleuth.propagation.Propagator; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Scope; -import org.springframework.messaging.rsocket.RSocketConnectorConfigurer; -import org.springframework.messaging.rsocket.RSocketRequester; -import org.springframework.messaging.rsocket.RSocketRequester.Builder; -import org.springframework.messaging.rsocket.RSocketStrategies; - -@Configuration(proxyBeanMethods = false) -@ConditionalOnBean(Tracer.class) -@ConditionalOnProperty(value = "spring.sleuth.rsocket.enabled", matchIfMissing = true) -@ConditionalOnClass({ RSocket.class, RSocketStrategies.class }) -@AutoConfigureAfter(BraveAutoConfiguration.class) -@AutoConfigureBefore({ RSocketRequesterAutoConfiguration.class, RSocketServerAutoConfiguration.class }) -@EnableConfigurationProperties(SleuthRSocketProperties.class) -public class TraceRSocketAutoConfiguration { - - // We're using text instead of objects cause we can have same properties from Brave / - // OTel - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - Builder rSocketRequesterBuilder(RSocketStrategies strategies, - ObjectProvider connectorConfigurerProvider) { - // TODO: should be in spring boot - final Builder builder = RSocketRequester.builder().rsocketStrategies(strategies); - connectorConfigurerProvider.forEach(builder::rsocketConnector); - return builder; - } - - private boolean containsZipkinPropagationType(List types) { - return types.stream().anyMatch(s -> s.equalsIgnoreCase("b3")); - } - - @Bean - RSocketConnectorConfigurer tracingRSocketConnectorConfigurer(Propagator propagator, Tracer tracer, - @Value("${spring.sleuth.propagation.type:B3}") List types) { - return new TracingRSocketConnectorConfigurer(propagator, tracer, containsZipkinPropagationType(types)); - } - - // We're using text instead of objects cause we can have same properties from Brave / - // OTel - @Bean - RSocketServerCustomizer tracingRSocketServerCustomizer(Propagator propagator, Tracer tracer, - @Value("${spring.sleuth.propagation.type:B3}") List types) { - return new TracingRSocketServerCustomizer(propagator, tracer, containsZipkinPropagationType(types)); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.rsocket; + +import java.util.List; + +import io.rsocket.RSocket; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration; +import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.rsocket.server.RSocketServerCustomizer; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.rsocket.TracingRSocketConnectorConfigurer; +import org.springframework.cloud.sleuth.instrument.rsocket.TracingRSocketServerCustomizer; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.messaging.rsocket.RSocketConnectorConfigurer; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketRequester.Builder; +import org.springframework.messaging.rsocket.RSocketStrategies; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(Tracer.class) +@ConditionalOnProperty(value = "spring.sleuth.rsocket.enabled", matchIfMissing = true) +@ConditionalOnClass({ RSocket.class, RSocketStrategies.class }) +@AutoConfigureAfter(BraveAutoConfiguration.class) +@AutoConfigureBefore({ RSocketRequesterAutoConfiguration.class, RSocketServerAutoConfiguration.class }) +@EnableConfigurationProperties(SleuthRSocketProperties.class) +public class TraceRSocketAutoConfiguration { + + // We're using text instead of objects cause we can have same properties from Brave / + // OTel + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + Builder rSocketRequesterBuilder(RSocketStrategies strategies, + ObjectProvider connectorConfigurerProvider) { + // TODO: should be in spring boot + final Builder builder = RSocketRequester.builder().rsocketStrategies(strategies); + connectorConfigurerProvider.forEach(builder::rsocketConnector); + return builder; + } + + private boolean containsZipkinPropagationType(List types) { + return types.stream().anyMatch(s -> s.equalsIgnoreCase("b3")); + } + + @Bean + RSocketConnectorConfigurer tracingRSocketConnectorConfigurer(Propagator propagator, Tracer tracer, + @Value("${spring.sleuth.propagation.type:B3}") List types) { + return new TracingRSocketConnectorConfigurer(propagator, tracer, containsZipkinPropagationType(types)); + } + + // We're using text instead of objects cause we can have same properties from Brave / + // OTel + @Bean + RSocketServerCustomizer tracingRSocketServerCustomizer(Propagator propagator, Tracer tracer, + @Value("${spring.sleuth.propagation.type:B3}") List types) { + return new TracingRSocketServerCustomizer(propagator, tracer, containsZipkinPropagationType(types)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index ef1e667b5..fef960fe4 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -154,6 +154,12 @@ "type": "java.lang.Boolean", "description": "Enable Spring Cloud Config Server instrumentation.", "defaultValue": true + }, + { + "name": "spring.sleuth.batch.enabled", + "type": "java.lang.Boolean", + "description": "Enable Spring Batch instrumentation.", + "defaultValue": true } ] } diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index 91d031660..fff9ab35e 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -3,6 +3,7 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncCustomAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncDefaultAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.batch.TraceBatchAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.config.TraceSpringCloudConfigAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.circuitbreaker.TraceCircuitBreakerAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.deployer.TraceDeployerAutoConfiguration,\ diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index 3b2d48a01..7252a853b 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -107,6 +107,11 @@ spring-boot-starter-websocket true + + org.springframework.boot + spring-boot-starter-batch + true + org.springframework.cloud spring-cloud-stream diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceJobBuilderFactory.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceJobBuilderFactory.java new file mode 100644 index 000000000..0aae8cdec --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceJobBuilderFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.batch; + +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.Tracer; + +/** + * StepBuilderFactory adding {@link TraceJobExecutionListener}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceJobBuilderFactory extends JobBuilderFactory { + + private final BeanFactory beanFactory; + + private final JobBuilderFactory delegate; + + private Tracer tracer; + + public TraceJobBuilderFactory(BeanFactory beanFactory, JobBuilderFactory delegate) { + super(null); + this.beanFactory = beanFactory; + this.delegate = delegate; + } + + @Override + public JobBuilder get(String name) { + return this.delegate.get(name).listener(new TraceJobExecutionListener(tracer())); + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceJobExecutionListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceJobExecutionListener.java new file mode 100644 index 000000000..c53edc5a9 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceJobExecutionListener.java @@ -0,0 +1,68 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.batch; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.Tracer; + +class TraceJobExecutionListener implements JobExecutionListener { + + private final Tracer tracer; + + private static Map SPANS = new ConcurrentHashMap<>(); + + TraceJobExecutionListener(Tracer tracer) { + this.tracer = tracer; + } + + @Override + public void beforeJob(JobExecution jobExecution) { + Span span = this.tracer.nextSpan().name(jobExecution.getJobInstance().getJobName()); + Tracer.SpanInScope spanInScope = this.tracer.withSpan(span.start()); + SPANS.put(jobExecution, new SpanAndScope(span, spanInScope)); + } + + @Override + public void afterJob(JobExecution jobExecution) { + SpanAndScope spanAndScope = SPANS.remove(jobExecution); + List throwables = jobExecution.getFailureExceptions(); + Span span = spanAndScope.getSpan(); + span.tag("batch.job.name", jobExecution.getJobInstance().getJobName()); + span.tag("batch.job.instanceId", String.valueOf(jobExecution.getJobInstance().getInstanceId())); + span.tag("batch.job.executionId", String.valueOf(jobExecution.getId())); + Tracer.SpanInScope scope = spanAndScope.getScope(); + if (!throwables.isEmpty()) { + span.error(mergedThrowables(throwables)); + } + span.end(); + scope.close(); + } + + private IllegalStateException mergedThrowables(List throwables) { + return new IllegalStateException( + throwables.stream().map(Throwable::toString).collect(Collectors.joining("\n"))); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceStepBuilderFactory.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceStepBuilderFactory.java new file mode 100644 index 000000000..d4ecb9481 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceStepBuilderFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.batch; + +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.Tracer; + +/** + * StepBuilderFactory adding {@link TraceStepExecutionListener}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceStepBuilderFactory extends StepBuilderFactory { + + private final BeanFactory beanFactory; + + private final StepBuilderFactory delegate; + + private Tracer tracer; + + public TraceStepBuilderFactory(BeanFactory beanFactory, StepBuilderFactory delegate) { + super(null, null); + this.beanFactory = beanFactory; + this.delegate = delegate; + } + + @Override + public StepBuilder get(String name) { + return this.delegate.get(name).listener(new TraceStepExecutionListener(tracer())); + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceStepExecutionListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceStepExecutionListener.java new file mode 100644 index 000000000..185e7c553 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceStepExecutionListener.java @@ -0,0 +1,72 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.batch; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.Tracer; + +class TraceStepExecutionListener implements StepExecutionListener { + + private final Tracer tracer; + + private static Map SPANS = new ConcurrentHashMap<>(); + + TraceStepExecutionListener(Tracer tracer) { + this.tracer = tracer; + } + + @Override + public void beforeStep(StepExecution stepExecution) { + Span span = this.tracer.nextSpan().name(stepExecution.getStepName()); + Tracer.SpanInScope spanInScope = this.tracer.withSpan(span.start()); + SPANS.put(stepExecution, new SpanAndScope(span, spanInScope)); + } + + @Override + public ExitStatus afterStep(StepExecution stepExecution) { + SpanAndScope spanAndScope = SPANS.remove(stepExecution); + List throwables = stepExecution.getFailureExceptions(); + Span span = spanAndScope.getSpan(); + span.tag("batch.step.name", stepExecution.getStepName()); + span.tag("batch.job.executionId", String.valueOf(stepExecution.getJobExecutionId())); + span.tag("batch.step.executionId", String.valueOf(stepExecution.getId())); + span.tag("batch.step.type", stepExecution.getExecutionContext().getString(Step.STEP_TYPE_KEY)); + Tracer.SpanInScope scope = spanAndScope.getScope(); + if (!throwables.isEmpty()) { + span.error(mergedThrowables(throwables)); + } + span.end(); + scope.close(); + return stepExecution.getExitStatus(); + } + + private IllegalStateException mergedThrowables(List throwables) { + return new IllegalStateException( + throwables.stream().map(Throwable::toString).collect(Collectors.joining("\n"))); + } + +} diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index b742ec657..23f1b8daa 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -1,75 +1,76 @@ - - - - - 4.0.0 - - spring-cloud-sleuth-tests-brave - pom - Spring Cloud Sleuth Brave Tests - Spring Cloud Sleuth Brave Tests - - - org.springframework.cloud - spring-cloud-sleuth-tests - 3.1.0-SNAPSHOT - .. - - - - spring-cloud-sleuth-instrumentation-annotation-tests - spring-cloud-sleuth-instrumentation-async-tests - spring-cloud-sleuth-instrumentation-baggage-tests - spring-cloud-sleuth-instrumentation-config-server-tests - spring-cloud-sleuth-instrumentation-circuitbreaker-tests - spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests - spring-cloud-sleuth-instrumentation-feign-tests - spring-cloud-sleuth-instrumentation-gateway-tests - spring-cloud-sleuth-instrumentation-grpc-tests - spring-cloud-sleuth-instrumentation-kafka-tests - spring-cloud-sleuth-instrumentation-lettuce-tests - spring-cloud-sleuth-instrumentation-messaging-tests - spring-cloud-sleuth-instrumentation-mvc-tests - spring-cloud-sleuth-instrumentation-quartz-tests - spring-cloud-sleuth-instrumentation-reactor-tests - spring-cloud-sleuth-instrumentation-rxjava-tests - spring-cloud-sleuth-instrumentation-scheduling-tests - spring-cloud-sleuth-instrumentation-task-tests - spring-cloud-sleuth-instrumentation-webflux-tests - spring-cloud-sleuth-instrumentation-rsocket-tests - spring-cloud-sleuth-zipkin-tests - - - - - - - - maven-deploy-plugin - - true - - - - - - - + + + + + 4.0.0 + + spring-cloud-sleuth-tests-brave + pom + Spring Cloud Sleuth Brave Tests + Spring Cloud Sleuth Brave Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests + 3.1.0-SNAPSHOT + .. + + + + spring-cloud-sleuth-instrumentation-annotation-tests + spring-cloud-sleuth-instrumentation-async-tests + spring-cloud-sleuth-instrumentation-baggage-tests + spring-cloud-sleuth-instrumentation-batch-tests + spring-cloud-sleuth-instrumentation-config-server-tests + spring-cloud-sleuth-instrumentation-circuitbreaker-tests + spring-cloud-sleuth-instrumentation-circuitbreaker-reactive-tests + spring-cloud-sleuth-instrumentation-feign-tests + spring-cloud-sleuth-instrumentation-gateway-tests + spring-cloud-sleuth-instrumentation-grpc-tests + spring-cloud-sleuth-instrumentation-kafka-tests + spring-cloud-sleuth-instrumentation-lettuce-tests + spring-cloud-sleuth-instrumentation-messaging-tests + spring-cloud-sleuth-instrumentation-mvc-tests + spring-cloud-sleuth-instrumentation-quartz-tests + spring-cloud-sleuth-instrumentation-reactor-tests + spring-cloud-sleuth-instrumentation-rxjava-tests + spring-cloud-sleuth-instrumentation-scheduling-tests + spring-cloud-sleuth-instrumentation-task-tests + spring-cloud-sleuth-instrumentation-webflux-tests + spring-cloud-sleuth-instrumentation-rsocket-tests + spring-cloud-sleuth-zipkin-tests + + + + + + + + maven-deploy-plugin + + true + + + + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-batch-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-batch-tests/pom.xml new file mode 100644 index 000000000..8b066d3ad --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-batch-tests/pom.xml @@ -0,0 +1,90 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-batch-tests + jar + Spring Cloud Sleuth Brave Batch Instrumentation Tests + Spring Cloud Sleuth Brave Batch Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + + + + org.springframework.cloud + spring-cloud-sleuth-tests-common + ${project.version} + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-batch + + + com.h2database + h2 + runtime + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-test + + + io.zipkin.brave + brave-tests + + + org.awaitility + awaitility + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-batch-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/batch/BatchIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-batch-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/batch/BatchIntegrationTests.java new file mode 100644 index 000000000..917e9d8be --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-batch-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/batch/BatchIntegrationTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.batch; + +import brave.sampler.Sampler; + +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +@SpringBootTest +@ContextConfiguration(classes = BatchIntegrationTests.Config.class) +public class BatchIntegrationTests extends org.springframework.cloud.sleuth.instrument.batch.BatchIntegrationTests { + + @Configuration(proxyBeanMethods = false) + @EnableBatchProcessing + static class Config { + + @Bean + TestSpanHandler testSpanHandlerSupplier(brave.test.TestSpanHandler testSpanHandler) { + return new BraveTestSpanHandler(testSpanHandler); + } + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + brave.test.TestSpanHandler braveTestSpanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-batch-tests/src/test/resources/application.yml b/tests/brave/spring-cloud-sleuth-instrumentation-batch-tests/src/test/resources/application.yml new file mode 100644 index 000000000..6d58539e0 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-batch-tests/src/test/resources/application.yml @@ -0,0 +1,5 @@ +logging.level.org.springframework.cloud: DEBUG +logging.level.com.netflix.discovery.InstanceInfoReplicator: ERROR +logging.level.org.springframework.cloud.sleuth.brave.instrument.web.client.feign: TRACE + +spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration diff --git a/tests/common/pom.xml b/tests/common/pom.xml index 7c1fb98de..a4f029dc5 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -64,6 +64,11 @@ spring-boot-starter-websocket true + + org.springframework.boot + spring-boot-starter-batch + true + org.springframework.boot spring-boot-starter-actuator diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/batch/BatchIntegrationTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/batch/BatchIntegrationTests.java new file mode 100644 index 000000000..04b615415 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/batch/BatchIntegrationTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.batch; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.BDDAssertions.then; + +@ContextConfiguration(classes = BatchIntegrationTests.TestConfig.class) +public abstract class BatchIntegrationTests { + + private static final Log log = LogFactory.getLog(BatchIntegrationTests.class); + + @Autowired + TestSpanHandler spans; + + @Autowired + Tracer tracer; + + @Autowired + StepBuilderFactory stepBuilderFactory; + + @Autowired + JobBuilderFactory jobBuilderFactory; + + @Autowired + JobLauncher jobLauncher; + + @BeforeEach + public void setup() { + this.spans.clear(); + } + + @Test + public void should_pass_tracing_information_when_using_batch() throws Exception { + AtomicReference spanFromTasklet = new AtomicReference<>(); + Job job = this.jobBuilderFactory.get("myJob") + .start(this.stepBuilderFactory.get("myTask").tasklet((stepContribution, chunkContext) -> { + log.info("Hello"); + spanFromTasklet.set(this.tracer.currentSpan()); + return RepeatStatus.FINISHED; + }).build()).build(); + + JobExecution jobExecution = this.jobLauncher.run(job, new JobParameters()); + + then(jobExecution.getExitStatus().getExitCode()).isEqualTo(ExitStatus.COMPLETED.getExitCode()); + then(spanFromTasklet.get()).isNotNull(); + List spans = this.spans.reportedSpans(); + then(spans).hasSize(2); + then(spans.stream().map(FinishedSpan::getTraceId).collect(Collectors.toSet())).hasSize(1); + then(spans.get(0).getName()).isEqualTo("myTask"); + then(spans.get(1).getName()).isEqualTo("myJob"); + then(this.tracer.currentSpan()).isNull(); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @EnableBatchProcessing + public static class TestConfig { + + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/sample/FlatMapTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/sample/FlatMapTests.java index 8288389e5..31bf1e94a 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/sample/FlatMapTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/sample/FlatMapTests.java @@ -143,7 +143,7 @@ public abstract class FlatMapTests { sender.port = port; spans.clear(); - Awaitility.await().atMost(5, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { + Awaitility.await().atMost(15, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { // when LOGGER.info("Start"); spans.clear(); From fffe64c1d3a9004201b58fc00d4b8a2dba25b4db Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 30 Apr 2021 14:26:48 +0200 Subject: [PATCH 17/59] Polish --- .../batch/TraceBatchAutoConfiguration.java | 111 +++++++++--------- 1 file changed, 54 insertions(+), 57 deletions(-) diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceBatchAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceBatchAutoConfiguration.java index 8527d90e5..5ede393d1 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceBatchAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceBatchAutoConfiguration.java @@ -1,57 +1,54 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.batch; - -import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration - * Auto-configuration} that registers instrumentation for Spring Batch. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(JobBuilderFactory.class) -@ConditionalOnBean(Tracer.class) -@ConditionalOnProperty(value = "spring.sleuth.batch.enabled", matchIfMissing = true) -@AutoConfigureAfter(BraveAutoConfiguration.class) -@AutoConfigureBefore(BatchAutoConfiguration.class) -public class TraceBatchAutoConfiguration { - - @Bean - static TraceJobBuilderFactoryBeanPostProcessor traceJobBuilderFactoryBeanPostProcessor(BeanFactory beanFactory) { - return new TraceJobBuilderFactoryBeanPostProcessor(beanFactory); - } - - @Bean - static TraceStepBuilderFactoryBeanPostProcessor traceStepBuilderFactoryBeanPostProcessor(BeanFactory beanFactory) { - return new TraceStepBuilderFactoryBeanPostProcessor(beanFactory); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.batch; + +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Spring Batch. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(JobBuilderFactory.class) +@ConditionalOnBean(Tracer.class) +@ConditionalOnProperty(value = "spring.sleuth.batch.enabled", matchIfMissing = true) +@AutoConfigureAfter(BraveAutoConfiguration.class) +public class TraceBatchAutoConfiguration { + + @Bean + static TraceJobBuilderFactoryBeanPostProcessor traceJobBuilderFactoryBeanPostProcessor(BeanFactory beanFactory) { + return new TraceJobBuilderFactoryBeanPostProcessor(beanFactory); + } + + @Bean + static TraceStepBuilderFactoryBeanPostProcessor traceStepBuilderFactoryBeanPostProcessor(BeanFactory beanFactory) { + return new TraceStepBuilderFactoryBeanPostProcessor(beanFactory); + } + +} From efe08e68b40fc4213af4addac11194b267ab6542 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 30 Apr 2021 14:33:54 +0200 Subject: [PATCH 18/59] Updated docs --- docs/src/main/asciidoc/integrations.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 8cfa25c70..9a67a7807 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -597,5 +597,5 @@ In order to disable this instrumentation set `spring.sleuth.rsocket.enabled` to This feature is available for all tracer implementations. -If you have Spring RSocket running on the classpath, we wrap the inbound and outbound communication to propagate the tracing context via the metadata. +If you have Spring Batch running on the classpath, we wrap the `StepBuilderFactory` and the `JobBuilderFactory` to propagate the tracing context. In order to disable this instrumentation set `spring.sleuth.batch.enabled` to `false`. From 4fef057e96d950729a48d7dad319824c577efaf9 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 6 May 2021 09:54:25 +0200 Subject: [PATCH 19/59] Added a ZipkinRestTemplateProvider to allow customization of the rest template; fixes gh-1932 --- docs/src/main/asciidoc/project-features.adoc | 986 +++++++++--------- .../batch/TraceBatchAutoConfiguration.java | 108 +- ...ZipkinRestTemplateSenderConfiguration.java | 15 +- ...nRestTemplateSenderConfigurationTests.java | 73 ++ .../zipkin2/ZipkinRestTemplateProvider.java | 31 + 5 files changed, 659 insertions(+), 554 deletions(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfigurationTests.java create mode 100644 spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinRestTemplateProvider.java diff --git a/docs/src/main/asciidoc/project-features.adoc b/docs/src/main/asciidoc/project-features.adoc index fb84d25c7..a84029762 100644 --- a/docs/src/main/asciidoc/project-features.adoc +++ b/docs/src/main/asciidoc/project-features.adoc @@ -1,494 +1,492 @@ -[[features]] -[[project-features]] -= Spring Cloud Sleuth Features - -include::_attributes.adoc[] - -This section dives into the details of {project-full-name}. -Here you can learn about the key features that you may want to use and customize. -If you have not already done so, you might want to read the "<>" and "<>" sections, so that you have a good grounding in the basics. - -[[features-context-propagation]] -== Context Propagation - -Traces connect from service to service using header propagation. -The default format is https://github.com/openzipkin/b3-propagation[B3]. -Similar to data formats, you can configure alternate header formats also, provided trace and span IDs are compatible with B3. Most notably, this means the trace ID and span IDs are lower-case hex, not UUIDs. -Besides trace identifiers, other properties (Baggage) can also be passed along with the request. -Remote Baggage must be predefined, but is flexible otherwise. - -To use the provided defaults you can set the `spring.sleuth.propagation.type` property. -The value can be a list in which case you will propagate more tracing headers. - -For Brave we support `AWS`, `B3`, `W3C` propagation types. - -You can read more about how to provide custom context propagation in this "<>". - -[[features-sampling]] -== Sampling - -Spring Cloud Sleuth pushes the sampling decision down to the tracer implementation. -However, there are cases where you can change the sampling decision at runtime. - -One of such cases is skip reporting of certain client spans. -To achieve that you can set the `spring.sleuth.web.client.skip-pattern` with the path patterns to be skipped. -Another option is to provide your own custom `org.springframework.cloud.sleuth.SamplerFunction<`org.springframework.cloud.sleuth.http.HttpRequest>` implementation and define when a given `HttpRequest` should not be sampled. - -[[features-baggage]] -== Baggage - -Distributed tracing works by propagating fields inside and across services that connect the trace together: traceId and spanId notably. -The context that holds these fields can optionally push other fields that need to be consistent regardless of many services are touched. -The simple name for these extra fields is "Baggage". - -Sleuth allows you to define which baggage are permitted to exist in the trace context, including what header names are used. - -The following example shows setting baggage values using Spring Cloud Sleuth's API: - -[source,java,indent=0] ----- -include::{common_tests_path}/src/main/java/org/springframework/cloud/sleuth/baggage/multiple/MultipleHopsIntegrationTests.java[tags=baggage,indent=0] ----- - -IMPORTANT: There is currently no limitation of the count or size of baggage items. -Keep in mind that too many can decrease system throughput or increase RPC latency. -In extreme cases, too much baggage can crash the application, due to exceeding transport-level message or header capacity. - -You can use properties to define fields that have no special configuration such as name mapping: - -* `spring.sleuth.baggage.remote-fields` is a list of header names to accept and propagate to remote services. -* `spring.sleuth.baggage.local-fields` is a list of names to propagate locally - -No prefixing applies with these keys. -What you set is literally what is used. - -A name set in either of these properties will result in a `Baggage` of the same name. - -In order to automatically set the baggage values to Slf4j's MDC, you have to set the `spring.sleuth.baggage.correlation-fields` property with a list of allowed local or remote keys. E.g. `spring.sleuth.baggage.correlation-fields=country-code` will set the value of the `country-code` baggage into MDC. - -Note that the extra field is propagated and added to MDC starting with the next downstream trace context. -To immediately add the extra field to MDC in the current trace context, configure the field to flush on update: - -``` -// configuration -@Bean -BaggageField countryCodeField() { - return BaggageField.create("country-code"); -} - -@Bean -ScopeDecorator mdcScopeDecorator() { - return MDCScopeDecorator.newBuilder() - .clear() - .add(SingleCorrelationField.newBuilder(countryCodeField()) - .flushOnUpdate() - .build()) - .build(); -} - -// service -@Autowired -BaggageField countryCodeField; - -countryCodeField.updateValue("new-value"); -``` - -IMPORTANT: Remember that adding entries to MDC can drastically decrease the performance of your application! - -If you want to add the baggage entries as tags, to make it possible to search for spans via the baggage entries, you can set the value of -`spring.sleuth.baggage.tag-fields` with a list of allowed baggage keys. -To disable the feature you have to pass the `spring.sleuth.propagation.tag.enabled=false` property. - -[[features-baggage-vs-tags]] -=== Baggage versus Tags - -Like trace IDs, Baggage is attached to messages or requests, usually as headers. -Tags are key value pairs sent in a Span to Zipkin. -Baggage values are not added spans by default, which means you can't search based on Baggage unless you opt-in. - -To make baggage also tags, use the property `spring.sleuth.baggage.tag-fields` -like so: - -[source,yml] ----- -include::{brave_path}/src/test/resources/application-baggage.yml[indent=0] ----- - -[[features-brave]] -== OpenZipkin Brave Tracer Integration - -Spring Cloud Sleuth integrates with the OpenZipkin Brave tracer via the bridge that is available in the `spring-cloud-sleuth-brave` module. -In this section you can read about specific Brave integrations. - -You can choose to use either Sleuth's API or the Brave API directly in your code (e.g. either Sleuth's `Tracer` or Brave's `Tracer`). -If you want to use this tracer implementation's API directly please read https://github.com/openzipkin/brave[their documentation to learn more about it]. - -[[features-brave-basics]] -=== Brave Basics - -Here are the most core types you might use: - -* `brave.SpanCustomizer` - to change the span currently in progress -* `brave.Tracer` - to get a start new spans ad-hoc - -Here are the most relevant links from the OpenZipkin Brave project: - -* https://github.com/openzipkin/brave/tree/master/brave[Brave's core library] -* https://github.com/openzipkin/brave/tree/master/brave#baggage[Baggage (propagated fields)] -* https://github.com/openzipkin/brave/tree/master/instrumentation/http[HTTP tracing] - -[[features-brave-sampling]] -=== Brave Sampling - -Sampling only applies to tracing backends, such as Zipkin. -Trace IDs appear in logs regardless of sample rate. -Sampling is a way to prevent overloading the system, by consistently tracing some, but not all requests. - -The default rate of 10 traces per second is controlled by the `spring.sleuth.sampler.rate` -property and applies when we know Sleuth is used for reasons besides logging. -Use a rate above 100 traces per second with extreme caution as it can overload your tracing system. - -The sampler can be set by Java Config also, as shown in the following example: - -[source,java,indent=0] ----- -include::{brave_path}/src/test/java/org/springframework/cloud/sleuth/brave/SpringCloudSleuthDocTests.java[tags=always_sampler,indent=0] ----- - -TIP: You can set the HTTP header `b3` to `1`, or, when doing messaging, you can set the `spanFlags` header to `1`. -Doing so forces the current request to be sampled regardless of configuration. - -By default samplers will work with the refresh scope mechanism. -That means that you can change the sampling properties at runtime, refresh the application and the changes will be reflected. -However, sometimes the fact of creating a proxy around samplers and calling it from too early (from `@PostConstruct` annotated method) may lead to dead locks. -In such a case either create a sampler bean explicitly, or set the property `spring.sleuth.sampler.refresh.enabled` to `false` to disable the refresh scope support. - -[[features-brave-baggage]] -=== Brave Baggage Java configuration - -If you need to do anything more advanced than above, do not define properties and instead use a -`@Bean` config for the baggage fields you use. - -* `BaggagePropagationCustomizer` sets up baggage fields -* Add a `SingleBaggageField` to control header names for a `Baggage`. -* `CorrelationScopeCustomizer` sets up MDC fields -* Add a `SingleCorrelationField` to change the MDC name of a `Baggage` or if updates flush. - -[[features-brave-customizations]] -=== Brave Customizations - -The `brave.Tracer` object is fully managed by sleuth, so you rarely need to affect it. -That said, Sleuth supports a number of `Customizer` types, that allow you to configure anything not already done by Sleuth with auto-configuration or properties. - -If you define one of the following as a `Bean`, Sleuth will invoke it to customize behaviour: - -* `RpcTracingCustomizer` - for RPC tagging and sampling policy -* `HttpTracingCustomizer` - for HTTP tagging and sampling policy -* `MessagingTracingCustomizer` - for messaging tagging and sampling policy -* `CurrentTraceContextCustomizer` - to integrate decorators such as correlation. -* `BaggagePropagationCustomizer` - for propagating baggage fields in process and over headers -* `CorrelationScopeDecoratorCustomizer` - for scope decorations such as MDC (logging) field correlation - -[[features-brave-sampling-customizations]] -==== Brave Sampling Customizations - -If client /server sampling is required, just register a bean of type -`brave.sampler.SamplerFunction` and name the bean -`sleuthHttpClientSampler` for client sampler and `sleuthHttpServerSampler` -for server sampler. - -For your convenience the `@HttpClientSampler` and `@HttpServerSampler` -annotations can be used to inject the proper beans or to reference the bean names via their static String `NAME` fields. - -Check out Brave's code to see an example of how to make a path-based sampler -https://github.com/openzipkin/brave/tree/master/instrumentation/http#sampling-policy - -If you want to completely rewrite the `HttpTracing` bean you can use the `SkipPatternProvider` -interface to retrieve the URL `Pattern` for spans that should be not sampled. -Below you can see an example of usage of `SkipPatternProvider` inside a server side, `Sampler`. - -[source,java,indent=0] ----- -@Configuration(proxyBeanMethods = false) - class Config { -include::{tests_path}/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceFilterWebIntegrationTests.java[tags=custom_server_sampler,indent=2] -} ----- - -[[features-brave-messaging]] -=== Brave Messaging - -Sleuth automatically configures the `MessagingTracing` bean which serves as a foundation for Messaging instrumentation such as Kafka or JMS. - -If a customization of producer / consumer sampling of messaging traces is required, just register a bean of type `brave.sampler.SamplerFunction` and name the bean `sleuthProducerSampler` for producer sampler and `sleuthConsumerSampler` -for consumer sampler. - -For your convenience the `@ProducerSampler` and `@ConsumerSampler` -annotations can be used to inject the proper beans or to reference the bean names via their static String `NAME` fields. - -Ex. -Here's a sampler that traces 100 consumer requests per second, except for the "alerts" channel. -Other requests will use a global rate provided by the -`Tracing` component. - -[source,java,indent=0] ----- -@Configuration(proxyBeanMethods = false) - class Config { -include::{autoconfig_path}/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/messaging/BraveMessagingAutoConfigurationIntegrationTests.java[tags=custom_messaging_consumer_sampler,indent=2] -} ----- - -For more, see https://github.com/openzipkin/brave/tree/master/instrumentation/messaging#sampling-policy - -[[features-brave-opentracing]] -=== Brave Opentracing - -You can integrate with Brave and https://opentracing.io/[OpenTracing] via the -`io.opentracing.brave:brave-opentracing` bridge. -Just add it to the classpath and the OpenTracing `Tracer` will be set up automatically. - -[[features-zipkin]] -== Sending Spans to Zipkin - -Spring Cloud Sleuth provides various integrations with the https://zipkin.io[OpenZipkin] distributed tracing system. -Regardless of the chosen tracer implementation it's enough to add `spring-cloud-sleuth-zipkin` to the classpath to start sending spans to Zipkin. -You can choose whether to do that via HTTP or messaging. -You can read more about how to do that in "<>". - -When the span is closed, it is sent to Zipkin over HTTP. The communication is asynchronous. -You can configure the URL by setting the `spring.zipkin.baseUrl` property, as follows: - -[source,yaml] ----- -spring.zipkin.baseUrl: https://192.168.99.100:9411/ ----- - -If you want to find Zipkin through service discovery, you can pass the Zipkin's service ID inside the URL, as shown in the following example for `zipkinserver` service ID: - -[source,yaml] ----- -spring.zipkin.baseUrl: https://zipkinserver/ ----- - -To disable this feature just set `spring.zipkin.discovery-client-enabled` to `false`. - -When the Discovery Client feature is enabled, Sleuth uses -`LoadBalancerClient` to find the URL of the Zipkin Server. -It means that you can set up the load balancing configuration. - -If you have `web`, `rabbit`, `activemq` or `kafka` together on the classpath, you might need to pick the means by which you would like to send spans to zipkin. -To do so, set `web`, `rabbit`, `activemq` or `kafka` to the `spring.zipkin.sender.type` property. -The following example shows setting the sender type for `web`: - -[source,yaml] ----- -spring.zipkin.sender.type: web ----- - -To customize the `RestTemplate` that sends spans to Zipkin via HTTP, you can register the `ZipkinRestTemplateCustomizer` bean. - -[source,java,indent=0] ----- -@Configuration(proxyBeanMethods = false) - class MyConfig { - @Bean ZipkinRestTemplateCustomizer myCustomizer() { - return new ZipkinRestTemplateCustomizer() { - @Override - void customize(RestTemplate restTemplate) { - // customize the RestTemplate - } - }; - } -} ----- - -If, however, you would like to control the full process of creating the `RestTemplate` -object, you will have to create a bean of `zipkin2.reporter.Sender` type. - -[source,java,indent=0] ----- - @Bean Sender myRestTemplateSender(ZipkinProperties zipkin, - ZipkinRestTemplateCustomizer zipkinRestTemplateCustomizer) { - RestTemplate restTemplate = mySuperCustomRestTemplate(); - zipkinRestTemplateCustomizer.customize(restTemplate); - return myCustomSender(zipkin, restTemplate); - } ----- - -By default, api path will be set to `api/v2/spans` or `api/v1/spans` depending on the encoder version. If you want to use a custom api path, you can configure it using the following property (empty case, set ""): - -[source,yaml] ----- -spring.zipkin.api-path: v2/path2 ----- - -[[features-zipkin-custom-service-name]] -=== Custom service name - -By default, Sleuth assumes that, when you send a span to Zipkin, you want the span's service name to be equal to the value of the `spring.application.name` property. -That is not always the case, though. -There are situations in which you want to explicitly provide a different service name for all spans coming from your application. -To achieve that, you can pass the following property to your application to override that value (the example is for a service named `myService`): - -[source,yaml] ----- -spring.zipkin.service.name: myService ----- - -[[features-zipkin-host-locator]] -=== Host Locator - -IMPORTANT: This section is about defining *host* from service discovery. -It is *NOT* about finding Zipkin through service discovery. - -To define the host that corresponds to a particular span, we need to resolve the host name and port. -The default approach is to take these values from server properties. -If those are not set, we try to retrieve the host name from the network interfaces. - -If you have the discovery client enabled and prefer to retrieve the host address from the registered instance in a service registry, you have to set the `spring.zipkin.locator.discovery.enabled` property (it is applicable for both HTTP-based and Stream-based span reporting), as follows: - -[source,yaml] ----- -spring.zipkin.locator.discovery.enabled: true ----- - -[[features-zipkin-custom-reported-spans]] -=== Customization of Reported Spans - -In Sleuth, we generate spans with a fixed name. -Some users want to modify the name depending on values of tags. - -Sleuth registers a `SpanFilter` bean that can automatically skip reporting spans of given name patterns. -The property `spring.sleuth.span-filter.span-name-patterns-to-skip` contains the default skip patterns for span names. -The property `spring.sleuth.span-filter.additional-span-name-patterns-to-skip` will append the provided span name patterns to the existing ones. -In order to disable this functionality just set `spring.sleuth.span-filter.enabled` to `false`. - -[[features-zipkin-custom-reported-spans-brave]] -==== Brave Customization of Reported Spans - -IMPORTANT: This section is applicable for Brave tracer only. - -Before reporting spans (for example, to Zipkin) you may want to modify that span in some way. -You can do so by implementing a `SpanHandler`. - -The following example shows how to register two beans that implement `SpanHandler`: - -[source,java,indent=0] ----- -include::{autoconfig_path}/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/SpanHandlerTests.java[tags=spanHandler,indent=0] ----- - -The preceding example results in changing the name of the reported span to `foo bar`, just before it gets reported (for example, to Zipkin). - -=== Overriding the auto-configuration of Zipkin - -Spring Cloud Sleuth supports sending traces to multiple tracing systems as of version 2.1.0. In order to get this to work, every tracing system needs to have a `Reporter` and `Sender`. -If you want to override the provided beans you need to give them a specific name. -To do this you can use respectively `ZipkinAutoConfiguration.REPORTER_BEAN_NAME` and `ZipkinAutoConfiguration.SENDER_BEAN_NAME`. - -[source,java,indent=0] ----- -include::{common_tests_path}/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinAutoConfigurationTests.java[tags=override_default_beans,indent=0] ----- - -[[features-log-integration]] -== Log integration - -Sleuth configures the logging context with variables including the service name (`%{spring.zipkin.service.name}` or `%{spring.application.name}` if the previous one was not set), span ID (`%{spanId}`) and the trace ID (`%{traceId}`). -These help you connect logs with distributed traces and allow you choice in what tools you use to troubleshoot your services. - -Once you find any log with an error, you can look for the trace ID in the message. -Paste that into your distributed tracing system to visualize the entire trace, regardless of how many services the first request ended up hitting. - -[source] ----- -backend.log: 2020-04-09 17:45:40.516 ERROR [backend,5e8eeec48b08e26882aba313eb08f0a4,dcc1df555b5777b3] 97203 --- [nio-9000-exec-1] o.s.c.s.i.web.ExceptionLoggingFilter : Uncaught exception thrown -frontend.log:2020-04-09 17:45:40.574 ERROR [frontend,5e8eeec48b08e26882aba313eb08f0a4,82aba313eb08f0a4] 97192 --- [nio-8081-exec-2] o.s.c.s.i.web.ExceptionLoggingFilter : Uncaught exception thrown ----- - -Above, you'll notice the trace ID is `5e8eeec48b08e26882aba313eb08f0a4`, for example. -This log configuration was automatically setup by Sleuth. -You can disable it by disabling Sleuth via `spring.sleuth.enabled=false` property or putting your own `logging.pattern.level` property. - -If you use a log aggregating tool (such as https://www.elastic.co/products/kibana[Kibana], https://www.splunk.com/[Splunk], and others), you can order the events that took place. -An example from Kibana would resemble the following image: - -image::{github-raw}/docs/src/main/asciidoc/images/kibana.png[Log correlation with Kibana] - -If you want to use https://www.elastic.co/guide/en/logstash/current/index.html[Logstash], the following listing shows the Grok pattern for Logstash: - -[source] ----- -filter { - # pattern matching logback pattern - grok { - match => { "message" => "%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" } - } - date { - match => ["timestamp", "ISO8601"] - } - mutate { - remove_field => ["timestamp"] - } -} ----- - -NOTE: If you want to use Grok together with the logs from Cloud Foundry, you have to use the following pattern: - -[source] ----- -filter { - # pattern matching logback pattern - grok { - match => { "message" => "(?m)OUT\s+%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" } - } - date { - match => ["timestamp", "ISO8601"] - } - mutate { - remove_field => ["timestamp"] - } -} ----- - -[[features-log-integration-json-logback]] -=== JSON Logback with Logstash - -Often, you do not want to store your logs in a text file but in a JSON file that Logstash can immediately pick. -To do so, you have to do the following (for readability, we pass the dependencies in the `groupId:artifactId:version` notation). - -*Dependencies Setup* - -. Ensure that Logback is on the classpath (`ch.qos.logback:logback-core`). -. Add Logstash Logback encode. -For example, to use version `4.6`, add `net.logstash.logback:logstash-logback-encoder:4.6`. - -*Logback Setup* - -Consider the following example of a Logback configuration file (logback-spring.xml). - -[source,xml] ------ -include::{project-root}/docs/src/main/asciidoc/logback-spring.xml[] ------ - -That Logback configuration file: - -* Logs information from the application in a JSON format to a `build/${spring.application.name}.json` file. -* Has commented out two additional appenders: console and standard log file. -* Has the same logging pattern as the one presented in the previous section. - -NOTE: If you use a custom `logback-spring.xml`, you must pass the `spring.application.name` in the `bootstrap` rather than the `application` property file. -Otherwise, your custom logback file does not properly read the property. - -[[features-whats-next]] -== What to Read Next - -If you want to learn more about any of the classes discussed in this section, you can browse the -{github-code}[source code directly]. -If you have specific questions, see the -<> section. - -If you are comfortable with {project-full-name}'s core features, you can continue on and read about -<>. +[[features]] +[[project-features]] += Spring Cloud Sleuth Features + +include::_attributes.adoc[] + +This section dives into the details of {project-full-name}. +Here you can learn about the key features that you may want to use and customize. +If you have not already done so, you might want to read the "<>" and "<>" sections, so that you have a good grounding in the basics. + +[[features-context-propagation]] +== Context Propagation + +Traces connect from service to service using header propagation. +The default format is https://github.com/openzipkin/b3-propagation[B3]. +Similar to data formats, you can configure alternate header formats also, provided trace and span IDs are compatible with B3. Most notably, this means the trace ID and span IDs are lower-case hex, not UUIDs. +Besides trace identifiers, other properties (Baggage) can also be passed along with the request. +Remote Baggage must be predefined, but is flexible otherwise. + +To use the provided defaults you can set the `spring.sleuth.propagation.type` property. +The value can be a list in which case you will propagate more tracing headers. + +For Brave we support `AWS`, `B3`, `W3C` propagation types. + +You can read more about how to provide custom context propagation in this "<>". + +[[features-sampling]] +== Sampling + +Spring Cloud Sleuth pushes the sampling decision down to the tracer implementation. +However, there are cases where you can change the sampling decision at runtime. + +One of such cases is skip reporting of certain client spans. +To achieve that you can set the `spring.sleuth.web.client.skip-pattern` with the path patterns to be skipped. +Another option is to provide your own custom `org.springframework.cloud.sleuth.SamplerFunction<`org.springframework.cloud.sleuth.http.HttpRequest>` implementation and define when a given `HttpRequest` should not be sampled. + +[[features-baggage]] +== Baggage + +Distributed tracing works by propagating fields inside and across services that connect the trace together: traceId and spanId notably. +The context that holds these fields can optionally push other fields that need to be consistent regardless of many services are touched. +The simple name for these extra fields is "Baggage". + +Sleuth allows you to define which baggage are permitted to exist in the trace context, including what header names are used. + +The following example shows setting baggage values using Spring Cloud Sleuth's API: + +[source,java,indent=0] +---- +include::{common_tests_path}/src/main/java/org/springframework/cloud/sleuth/baggage/multiple/MultipleHopsIntegrationTests.java[tags=baggage,indent=0] +---- + +IMPORTANT: There is currently no limitation of the count or size of baggage items. +Keep in mind that too many can decrease system throughput or increase RPC latency. +In extreme cases, too much baggage can crash the application, due to exceeding transport-level message or header capacity. + +You can use properties to define fields that have no special configuration such as name mapping: + +* `spring.sleuth.baggage.remote-fields` is a list of header names to accept and propagate to remote services. +* `spring.sleuth.baggage.local-fields` is a list of names to propagate locally + +No prefixing applies with these keys. +What you set is literally what is used. + +A name set in either of these properties will result in a `Baggage` of the same name. + +In order to automatically set the baggage values to Slf4j's MDC, you have to set the `spring.sleuth.baggage.correlation-fields` property with a list of allowed local or remote keys. E.g. `spring.sleuth.baggage.correlation-fields=country-code` will set the value of the `country-code` baggage into MDC. + +Note that the extra field is propagated and added to MDC starting with the next downstream trace context. +To immediately add the extra field to MDC in the current trace context, configure the field to flush on update: + +``` +// configuration +@Bean +BaggageField countryCodeField() { + return BaggageField.create("country-code"); +} + +@Bean +ScopeDecorator mdcScopeDecorator() { + return MDCScopeDecorator.newBuilder() + .clear() + .add(SingleCorrelationField.newBuilder(countryCodeField()) + .flushOnUpdate() + .build()) + .build(); +} + +// service +@Autowired +BaggageField countryCodeField; + +countryCodeField.updateValue("new-value"); +``` + +IMPORTANT: Remember that adding entries to MDC can drastically decrease the performance of your application! + +If you want to add the baggage entries as tags, to make it possible to search for spans via the baggage entries, you can set the value of +`spring.sleuth.baggage.tag-fields` with a list of allowed baggage keys. +To disable the feature you have to pass the `spring.sleuth.propagation.tag.enabled=false` property. + +[[features-baggage-vs-tags]] +=== Baggage versus Tags + +Like trace IDs, Baggage is attached to messages or requests, usually as headers. +Tags are key value pairs sent in a Span to Zipkin. +Baggage values are not added spans by default, which means you can't search based on Baggage unless you opt-in. + +To make baggage also tags, use the property `spring.sleuth.baggage.tag-fields` +like so: + +[source,yml] +---- +include::{brave_path}/src/test/resources/application-baggage.yml[indent=0] +---- + +[[features-brave]] +== OpenZipkin Brave Tracer Integration + +Spring Cloud Sleuth integrates with the OpenZipkin Brave tracer via the bridge that is available in the `spring-cloud-sleuth-brave` module. +In this section you can read about specific Brave integrations. + +You can choose to use either Sleuth's API or the Brave API directly in your code (e.g. either Sleuth's `Tracer` or Brave's `Tracer`). +If you want to use this tracer implementation's API directly please read https://github.com/openzipkin/brave[their documentation to learn more about it]. + +[[features-brave-basics]] +=== Brave Basics + +Here are the most core types you might use: + +* `brave.SpanCustomizer` - to change the span currently in progress +* `brave.Tracer` - to get a start new spans ad-hoc + +Here are the most relevant links from the OpenZipkin Brave project: + +* https://github.com/openzipkin/brave/tree/master/brave[Brave's core library] +* https://github.com/openzipkin/brave/tree/master/brave#baggage[Baggage (propagated fields)] +* https://github.com/openzipkin/brave/tree/master/instrumentation/http[HTTP tracing] + +[[features-brave-sampling]] +=== Brave Sampling + +Sampling only applies to tracing backends, such as Zipkin. +Trace IDs appear in logs regardless of sample rate. +Sampling is a way to prevent overloading the system, by consistently tracing some, but not all requests. + +The default rate of 10 traces per second is controlled by the `spring.sleuth.sampler.rate` +property and applies when we know Sleuth is used for reasons besides logging. +Use a rate above 100 traces per second with extreme caution as it can overload your tracing system. + +The sampler can be set by Java Config also, as shown in the following example: + +[source,java,indent=0] +---- +include::{brave_path}/src/test/java/org/springframework/cloud/sleuth/brave/SpringCloudSleuthDocTests.java[tags=always_sampler,indent=0] +---- + +TIP: You can set the HTTP header `b3` to `1`, or, when doing messaging, you can set the `spanFlags` header to `1`. +Doing so forces the current request to be sampled regardless of configuration. + +By default samplers will work with the refresh scope mechanism. +That means that you can change the sampling properties at runtime, refresh the application and the changes will be reflected. +However, sometimes the fact of creating a proxy around samplers and calling it from too early (from `@PostConstruct` annotated method) may lead to dead locks. +In such a case either create a sampler bean explicitly, or set the property `spring.sleuth.sampler.refresh.enabled` to `false` to disable the refresh scope support. + +[[features-brave-baggage]] +=== Brave Baggage Java configuration + +If you need to do anything more advanced than above, do not define properties and instead use a +`@Bean` config for the baggage fields you use. + +* `BaggagePropagationCustomizer` sets up baggage fields +* Add a `SingleBaggageField` to control header names for a `Baggage`. +* `CorrelationScopeCustomizer` sets up MDC fields +* Add a `SingleCorrelationField` to change the MDC name of a `Baggage` or if updates flush. + +[[features-brave-customizations]] +=== Brave Customizations + +The `brave.Tracer` object is fully managed by sleuth, so you rarely need to affect it. +That said, Sleuth supports a number of `Customizer` types, that allow you to configure anything not already done by Sleuth with auto-configuration or properties. + +If you define one of the following as a `Bean`, Sleuth will invoke it to customize behaviour: + +* `RpcTracingCustomizer` - for RPC tagging and sampling policy +* `HttpTracingCustomizer` - for HTTP tagging and sampling policy +* `MessagingTracingCustomizer` - for messaging tagging and sampling policy +* `CurrentTraceContextCustomizer` - to integrate decorators such as correlation. +* `BaggagePropagationCustomizer` - for propagating baggage fields in process and over headers +* `CorrelationScopeDecoratorCustomizer` - for scope decorations such as MDC (logging) field correlation + +[[features-brave-sampling-customizations]] +==== Brave Sampling Customizations + +If client /server sampling is required, just register a bean of type +`brave.sampler.SamplerFunction` and name the bean +`sleuthHttpClientSampler` for client sampler and `sleuthHttpServerSampler` +for server sampler. + +For your convenience the `@HttpClientSampler` and `@HttpServerSampler` +annotations can be used to inject the proper beans or to reference the bean names via their static String `NAME` fields. + +Check out Brave's code to see an example of how to make a path-based sampler +https://github.com/openzipkin/brave/tree/master/instrumentation/http#sampling-policy + +If you want to completely rewrite the `HttpTracing` bean you can use the `SkipPatternProvider` +interface to retrieve the URL `Pattern` for spans that should be not sampled. +Below you can see an example of usage of `SkipPatternProvider` inside a server side, `Sampler`. + +[source,java,indent=0] +---- +@Configuration(proxyBeanMethods = false) + class Config { +include::{tests_path}/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceFilterWebIntegrationTests.java[tags=custom_server_sampler,indent=2] +} +---- + +[[features-brave-messaging]] +=== Brave Messaging + +Sleuth automatically configures the `MessagingTracing` bean which serves as a foundation for Messaging instrumentation such as Kafka or JMS. + +If a customization of producer / consumer sampling of messaging traces is required, just register a bean of type `brave.sampler.SamplerFunction` and name the bean `sleuthProducerSampler` for producer sampler and `sleuthConsumerSampler` +for consumer sampler. + +For your convenience the `@ProducerSampler` and `@ConsumerSampler` +annotations can be used to inject the proper beans or to reference the bean names via their static String `NAME` fields. + +Ex. +Here's a sampler that traces 100 consumer requests per second, except for the "alerts" channel. +Other requests will use a global rate provided by the +`Tracing` component. + +[source,java,indent=0] +---- +@Configuration(proxyBeanMethods = false) + class Config { +include::{autoconfig_path}/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/messaging/BraveMessagingAutoConfigurationIntegrationTests.java[tags=custom_messaging_consumer_sampler,indent=2] +} +---- + +For more, see https://github.com/openzipkin/brave/tree/master/instrumentation/messaging#sampling-policy + +[[features-brave-opentracing]] +=== Brave Opentracing + +You can integrate with Brave and https://opentracing.io/[OpenTracing] via the +`io.opentracing.brave:brave-opentracing` bridge. +Just add it to the classpath and the OpenTracing `Tracer` will be set up automatically. + +[[features-zipkin]] +== Sending Spans to Zipkin + +Spring Cloud Sleuth provides various integrations with the https://zipkin.io[OpenZipkin] distributed tracing system. +Regardless of the chosen tracer implementation it's enough to add `spring-cloud-sleuth-zipkin` to the classpath to start sending spans to Zipkin. +You can choose whether to do that via HTTP or messaging. +You can read more about how to do that in "<>". + +When the span is closed, it is sent to Zipkin over HTTP. The communication is asynchronous. +You can configure the URL by setting the `spring.zipkin.baseUrl` property, as follows: + +[source,yaml] +---- +spring.zipkin.baseUrl: https://192.168.99.100:9411/ +---- + +If you want to find Zipkin through service discovery, you can pass the Zipkin's service ID inside the URL, as shown in the following example for `zipkinserver` service ID: + +[source,yaml] +---- +spring.zipkin.baseUrl: https://zipkinserver/ +---- + +To disable this feature just set `spring.zipkin.discovery-client-enabled` to `false`. + +When the Discovery Client feature is enabled, Sleuth uses +`LoadBalancerClient` to find the URL of the Zipkin Server. +It means that you can set up the load balancing configuration. + +If you have `web`, `rabbit`, `activemq` or `kafka` together on the classpath, you might need to pick the means by which you would like to send spans to zipkin. +To do so, set `web`, `rabbit`, `activemq` or `kafka` to the `spring.zipkin.sender.type` property. +The following example shows setting the sender type for `web`: + +[source,yaml] +---- +spring.zipkin.sender.type: web +---- + +To customize the `RestTemplate` that sends spans to Zipkin via HTTP, you can register the `ZipkinRestTemplateCustomizer` bean. + +[source,java,indent=0] +---- +@Configuration(proxyBeanMethods = false) + class MyConfig { + @Bean ZipkinRestTemplateCustomizer myCustomizer() { + return new ZipkinRestTemplateCustomizer() { + @Override + void customize(RestTemplate restTemplate) { + // customize the RestTemplate + } + }; + } +} +---- + +If, however, you would like to control the full process of creating the `RestTemplate` +object, you will have to create a bean of `ZipkinRestTemplateProvider` type. + +[source,java,indent=0] +---- + @Bean + ZipkinRestTemplateProvider myZipkinRestTemplateProvider() { + return MyRestTemplate::new; + } +---- + +By default, api path will be set to `api/v2/spans` or `api/v1/spans` depending on the encoder version. If you want to use a custom api path, you can configure it using the following property (empty case, set ""): + +[source,yaml] +---- +spring.zipkin.api-path: v2/path2 +---- + +[[features-zipkin-custom-service-name]] +=== Custom service name + +By default, Sleuth assumes that, when you send a span to Zipkin, you want the span's service name to be equal to the value of the `spring.application.name` property. +That is not always the case, though. +There are situations in which you want to explicitly provide a different service name for all spans coming from your application. +To achieve that, you can pass the following property to your application to override that value (the example is for a service named `myService`): + +[source,yaml] +---- +spring.zipkin.service.name: myService +---- + +[[features-zipkin-host-locator]] +=== Host Locator + +IMPORTANT: This section is about defining *host* from service discovery. +It is *NOT* about finding Zipkin through service discovery. + +To define the host that corresponds to a particular span, we need to resolve the host name and port. +The default approach is to take these values from server properties. +If those are not set, we try to retrieve the host name from the network interfaces. + +If you have the discovery client enabled and prefer to retrieve the host address from the registered instance in a service registry, you have to set the `spring.zipkin.locator.discovery.enabled` property (it is applicable for both HTTP-based and Stream-based span reporting), as follows: + +[source,yaml] +---- +spring.zipkin.locator.discovery.enabled: true +---- + +[[features-zipkin-custom-reported-spans]] +=== Customization of Reported Spans + +In Sleuth, we generate spans with a fixed name. +Some users want to modify the name depending on values of tags. + +Sleuth registers a `SpanFilter` bean that can automatically skip reporting spans of given name patterns. +The property `spring.sleuth.span-filter.span-name-patterns-to-skip` contains the default skip patterns for span names. +The property `spring.sleuth.span-filter.additional-span-name-patterns-to-skip` will append the provided span name patterns to the existing ones. +In order to disable this functionality just set `spring.sleuth.span-filter.enabled` to `false`. + +[[features-zipkin-custom-reported-spans-brave]] +==== Brave Customization of Reported Spans + +IMPORTANT: This section is applicable for Brave tracer only. + +Before reporting spans (for example, to Zipkin) you may want to modify that span in some way. +You can do so by implementing a `SpanHandler`. + +The following example shows how to register two beans that implement `SpanHandler`: + +[source,java,indent=0] +---- +include::{autoconfig_path}/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/SpanHandlerTests.java[tags=spanHandler,indent=0] +---- + +The preceding example results in changing the name of the reported span to `foo bar`, just before it gets reported (for example, to Zipkin). + +=== Overriding the auto-configuration of Zipkin + +Spring Cloud Sleuth supports sending traces to multiple tracing systems as of version 2.1.0. In order to get this to work, every tracing system needs to have a `Reporter` and `Sender`. +If you want to override the provided beans you need to give them a specific name. +To do this you can use respectively `ZipkinAutoConfiguration.REPORTER_BEAN_NAME` and `ZipkinAutoConfiguration.SENDER_BEAN_NAME`. + +[source,java,indent=0] +---- +include::{common_tests_path}/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinAutoConfigurationTests.java[tags=override_default_beans,indent=0] +---- + +[[features-log-integration]] +== Log integration + +Sleuth configures the logging context with variables including the service name (`%{spring.zipkin.service.name}` or `%{spring.application.name}` if the previous one was not set), span ID (`%{spanId}`) and the trace ID (`%{traceId}`). +These help you connect logs with distributed traces and allow you choice in what tools you use to troubleshoot your services. + +Once you find any log with an error, you can look for the trace ID in the message. +Paste that into your distributed tracing system to visualize the entire trace, regardless of how many services the first request ended up hitting. + +[source] +---- +backend.log: 2020-04-09 17:45:40.516 ERROR [backend,5e8eeec48b08e26882aba313eb08f0a4,dcc1df555b5777b3] 97203 --- [nio-9000-exec-1] o.s.c.s.i.web.ExceptionLoggingFilter : Uncaught exception thrown +frontend.log:2020-04-09 17:45:40.574 ERROR [frontend,5e8eeec48b08e26882aba313eb08f0a4,82aba313eb08f0a4] 97192 --- [nio-8081-exec-2] o.s.c.s.i.web.ExceptionLoggingFilter : Uncaught exception thrown +---- + +Above, you'll notice the trace ID is `5e8eeec48b08e26882aba313eb08f0a4`, for example. +This log configuration was automatically setup by Sleuth. +You can disable it by disabling Sleuth via `spring.sleuth.enabled=false` property or putting your own `logging.pattern.level` property. + +If you use a log aggregating tool (such as https://www.elastic.co/products/kibana[Kibana], https://www.splunk.com/[Splunk], and others), you can order the events that took place. +An example from Kibana would resemble the following image: + +image::{github-raw}/docs/src/main/asciidoc/images/kibana.png[Log correlation with Kibana] + +If you want to use https://www.elastic.co/guide/en/logstash/current/index.html[Logstash], the following listing shows the Grok pattern for Logstash: + +[source] +---- +filter { + # pattern matching logback pattern + grok { + match => { "message" => "%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" } + } + date { + match => ["timestamp", "ISO8601"] + } + mutate { + remove_field => ["timestamp"] + } +} +---- + +NOTE: If you want to use Grok together with the logs from Cloud Foundry, you have to use the following pattern: + +[source] +---- +filter { + # pattern matching logback pattern + grok { + match => { "message" => "(?m)OUT\s+%{TIMESTAMP_ISO8601:timestamp}\s+%{LOGLEVEL:severity}\s+\[%{DATA:service},%{DATA:trace},%{DATA:span}\]\s+%{DATA:pid}\s+---\s+\[%{DATA:thread}\]\s+%{DATA:class}\s+:\s+%{GREEDYDATA:rest}" } + } + date { + match => ["timestamp", "ISO8601"] + } + mutate { + remove_field => ["timestamp"] + } +} +---- + +[[features-log-integration-json-logback]] +=== JSON Logback with Logstash + +Often, you do not want to store your logs in a text file but in a JSON file that Logstash can immediately pick. +To do so, you have to do the following (for readability, we pass the dependencies in the `groupId:artifactId:version` notation). + +*Dependencies Setup* + +. Ensure that Logback is on the classpath (`ch.qos.logback:logback-core`). +. Add Logstash Logback encode. +For example, to use version `4.6`, add `net.logstash.logback:logstash-logback-encoder:4.6`. + +*Logback Setup* + +Consider the following example of a Logback configuration file (logback-spring.xml). + +[source,xml] +----- +include::{project-root}/docs/src/main/asciidoc/logback-spring.xml[] +----- + +That Logback configuration file: + +* Logs information from the application in a JSON format to a `build/${spring.application.name}.json` file. +* Has commented out two additional appenders: console and standard log file. +* Has the same logging pattern as the one presented in the previous section. + +NOTE: If you use a custom `logback-spring.xml`, you must pass the `spring.application.name` in the `bootstrap` rather than the `application` property file. +Otherwise, your custom logback file does not properly read the property. + +[[features-whats-next]] +== What to Read Next + +If you want to learn more about any of the classes discussed in this section, you can browse the +{github-code}[source code directly]. +If you have specific questions, see the +<> section. + +If you are comfortable with {project-full-name}'s core features, you can continue on and read about +<>. diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceBatchAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceBatchAutoConfiguration.java index 5ede393d1..6261cc15c 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceBatchAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceBatchAutoConfiguration.java @@ -1,54 +1,54 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.batch; - -import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration - * Auto-configuration} that registers instrumentation for Spring Batch. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnClass(JobBuilderFactory.class) -@ConditionalOnBean(Tracer.class) -@ConditionalOnProperty(value = "spring.sleuth.batch.enabled", matchIfMissing = true) -@AutoConfigureAfter(BraveAutoConfiguration.class) -public class TraceBatchAutoConfiguration { - - @Bean - static TraceJobBuilderFactoryBeanPostProcessor traceJobBuilderFactoryBeanPostProcessor(BeanFactory beanFactory) { - return new TraceJobBuilderFactoryBeanPostProcessor(beanFactory); - } - - @Bean - static TraceStepBuilderFactoryBeanPostProcessor traceStepBuilderFactoryBeanPostProcessor(BeanFactory beanFactory) { - return new TraceStepBuilderFactoryBeanPostProcessor(beanFactory); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.batch; + +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Spring Batch. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(JobBuilderFactory.class) +@ConditionalOnBean(Tracer.class) +@ConditionalOnProperty(value = "spring.sleuth.batch.enabled", matchIfMissing = true) +@AutoConfigureAfter(BraveAutoConfiguration.class) +public class TraceBatchAutoConfiguration { + + @Bean + static TraceJobBuilderFactoryBeanPostProcessor traceJobBuilderFactoryBeanPostProcessor(BeanFactory beanFactory) { + return new TraceJobBuilderFactoryBeanPostProcessor(beanFactory); + } + + @Bean + static TraceStepBuilderFactoryBeanPostProcessor traceStepBuilderFactoryBeanPostProcessor(BeanFactory beanFactory) { + return new TraceStepBuilderFactoryBeanPostProcessor(beanFactory); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfiguration.java index 8fa5f2358..57c2795e6 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfiguration.java @@ -16,8 +16,6 @@ package org.springframework.cloud.sleuth.autoconfig.zipkin2; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import zipkin2.reporter.Sender; import org.springframework.beans.factory.annotation.Autowired; @@ -34,6 +32,7 @@ import org.springframework.cloud.sleuth.zipkin2.StaticInstanceZipkinLoadBalancer import org.springframework.cloud.sleuth.zipkin2.ZipkinLoadBalancer; import org.springframework.cloud.sleuth.zipkin2.ZipkinProperties; import org.springframework.cloud.sleuth.zipkin2.ZipkinRestTemplateCustomizer; +import org.springframework.cloud.sleuth.zipkin2.ZipkinRestTemplateProvider; import org.springframework.cloud.sleuth.zipkin2.ZipkinRestTemplateWrapper; import org.springframework.cloud.sleuth.zipkin2.ZipkinUrlExtractor; import org.springframework.context.annotation.Bean; @@ -47,16 +46,20 @@ import org.springframework.web.client.RestTemplate; @EnableConfigurationProperties(ZipkinSenderProperties.class) class ZipkinRestTemplateSenderConfiguration { - private static final Log log = LogFactory.getLog(ZipkinRestTemplateSenderConfiguration.class); - @Bean(ZipkinAutoConfiguration.SENDER_BEAN_NAME) Sender restTemplateSender(ZipkinProperties zipkin, ZipkinRestTemplateCustomizer zipkinRestTemplateCustomizer, - ZipkinUrlExtractor extractor) { - RestTemplate restTemplate = new ZipkinRestTemplateWrapper(zipkin, extractor); + ZipkinRestTemplateProvider zipkinRestTemplateProvider) { + RestTemplate restTemplate = zipkinRestTemplateProvider.zipkinRestTemplate(); restTemplate = zipkinRestTemplateCustomizer.customizeTemplate(restTemplate); return new RestTemplateSender(restTemplate, zipkin.getBaseUrl(), zipkin.getApiPath(), zipkin.getEncoder()); } + @Bean + @ConditionalOnMissingBean + ZipkinRestTemplateProvider zipkinRestTemplateProvider(ZipkinProperties zipkin, ZipkinUrlExtractor extractor) { + return () -> new ZipkinRestTemplateWrapper(zipkin, extractor); + } + @Bean ZipkinUrlExtractor defaultZipkinUrlExtractor(final ZipkinLoadBalancer zipkinLoadBalancer) { return new CachingZipkinUrlExtractor(zipkinLoadBalancer); diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfigurationTests.java new file mode 100644 index 000000000..336fd2ca9 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinRestTemplateSenderConfigurationTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.zipkin2; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.zipkin2.ZipkinProperties; +import org.springframework.cloud.sleuth.zipkin2.ZipkinRestTemplateCustomizer; +import org.springframework.cloud.sleuth.zipkin2.ZipkinRestTemplateProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +class ZipkinRestTemplateSenderConfigurationTests { + + @Test + void should_override_the_default_rest_template() { + ApplicationContextRunner runner = new ApplicationContextRunner().withUserConfiguration(Config.class, + ZipkinRestTemplateSenderConfiguration.class, ZipkinProperties.class); + + runner.run(context -> { + Config config = context.getBean(Config.class); + assertThat(config.customizerCalled).isTrue(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + boolean customizerCalled; + + @Bean + ZipkinRestTemplateProvider myZipkinRestTemplateProvider() { + return MyRestTemplate::new; + } + + @Bean + ZipkinRestTemplateCustomizer myZipkinRestTemplateCustomizer() { + return new ZipkinRestTemplateCustomizer() { + @Override + public RestTemplate customizeTemplate(RestTemplate restTemplate) { + customizerCalled = true; + BDDAssertions.then(restTemplate).isInstanceOf(MyRestTemplate.class); + return restTemplate; + } + }; + } + + } + + static class MyRestTemplate extends RestTemplate { + + } + +} diff --git a/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinRestTemplateProvider.java b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinRestTemplateProvider.java new file mode 100644 index 000000000..3fff6d87b --- /dev/null +++ b/spring-cloud-sleuth-zipkin/src/main/java/org/springframework/cloud/sleuth/zipkin2/ZipkinRestTemplateProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.zipkin2; + +import org.springframework.web.client.RestTemplate; + +/** + * A provider for a {@link RestTemplate} used to send spans to Zipkin. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface ZipkinRestTemplateProvider { + + RestTemplate zipkinRestTemplate(); + +} From 507e918c39522fe4d00e7866ba3ec7ade741a40a Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 7 May 2021 16:33:37 +0200 Subject: [PATCH 20/59] Added TX support fixes gh-1941 --- docs/src/main/asciidoc/integrations.adoc | 8 + .../task/TraceTaskAutoConfiguration.java | 3 +- ...rmTransactionManagerBeanPostProcessor.java | 47 ++++ ...veTransactionManagerBeanPostProcessor.java | 47 ++++ .../tx/TraceTxAutoConfiguration.java | 57 +++++ ...itional-spring-configuration-metadata.json | 6 + .../main/resources/META-INF/spring.factories | 1 + .../tx/TraceTxAutoConfigurationTests.java | 63 ++++++ .../instrument/deployer/TraceAppDeployer.java | 5 +- .../instrument/reactor/ReactorSleuth.java | 51 +++-- .../tx/TracePlatformTransactionManager.java | 173 +++++++++++++++ .../TracePlatformTransactionManagerTags.java | 81 +++++++ .../tx/TraceReactiveTransactionManager.java | 189 ++++++++++++++++ .../OkHttpFeignClientBeanPostProcessor.java | 1 + .../cloud/sleuth/ArchitectureTests.java | 2 + .../deployer/TraceAppDeployerTests.java | 2 + .../TracePlatformTransactionManagerTests.java | 201 ++++++++++++++++++ .../NoOpCurrentTraceContext.java | 4 +- .../deployer => tracer}/NoOpSpanInScope.java | 4 +- .../deployer => tracer}/NoOpTraceContext.java | 4 +- .../deployer => tracer}/SimpleSpan.java | 23 +- .../deployer => tracer}/SimpleTracer.java | 10 +- 22 files changed, 939 insertions(+), 43 deletions(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TracePlatformTransactionManagerBeanPostProcessor.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TraceReactiveTransactionManagerBeanPostProcessor.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TraceTxAutoConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TraceTxAutoConfigurationTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManager.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTags.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTests.java rename spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/{instrument/deployer => tracer}/NoOpCurrentTraceContext.java (92%) rename spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/{instrument/deployer => tracer}/NoOpSpanInScope.java (87%) rename spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/{instrument/deployer => tracer}/NoOpTraceContext.java (90%) rename spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/{instrument/deployer => tracer}/SimpleSpan.java (75%) rename spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/{instrument/deployer => tracer}/SimpleTracer.java (92%) diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 9a67a7807..c710467bc 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -599,3 +599,11 @@ This feature is available for all tracer implementations. If you have Spring Batch running on the classpath, we wrap the `StepBuilderFactory` and the `JobBuilderFactory` to propagate the tracing context. In order to disable this instrumentation set `spring.sleuth.batch.enabled` to `false`. + +[[sleuth-tx-integration]] +== Spring Tx + +This feature is available for all tracer implementations. + +If you have Spring Tx on the classpath we will instrument the `PlatformTransactionManager` and the `ReactiveTransactionManager` to create a span whenever a new transaction is created. +In order to disable this instrumentation set `spring.sleuth.tx.enabled` to `false`. diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceTaskAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceTaskAutoConfiguration.java index 591ca5941..52bf73685 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceTaskAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceTaskAutoConfiguration.java @@ -30,7 +30,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** - * Registers beans related to Spring Cloud Task scheduling. + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Spring Cloud Task. * * @author Marcin Grzejszczak * @since 3.1.0 diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TracePlatformTransactionManagerBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TracePlatformTransactionManagerBeanPostProcessor.java new file mode 100644 index 000000000..7ecb647dc --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TracePlatformTransactionManagerBeanPostProcessor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.tx; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.sleuth.instrument.tx.TracePlatformTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * Post processor that wraps a {@link PlatformTransactionManager}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TracePlatformTransactionManagerBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + public TracePlatformTransactionManagerBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof PlatformTransactionManager && !(bean instanceof TracePlatformTransactionManager)) { + return new TracePlatformTransactionManager((PlatformTransactionManager) bean, this.beanFactory); + } + return bean; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TraceReactiveTransactionManagerBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TraceReactiveTransactionManagerBeanPostProcessor.java new file mode 100644 index 000000000..8a7ac5510 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TraceReactiveTransactionManagerBeanPostProcessor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.tx; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.sleuth.instrument.tx.TraceReactiveTransactionManager; +import org.springframework.transaction.ReactiveTransactionManager; + +/** + * Post processor that wraps a {@link ReactiveTransactionManager}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceReactiveTransactionManagerBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + public TraceReactiveTransactionManagerBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ReactiveTransactionManager && !(bean instanceof TraceReactiveTransactionManager)) { + return new TraceReactiveTransactionManager((ReactiveTransactionManager) bean, this.beanFactory); + } + return bean; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TraceTxAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TraceTxAutoConfiguration.java new file mode 100644 index 000000000..f1fd9b189 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TraceTxAutoConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.tx; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Spring TX. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(value = "spring.sleuth.tx.enabled", matchIfMissing = true) +@ConditionalOnBean(Tracer.class) +@AutoConfigureAfter(BraveAutoConfiguration.class) +public class TraceTxAutoConfiguration { + + @Bean + @ConditionalOnClass(name = "org.springframework.transaction.PlatformTransactionManager") + static TracePlatformTransactionManagerBeanPostProcessor tracePlatformTransactionManagerBeanPostProcessor( + BeanFactory beanFactory) { + return new TracePlatformTransactionManagerBeanPostProcessor(beanFactory); + } + + @Bean + @ConditionalOnClass( + name = { "org.springframework.transaction.ReactiveTransactionManager", "reactor.core.publisher.Mono" }) + static TraceReactiveTransactionManagerBeanPostProcessor traceReactiveTransactionManagerBeanPostProcessor( + BeanFactory beanFactory) { + return new TraceReactiveTransactionManagerBeanPostProcessor(beanFactory); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index fef960fe4..c94fec07a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -155,6 +155,12 @@ "description": "Enable Spring Cloud Config Server instrumentation.", "defaultValue": true }, + { + "name": "spring.sleuth.tx.enabled", + "type": "java.lang.Boolean", + "description": "Enable Spring TX instrumentation.", + "defaultValue": true + }, { "name": "spring.sleuth.batch.enabled", "type": "java.lang.Boolean", diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index fff9ab35e..d91d9d53e 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -21,6 +21,7 @@ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringInte org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringMessagingAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceWebSocketAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.rsocket.TraceRSocketAutoConfiguration, \ +org.springframework.cloud.sleuth.autoconfig.instrument.tx.TraceTxAutoConfiguration, \ org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.brave.instrument.web.client.BraveWebClientAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.brave.instrument.rpc.BraveRpcAutoConfiguration,\ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TraceTxAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TraceTxAutoConfigurationTests.java new file mode 100644 index 000000000..04f3e5acd --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/tx/TraceTxAutoConfigurationTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.tx; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.ReactiveTransactionManager; + +class TraceTxAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.sleuth.noop.enabled=true") + .withConfiguration(AutoConfigurations.of(TraceNoOpAutoConfiguration.class, TraceTxAutoConfiguration.class)); + + @Test + void should_register_bean_post_processors() { + this.contextRunner.run(context -> Assertions.assertThat(context) + .hasSingleBean(TracePlatformTransactionManagerBeanPostProcessor.class) + .hasSingleBean(TraceReactiveTransactionManagerBeanPostProcessor.class)); + } + + @Test + void should_not_register_bean_post_processor_when_tx_not_on_classpath() { + this.contextRunner.withClassLoader(new FilteredClassLoader(PlatformTransactionManager.class)) + .run(context -> Assertions.assertThat(context) + .doesNotHaveBean(TracePlatformTransactionManagerBeanPostProcessor.class)); + } + + @Test + void should_not_register_reactive_bean_post_processor_when_reactive_tx_not_on_classpath() { + this.contextRunner.withClassLoader(new FilteredClassLoader(ReactiveTransactionManager.class)) + .run(context -> Assertions.assertThat(context) + .doesNotHaveBean(TraceReactiveTransactionManagerBeanPostProcessor.class)); + } + + @Test + void should_not_register_reactive_bean_post_processor_when_reactor_not_on_classpath() { + this.contextRunner.withClassLoader(new FilteredClassLoader(Mono.class)).run(context -> Assertions + .assertThat(context).doesNotHaveBean(TraceReactiveTransactionManagerBeanPostProcessor.class)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java index b4f2febe5..6e2ba7e38 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java @@ -129,13 +129,14 @@ public class TraceAppDeployer implements AppDeployer { @Override public Mono statusReactive(String id) { return ReactorSleuth.tracedMono(tracer(), currentTraceContext(), "status", - () -> this.delegate.statusReactive(id), span -> span.tag("deployer.app.id", id)); + () -> this.delegate.statusReactive(id), (o, span) -> span.tag("deployer.app.id", id)); } @Override public Flux statusesReactive(String... ids) { return ReactorSleuth.tracedFlux(tracer(), currentTraceContext(), "statuses", - () -> this.delegate.statusesReactive(ids), span -> span.tag("deployer.app.ids", Arrays.toString(ids))); + () -> this.delegate.statusesReactive(ids), + (o, span) -> span.tag("deployer.app.ids", Arrays.toString(ids))); } @Override diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java index 02d870027..45965a763 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java @@ -16,8 +16,8 @@ package org.springframework.cloud.sleuth.instrument.reactor; +import java.util.function.BiConsumer; import java.util.function.BiFunction; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -333,18 +333,21 @@ public abstract class ReactorSleuth { */ public static Mono tracedMono(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, @NonNull String childSpanName, @NonNull Supplier> supplier, - @NonNull Consumer spanCustomizer) { + @NonNull BiConsumer spanCustomizer) { return runMonoSupplierInScope(supplier, spanCustomizer).contextWrite( context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); } - private static Mono runMonoSupplierInScope(Supplier> supplier, Consumer spanCustomizer) { + private static Mono runMonoSupplierInScope(Supplier> supplier, BiConsumer spanCustomizer) { return Mono.deferContextual(contextView -> { Span span = contextView.get(Span.class); - spanCustomizer.accept(span); Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); // @formatter:off return supplier.get() + .map(t -> { + spanCustomizer.accept(t, span); + return t; + }) // TODO: Fix me when this is resolved in Reactor // .doOnSubscribe(__ -> scope.close()) .doOnError(span::error) @@ -368,7 +371,7 @@ public abstract class ReactorSleuth { */ public static Mono tracedMono(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, @NonNull String childSpanName, @NonNull Supplier> supplier) { - return tracedMono(tracer, currentTraceContext, childSpanName, supplier, span -> { + return tracedMono(tracer, currentTraceContext, childSpanName, supplier, (o, span) -> { }); } @@ -382,7 +385,7 @@ public abstract class ReactorSleuth { */ public static Mono tracedMono(@NonNull Tracer tracer, @NonNull Span span, @NonNull Supplier> supplier) { - return runMonoSupplierInScope(supplier, span1 -> { + return runMonoSupplierInScope(supplier, (o, span1) -> { }).contextWrite(context -> ReactorSleuth.putSpanInScope(tracer, context, span)); } @@ -399,7 +402,7 @@ public abstract class ReactorSleuth { */ public static Flux tracedFlux(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, @NonNull String childSpanName, @NonNull Supplier> supplier, - @NonNull Consumer spanCustomizer) { + @NonNull BiConsumer spanCustomizer) { return runFluxSupplierInScope(supplier, spanCustomizer).contextWrite( context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); } @@ -415,17 +418,20 @@ public abstract class ReactorSleuth { */ public static Flux tracedFlux(@NonNull Tracer tracer, @NonNull Span span, @NonNull Supplier> supplier) { - return runFluxSupplierInScope(supplier, span1 -> { + return runFluxSupplierInScope(supplier, (o, span1) -> { }).contextWrite(context -> ReactorSleuth.putSpanInScope(tracer, context, span)); } - private static Flux runFluxSupplierInScope(Supplier> supplier, Consumer spanCustomizer) { + private static Flux runFluxSupplierInScope(Supplier> supplier, BiConsumer spanCustomizer) { return Flux.deferContextual(contextView -> { Span span = contextView.get(Span.class); - spanCustomizer.accept(span); Tracer.SpanInScope scope = contextView.get(Tracer.SpanInScope.class); // @formatter:off return supplier.get() + .map(t -> { + spanCustomizer.accept(t, span); + return t; + }) // TODO: Fix me when this is resolved in Reactor // .doOnSubscribe(__ -> scope.close()) .doOnError(span::error) @@ -449,24 +455,21 @@ public abstract class ReactorSleuth { */ public static Flux tracedFlux(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, @NonNull String childSpanName, @NonNull Supplier> supplier) { - return tracedFlux(tracer, currentTraceContext, childSpanName, supplier, span -> { + return tracedFlux(tracer, currentTraceContext, childSpanName, supplier, (o, span) -> { }); } - private static Span spanFromContext(Tracer tracer, CurrentTraceContext currentTraceContext, + private static Span childSpanFromContext(Tracer tracer, CurrentTraceContext currentTraceContext, reactor.util.context.Context context, String childSpanName) { TraceContext traceContext = context.getOrDefault(TraceContext.class, null); - Span span = null; - if (traceContext == null) { - span = context.getOrDefault(Span.class, null); - } + Span span = context.getOrDefault(Span.class, null); if (traceContext == null && span == null) { span = tracer.nextSpan(); if (log.isDebugEnabled()) { log.debug("There was no previous span in reactor context, created a new one [" + span + "]"); } } - else if (traceContext != null) { + else if (traceContext != null && span == null) { // there was a previous span - we create a child one try (CurrentTraceContext.Scope scope = currentTraceContext.maybeScope(traceContext)) { if (log.isDebugEnabled()) { @@ -490,9 +493,19 @@ public abstract class ReactorSleuth { return span.name(childSpanName).start(); } - private static Context enhanceContext(Tracer tracer, CurrentTraceContext currentTraceContext, + /** + * Updates the Reactor context with tracing information. Creates a new span if there + * is no current span. Creates a child span if there was an entry in the context + * already. + * @param tracer tracer + * @param currentTraceContext current trace context + * @param context Reactor context + * @param childSpanName child span name when there is no span in context + * @return updated Reactor context + */ + public static Context enhanceContext(Tracer tracer, CurrentTraceContext currentTraceContext, reactor.util.context.Context context, String childSpanName) { - Span span = spanFromContext(tracer, currentTraceContext, context, childSpanName); + Span span = childSpanFromContext(tracer, currentTraceContext, context, childSpanName); return putSpanInScope(tracer, context, span); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManager.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManager.java new file mode 100644 index 000000000..e2d60723c --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManager.java @@ -0,0 +1,173 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.tx; + +import javax.annotation.PostConstruct; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.ThreadLocalSpan; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; + +/** + * A trace representation of a {@link PlatformTransactionManager}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TracePlatformTransactionManager implements PlatformTransactionManager { + + private static final Log log = LogFactory.getLog(TracePlatformTransactionManager.class); + + private final PlatformTransactionManager delegate; + + private final BeanFactory beanFactory; + + private Tracer tracer; + + volatile ThreadLocalSpan threadLocalSpan; + + public TracePlatformTransactionManager(PlatformTransactionManager delegate, BeanFactory beanFactory) { + this.delegate = delegate; + this.beanFactory = beanFactory; + } + + @PostConstruct + void initialize() { + if (this.threadLocalSpan == null) { + this.threadLocalSpan = new ThreadLocalSpan(tracer()); + } + } + + @Override + public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { + initialize(); + SpanAndScope spanAndScope = this.threadLocalSpan.get(); + Span currentSpan = spanAndScope != null ? spanAndScope.getSpan() : tracer().currentSpan(); + Span span = fallbackSpan(); + try { + TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults()); + TransactionStatus status = this.delegate.getTransaction(definition); + span = taggedSpan(currentSpan, span, def, status); + return status; + } + catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug( + "Exception occurred while trying to get a transaction, will mark the span with error and report it"); + } + span.error(e); + span.end(); + throw e; + } + } + + Span fallbackSpan() { + return tracer().nextSpan().name("tx").start(); + } + + private Span taggedSpan(Span currentSpan, Span span, TransactionDefinition def, TransactionStatus status) { + if (status.isNewTransaction() || currentSpan == null) { + log.info("Creating new span cause a new transaction is started"); + TracePlatformTransactionManagerTags.tag(span, def, this.delegate.getClass()); + } + else { + span = currentSpan; + } + this.threadLocalSpan.set(span); + return span; + } + + @Override + public void commit(TransactionStatus status) throws TransactionException { + SpanAndScope spanAndScope = this.threadLocalSpan.get(); + if (spanAndScope == null) { + if (log.isDebugEnabled()) { + log.debug("No span and scope found - this shouldn't happen, sth is wrong"); + } + this.delegate.commit(status); + return; + } + Exception ex = null; + Span span = spanAndScope.getSpan(); + try { + if (log.isDebugEnabled()) { + log.debug("Wrapping commit"); + } + this.delegate.commit(status); + } + catch (Exception e) { + ex = e; + span.error(e); + throw e; + } + finally { + span.event("tx commit"); + span.end(); + if (ex == null) { + if (log.isDebugEnabled()) { + log.debug("No exception was found - will clear thread local span"); + } + this.threadLocalSpan.remove(); + } + } + } + + @Override + public void rollback(TransactionStatus status) throws TransactionException { + SpanAndScope spanAndScope = this.threadLocalSpan.get(); + if (spanAndScope == null) { + if (log.isDebugEnabled()) { + log.debug("No span and scope found - this shouldn't happen, sth is wrong"); + } + this.delegate.rollback(status); + return; + } + Span span = spanAndScope.getSpan(); + try { + if (log.isDebugEnabled()) { + log.debug("Wrapping rollback"); + } + this.delegate.rollback(status); + } + catch (Exception e) { + span.error(e); + throw e; + } + finally { + span.event("tx rollback"); + span.end(); + this.threadLocalSpan.remove(); + } + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTags.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTags.java new file mode 100644 index 000000000..664c64b86 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTags.java @@ -0,0 +1,81 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.tx; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +final class TracePlatformTransactionManagerTags { + + private TracePlatformTransactionManagerTags() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + static void tag(Span span, TransactionDefinition def, Class transactionManagerClass) { + span.tag("tx.transaction-manager", ClassUtils.getQualifiedName(transactionManagerClass)); + span.tag("tx.read-only", String.valueOf(def.isReadOnly())); + span.tag("tx.propagation-level", propagationLevel(def)); + span.tag("tx.isolation-level", isolationLevel(def)); + if (def.getTimeout() > 0) { + span.tag("tx.timeout", String.valueOf(def.getTimeout())); + } + if (StringUtils.hasText(def.getName())) { + span.tag("tx.name", def.getName()); + } + } + + private static String propagationLevel(TransactionDefinition def) { + switch (def.getPropagationBehavior()) { + case 0: + return "PROPAGATION_REQUIRED"; + case 1: + return "PROPAGATION_SUPPORTS"; + case 2: + return "PROPAGATION_MANDATORY"; + case 3: + return "PROPAGATION_REQUIRES_NEW"; + case 4: + return "PROPAGATION_NOT_SUPPORTED"; + case 5: + return "PROPAGATION_NEVER"; + case 6: + return "PROPAGATION_NESTED"; + default: + return String.valueOf(def.getPropagationBehavior()); + } + } + + private static String isolationLevel(TransactionDefinition def) { + switch (def.getIsolationLevel()) { + case -1: + return "ISOLATION_DEFAULT"; + case 1: + return "ISOLATION_READ_UNCOMMITTED"; + case 2: + return "ISOLATION_READ_COMMITTED"; + case 4: + return "ISOLATION_REPEATABLE_READ"; + case 8: + return "ISOLATION_SERIALIZABLE"; + default: + return String.valueOf(def.getIsolationLevel()); + } + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java new file mode 100644 index 000000000..4a5f8fce2 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java @@ -0,0 +1,189 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.tx; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.transaction.ReactiveTransaction; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; + +/** + * A trace representation of a {@link ReactiveTransactionManager}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceReactiveTransactionManager implements ReactiveTransactionManager { + + private static final Log log = LogFactory.getLog(TraceReactiveTransactionManager.class); + + private final ReactiveTransactionManager delegate; + + private final BeanFactory beanFactory; + + private Tracer tracer; + + private CurrentTraceContext currentTraceContext; + + public TraceReactiveTransactionManager(ReactiveTransactionManager delegate, BeanFactory beanFactory) { + this.delegate = delegate; + this.beanFactory = beanFactory; + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + + private CurrentTraceContext currentTraceContext() { + if (this.currentTraceContext == null) { + this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); + } + return this.currentTraceContext; + } + + @Override + public Mono getReactiveTransaction(TransactionDefinition definition) + throws TransactionException { + return Mono.deferContextual(contextView -> { + return this.delegate.getReactiveTransaction(definition).map(tx -> { + Span span = span(contextView); + if (tx.isNewTransaction() || span == null) { + if (span == null) { + span = tracer().nextSpan().name("tx").start(); + } + else { + span = tracer().nextSpan(span).name("tx").start(); + } + TracePlatformTransactionManagerTags.tag(span, definition, this.delegate.getClass()); + } + Tracer.SpanInScope withSpan = tracer().withSpan(span); + SpanAndScope spanAndScope = new SpanAndScope(span, withSpan); + return new TraceReactiveTransaction(tx, spanAndScope); + }); + }); + } + + private Span span(reactor.util.context.ContextView contextView) { + Span span = contextView.getOrDefault(Span.class, null); + if (span == null) { + TraceContext traceContext = contextView.getOrDefault(TraceContext.class, null); + if (traceContext == null) { + Span currentSpan = tracer().currentSpan(); + if (log.isDebugEnabled()) { + log.debug("There's no Span or TraceContext in the reactor context. Current span is [" + currentSpan + + "]"); + } + span = currentSpan; + } + else { + span = spanFromContext(traceContext); + } + } + return span; + } + + private Span spanFromContext(TraceContext traceContext) { + try (CurrentTraceContext.Scope scope = currentTraceContext.maybeScope(traceContext)) { + return tracer().currentSpan(); + } + } + + @Override + public Mono commit(ReactiveTransaction transaction) throws TransactionException { + if (!(transaction instanceof TraceReactiveTransaction)) { + return this.delegate.commit(transaction); + } + TraceReactiveTransaction reactiveTransaction = (TraceReactiveTransaction) transaction; + SpanAndScope spanAndScope = reactiveTransaction.spanAndScope; + Span span = spanAndScope.getSpan(); + Tracer.SpanInScope scope = spanAndScope.getScope(); + return this.delegate.commit(reactiveTransaction.delegate) + // TODO: Fix me when this is resolved in Reactor + // .doOnSubscribe(__ -> scope.close()) + .doOnError(span::error).doOnSuccess(signalType -> { + span.end(); + scope.close(); + }); + } + + @Override + public Mono rollback(ReactiveTransaction transaction) throws TransactionException { + if (!(transaction instanceof TraceReactiveTransaction)) { + return this.delegate.rollback(transaction); + } + TraceReactiveTransaction reactiveTransaction = (TraceReactiveTransaction) transaction; + SpanAndScope spanAndScope = reactiveTransaction.spanAndScope; + Span span = spanAndScope.getSpan(); + Tracer.SpanInScope scope = spanAndScope.getScope(); + return this.delegate.rollback(reactiveTransaction.delegate) + // TODO: Fix me when this is resolved in Reactor + // .doOnSubscribe(__ -> scope.close()) + .doOnError(span::error).doFinally(signalType -> { + span.end(); + if (scope != null) { + scope.close(); + } + }); + } + + static class TraceReactiveTransaction implements ReactiveTransaction { + + final ReactiveTransaction delegate; + + final SpanAndScope spanAndScope; + + TraceReactiveTransaction(ReactiveTransaction delegate, SpanAndScope spanAndScope) { + this.delegate = delegate; + this.spanAndScope = spanAndScope; + } + + @Override + public boolean isNewTransaction() { + return this.delegate.isNewTransaction(); + } + + @Override + public void setRollbackOnly() { + this.delegate.setRollbackOnly(); + } + + @Override + public boolean isRollbackOnly() { + return this.delegate.isRollbackOnly(); + } + + @Override + public boolean isCompleted() { + return this.delegate.isCompleted(); + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/OkHttpFeignClientBeanPostProcessor.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/OkHttpFeignClientBeanPostProcessor.java index 4f798c4c7..a4f49c81b 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/OkHttpFeignClientBeanPostProcessor.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/feign/OkHttpFeignClientBeanPostProcessor.java @@ -29,6 +29,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor; * @author Marcin Grzejszczak * @since 1.1.3 */ +// TODO: Move this to autoconfigure public class OkHttpFeignClientBeanPostProcessor implements BeanPostProcessor { private final BeanFactory beanFactory; diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/ArchitectureTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/ArchitectureTests.java index 53b9421e7..860718387 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/ArchitectureTests.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/ArchitectureTests.java @@ -27,6 +27,8 @@ import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; @AnalyzeClasses(packagesOf = ArchitectureTests.class, importOptions = ArchitectureTests.ProductionCode.class) public class ArchitectureTests { + // TODO: Add "..org.springframework.beans.factory.config.." - BeanPostProcessors + // should end up in [autoconfig] @ArchTest public static final ArchRule should_not_contain_any_spring_configuration_reference_in_module = noClasses().should() .dependOnClassesThat().resideInAnyPackage("..org.springframework.boot.context.properties..", diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java index c6b50b62e..008ff5b59 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java @@ -34,6 +34,8 @@ import org.springframework.cloud.deployer.spi.app.AppScaleRequest; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; +import org.springframework.cloud.sleuth.tracer.NoOpCurrentTraceContext; +import org.springframework.cloud.sleuth.tracer.SimpleTracer; import org.springframework.core.env.Environment; import org.springframework.core.io.PathResource; import org.springframework.mock.env.MockEnvironment; diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTests.java new file mode 100644 index 000000000..b9c623841 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTests.java @@ -0,0 +1,201 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.tx; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.tracer.SimpleSpan; +import org.springframework.cloud.sleuth.tracer.SimpleTracer; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.TransactionTimedOutException; +import org.springframework.transaction.support.SimpleTransactionStatus; + +import static org.assertj.core.api.BDDAssertions.then; + +class TracePlatformTransactionManagerTests { + + SimpleTracer tracer = new SimpleTracer(); + + PlatformTransactionManager delegate = BDDMockito.mock(PlatformTransactionManager.class); + + @Test + void should_create_a_new_span_for_a_new_committed_transaction() { + // given + TracePlatformTransactionManager manager = manager(); + setupTransactionStatusWithNewTransactionStatusEqualTo(true); + thenThreadLocalIsClear(manager); + + // when + TransactionStatus transaction = manager.getTransaction(null); + // then + SimpleSpan span = thenATaggedSpanWasCreated(manager); + + // when + manager.commit(transaction); + // then + thenOneSpanWasReported(manager, span); + } + + @Test + void should_create_a_new_span_for_a_new_rolled_back_transaction() { + // given + TracePlatformTransactionManager manager = manager(); + setupTransactionStatusWithNewTransactionStatusEqualTo(true); + thenThreadLocalIsClear(manager); + + // when + TransactionStatus transaction = manager.getTransaction(null); + // then + SimpleSpan span = thenATaggedSpanWasCreated(manager); + + // when + manager.rollback(transaction); + // then + thenOneSpanWasReported(manager, span); + } + + @Test + void should_continue_a_span_for_a_the_same_transaction() { + // given + TracePlatformTransactionManager manager = managerWithManualFallback(); + setupTransactionStatusWithNewTransactionStatusEqualTo(false); + SimpleSpan firstSpan = threadLocalSpan(manager); + + // when + TransactionStatus transaction = manager.getTransaction(null); + // then + SpanAndScope spanAndScope = manager.threadLocalSpan.get(); + then(spanAndScope).isNotNull(); + then(spanAndScope.getSpan()).isSameAs(firstSpan); + + // when + manager.commit(transaction); + // then + thenPreviouslyCreatedSpanWasFinished(firstSpan); + then(firstSpan).isSameAs(manager.threadLocalSpan.get().getSpan()); + manager.threadLocalSpan.remove(); + thenThreadLocalIsClear(manager); + } + + private SimpleSpan threadLocalSpan(TracePlatformTransactionManager manager) { + SimpleSpan firstSpan = tracer.nextSpan().start(); + manager.threadLocalSpan.set(firstSpan); + return firstSpan; + } + + @Test + void should_report_a_fallback_span_when_exception_occurred_while_getting_transaction() { + TracePlatformTransactionManager manager = manager(); + BDDMockito.given(this.delegate.getTransaction(BDDMockito.any())) + .willThrow(new TransactionTimedOutException("boom")); + thenThreadLocalIsClear(manager); + + BDDAssertions.thenThrownBy(() -> manager.getTransaction(null)).isInstanceOf(TransactionTimedOutException.class); + + SimpleSpan span = tracer.getOnlySpan(); + then(span.throwable).isInstanceOf(TransactionTimedOutException.class); + } + + @Test + void should_report_a_span_when_exception_occurred_while_committing_transaction() { + TracePlatformTransactionManager manager = manager(); + BDDMockito.willThrow(new TransactionTimedOutException("boom")).given(this.delegate).commit(BDDMockito.any()); + threadLocalSpan(manager); + + BDDAssertions.thenThrownBy(() -> manager.commit(null)).isInstanceOf(TransactionTimedOutException.class); + + SimpleSpan span = tracer.getOnlySpan(); + then(span.throwable).isInstanceOf(TransactionTimedOutException.class); + manager.threadLocalSpan.remove(); + thenThreadLocalIsClear(manager); + } + + @Test + void should_report_a_span_when_exception_occurred_while_rolling_back_transaction() { + TracePlatformTransactionManager manager = manager(); + BDDMockito.willThrow(new TransactionTimedOutException("boom")).given(this.delegate).rollback(BDDMockito.any()); + threadLocalSpan(manager); + + BDDAssertions.thenThrownBy(() -> manager.rollback(null)).isInstanceOf(TransactionTimedOutException.class); + + SimpleSpan span = tracer.getOnlySpan(); + then(span.throwable).isInstanceOf(TransactionTimedOutException.class); + manager.threadLocalSpan.remove(); + thenThreadLocalIsClear(manager); + } + + private void setupTransactionStatusWithNewTransactionStatusEqualTo(boolean transactionStatus) { + BDDMockito.given(this.delegate.getTransaction(BDDMockito.any())) + .willReturn(new SimpleTransactionStatus(transactionStatus)); + } + + private void thenThreadLocalIsClear(TracePlatformTransactionManager manager) { + then(manager.threadLocalSpan.get()).as("Thread local was cleared").isNull(); + } + + private void thenOneSpanWasReported(TracePlatformTransactionManager manager, SimpleSpan firstSpan) { + thenPreviouslyCreatedSpanWasFinished(firstSpan); + thenThreadLocalIsClear(manager); + } + + private void thenPreviouslyCreatedSpanWasFinished(SimpleSpan firstSpan) { + then(firstSpan.ended).as("The previously created span was finished").isTrue(); + then(tracer.getOnlySpan()).as("The previously created span was reported").isSameAs(firstSpan); + } + + private TracePlatformTransactionManager manager() { + final TracePlatformTransactionManager manager = new TracePlatformTransactionManager(this.delegate, + beanFactory()); + manager.initialize(); + return manager; + } + + private TracePlatformTransactionManager managerWithManualFallback() { + final TracePlatformTransactionManager manager = new TracePlatformTransactionManager(this.delegate, + beanFactory()) { + @Override + Span fallbackSpan() { + return new SimpleSpan().start(); + } + }; + manager.initialize(); + return manager; + } + + private SimpleSpan thenATaggedSpanWasCreated(TracePlatformTransactionManager manager) { + SpanAndScope spanAndScope = manager.threadLocalSpan.get(); + then(spanAndScope).isNotNull(); + SimpleSpan span = (SimpleSpan) spanAndScope.getSpan(); + then(span.started).isTrue(); + then(span.tags).isNotEmpty(); + return span; + } + + private BeanFactory beanFactory() { + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); + beanFactory.addBean("tracer", tracer); + return beanFactory; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpCurrentTraceContext.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpCurrentTraceContext.java similarity index 92% rename from spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpCurrentTraceContext.java rename to spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpCurrentTraceContext.java index 394c3eefb..2634c18cf 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpCurrentTraceContext.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpCurrentTraceContext.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.sleuth.instrument.deployer; +package org.springframework.cloud.sleuth.tracer; import java.util.concurrent.Callable; import java.util.concurrent.Executor; @@ -29,7 +29,7 @@ import org.springframework.cloud.sleuth.TraceContext; * @author Marcin Grzejszczak * @since 3.0.0 */ -class NoOpCurrentTraceContext implements CurrentTraceContext { +public class NoOpCurrentTraceContext implements CurrentTraceContext { @Override public TraceContext context() { diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpSpanInScope.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpSpanInScope.java similarity index 87% rename from spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpSpanInScope.java rename to spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpSpanInScope.java index d2339918a..ac13149a4 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpSpanInScope.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpSpanInScope.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.sleuth.instrument.deployer; +package org.springframework.cloud.sleuth.tracer; import org.springframework.cloud.sleuth.Tracer; @@ -24,7 +24,7 @@ import org.springframework.cloud.sleuth.Tracer; * @author Marcin Grzejszczak * @since 3.0.0 */ -class NoOpSpanInScope implements Tracer.SpanInScope { +public class NoOpSpanInScope implements Tracer.SpanInScope { @Override public void close() { diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpTraceContext.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpTraceContext.java similarity index 90% rename from spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpTraceContext.java rename to spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpTraceContext.java index 057b4b41f..c8fa01ac6 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/NoOpTraceContext.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpTraceContext.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.sleuth.instrument.deployer; +package org.springframework.cloud.sleuth.tracer; import org.springframework.cloud.sleuth.TraceContext; @@ -24,7 +24,7 @@ import org.springframework.cloud.sleuth.TraceContext; * @author Marcin Grzejszczak * @since 3.0.0 */ -class NoOpTraceContext implements TraceContext { +public class NoOpTraceContext implements TraceContext { @Override public String traceId() { diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleSpan.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java similarity index 75% rename from spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleSpan.java rename to spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java index 86cdfd169..ad86a5817 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleSpan.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.sleuth.instrument.deployer; +package org.springframework.cloud.sleuth.tracer; import java.util.HashMap; import java.util.Map; @@ -28,13 +28,15 @@ import org.springframework.cloud.sleuth.TraceContext; * @author Marcin Grzejszczak * @since 3.0.0 */ -class SimpleSpan implements Span { +public class SimpleSpan implements Span { - Map tags = new HashMap<>(); + public Map tags = new HashMap<>(); - boolean started; + public boolean started; - boolean ended; + public boolean ended; + + public Throwable throwable; @Override public boolean isNoop() { @@ -47,29 +49,30 @@ class SimpleSpan implements Span { } @Override - public Span start() { + public SimpleSpan start() { this.started = true; return this; } @Override - public Span name(String name) { + public SimpleSpan name(String name) { return this; } @Override - public Span event(String value) { + public SimpleSpan event(String value) { return this; } @Override - public Span tag(String key, String value) { + public SimpleSpan tag(String key, String value) { this.tags.put(key, value); return this; } @Override - public Span error(Throwable throwable) { + public SimpleSpan error(Throwable throwable) { + this.throwable = throwable; return this; } diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java similarity index 92% rename from spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java rename to spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java index 0b56fc984..c4cbf633e 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/SimpleTracer.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.sleuth.instrument.deployer; +package org.springframework.cloud.sleuth.tracer; import java.util.ArrayList; import java.util.HashMap; @@ -36,16 +36,16 @@ import org.springframework.cloud.sleuth.Tracer; * @author Marcin Grzejszczak * @since 3.0.0 */ -class SimpleTracer implements Tracer { +public class SimpleTracer implements Tracer { - List spans = new ArrayList<>(); + public List spans = new ArrayList<>(); @Override public Span nextSpan(Span parent) { return new SimpleSpan(); } - SimpleSpan getOnlySpan() { + public SimpleSpan getOnlySpan() { BDDAssertions.then(this.spans).hasSize(1); SimpleSpan span = this.spans.get(0); BDDAssertions.then(span.started).as("Span must be started").isTrue(); @@ -69,7 +69,7 @@ class SimpleTracer implements Tracer { } @Override - public Span nextSpan() { + public SimpleSpan nextSpan() { final SimpleSpan span = new SimpleSpan(); this.spans.add(span); return span; From 59c170d1472a48f6d644174fcd29944e735c19e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flaviu=20Mure=C8=99an?= Date: Fri, 7 May 2021 22:20:37 +0200 Subject: [PATCH 21/59] Kafka instrumentation enhancements (#1936) * Bump javadocs since version to 3.1.0 * Extend integration tests to cover all Kafka clients * Add autoconfiguration module for kafka instrumentation * Refactor instrumentation for reactive Kafka Receiver * Refactor instrumentation for reactive Kafka Receiver * Add docs for Kafka instrumentation * Revert "Refactor instrumentation for reactive Kafka Receiver" This reverts commit 58c8f2fa * Revert "Revert "Refactor instrumentation for reactive Kafka Receiver"" This reverts commit 450a9f8c * Remove empty test * Resolve comments from PR 1936 * Revert whitespaces * Revert whitespaces in common tests pom.xml * Fix autoconfig to consider generics when registering beans. Only register reactor-kafka beans if the dependency is on the classpath. * Split autoconfig for kafka and reactor-kafka --- .../main/asciidoc/documentation-overview.adoc | 3 +- docs/src/main/asciidoc/integrations.adoc | 19 ++ spring-cloud-sleuth-autoconfigure/pom.xml | 5 + .../kafka/TracingKafkaAutoConfiguration.java | 74 ++++++++ ...TracingKafkaConsumerBeanPostProcessor.java | 49 +++++ ...TracingKafkaProducerBeanPostProcessor.java | 49 +++++ .../TracingReactorKafkaAutoConfiguration.java | 61 +++++++ ...TraceSpringMessagingAutoConfiguration.java | 4 +- ...itional-spring-configuration-metadata.json | 6 + .../main/resources/META-INF/spring.factories | 1 + .../kafka/KafkaTracingCallback.java | 2 +- .../kafka/TracingKafkaConsumer.java | 39 +++- .../kafka/TracingKafkaConsumerFactory.java | 49 +++++ .../kafka/TracingKafkaProducer.java | 53 ++++-- .../kafka/TracingKafkaProducerFactory.java | 17 +- .../kafka/TracingKafkaPropagatorGetter.java | 2 +- .../kafka/TracingKafkaPropagatorSetter.java | 2 +- .../kafka/TracingKafkaReceiver.java | 112 ------------ .../kafka/TracingKafkaConsumerTest.java | 14 +- .../kafka/TracingKafkaProducerTest.java | 17 +- .../kafka/TracingKafkaReceiverTest.java | 61 ------- .../pom.xml | 5 +- .../instrument/kafka/KafkaConsumerTest.java | 65 +++++++ .../instrument/kafka/KafkaProducerTest.java | 28 +++ .../instrument/kafka/KafkaReceiverTest.java | 64 +++++++ .../instrument/kafka/KafkaSenderTest.java | 69 +++++++ tests/common/pom.xml | 5 + .../instrument/kafka/KafkaConsumerTest.java | 159 ++++++++++++++++ .../instrument/kafka/KafkaProducerTest.java | 93 ++++++++-- .../instrument/kafka/KafkaReceiverTest.java | 150 ++++++++++++++++ .../instrument/kafka/KafkaSenderTest.java | 169 ++++++++++++++++++ .../instrument/kafka/KafkaTestUtils.java | 52 ++++++ 32 files changed, 1266 insertions(+), 232 deletions(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingKafkaAutoConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingKafkaConsumerBeanPostProcessor.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingKafkaProducerBeanPostProcessor.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingReactorKafkaAutoConfiguration.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumerFactory.java delete mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiver.java delete mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiverTest.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaConsumerTest.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaReceiverTest.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaSenderTest.java create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaConsumerTest.java create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaReceiverTest.java create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaSenderTest.java create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTestUtils.java diff --git a/docs/src/main/asciidoc/documentation-overview.adoc b/docs/src/main/asciidoc/documentation-overview.adoc index 9bae80a47..741e3bcca 100644 --- a/docs/src/main/asciidoc/documentation-overview.adoc +++ b/docs/src/main/asciidoc/documentation-overview.adoc @@ -77,6 +77,7 @@ Need more details about {project-full-name}'s core features? Finally, we have topics related to instrumentation integrations: * *Integrations:* +<> | <> | <> | <> | @@ -91,4 +92,4 @@ Finally, we have topics related to instrumentation integrations: <> | <> | <> | -<> \ No newline at end of file +<> diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index c710467bc..12670a2b0 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -5,6 +5,25 @@ include::_attributes.adoc[] In this section, we describe how to customize various parts of Spring Cloud Sleuth. +[[sleuth-kafka-integration]] +== Apache Kafka + +This feature is available for all tracer implementations. + +We decorate the Kafka clients (`KafkaProducer` and `KafkaConsumer`) to create a span for each event that is produced or consumed. You can disable this feature by setting the value of `spring.sleuth.kafka.enabled` to `false`. + +IMPORTANT: You have to register the `Producer` or `Consumer` as beans in order for Sleuth's auto-configuration to decorate them. When you then inject the beans, the expected type must be `Producer` or `Consumer` (and NOT e.g. `KafkaProducer`). + +We also provide `TracingKafkaProducerFactory` and `TracingKafkaConsumerFactory` to be used with the https://projectreactor.io/docs/kafka/release/reference/[Reactor Kafka] clients (`KafkaSender` and `KafkaReceiver`, respectively). See an example in the snippet below: + +[source,java,indent=0] +---- +@Bean +KafkaReceiver reactiveKafkaReceiver(TracingKafkaConsumerFactory tracingKafkaConsumerFactory, KafkaReceiverOptions kafkaReceiverOptions) { + return KafkaReceiver.create(tracingKafkaConsumerFactory, kafkaReceiverOptions); +} +---- + [[sleuth-async-integration]] == Asynchronous Communication diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index 1c74bedd6..970d8d5a7 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -275,6 +275,11 @@ spring-jms true + + io.projectreactor.kafka + reactor-kafka + true + io.github.lognet diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingKafkaAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingKafkaAutoConfiguration.java new file mode 100644 index 000000000..4eba0bf43 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingKafkaAutoConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.kafka; + +import org.apache.kafka.clients.KafkaClient; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaPropagatorGetter; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaPropagatorSetter; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Kafka. + * + * @author Anders Clausen + * @author Flaviu Muresan + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(KafkaClient.class) +@ConditionalOnBean(Tracer.class) +@AutoConfigureAfter(BraveAutoConfiguration.class) +@ConditionalOnProperty(value = "spring.sleuth.kafka.enabled", matchIfMissing = true) +public class TracingKafkaAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(value = ProducerRecord.class, parameterizedContainer = Propagator.Setter.class) + Propagator.Setter> tracingKafkaPropagationSetter() { + return new TracingKafkaPropagatorSetter(); + } + + @Bean + @ConditionalOnMissingBean(value = ConsumerRecord.class, parameterizedContainer = Propagator.Getter.class) + Propagator.Getter> tracingKafkaPropagationGetter() { + return new TracingKafkaPropagatorGetter(); + } + + @Bean + static TracingKafkaProducerBeanPostProcessor tracingKafkaProducerBeanPostProcessor(BeanFactory beanFactory) { + return new TracingKafkaProducerBeanPostProcessor(beanFactory); + } + + @Bean + static TracingKafkaConsumerBeanPostProcessor tracingKafkaConsumerBeanPostProcessor(BeanFactory beanFactory) { + return new TracingKafkaConsumerBeanPostProcessor(beanFactory); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingKafkaConsumerBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingKafkaConsumerBeanPostProcessor.java new file mode 100644 index 000000000..dbcd976a4 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingKafkaConsumerBeanPostProcessor.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.kafka; + +import org.apache.kafka.clients.consumer.Consumer; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaConsumer; + +/** + * Bean post processor for {@link org.apache.kafka.clients.consumer.Consumer}. + * + * @author Anders Clausen + * @author Flaviu Muresan + * @since 3.1.0 + */ +public class TracingKafkaConsumerBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + public TracingKafkaConsumerBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof Consumer && !(bean instanceof TracingKafkaConsumer)) { + return new TracingKafkaConsumer<>((Consumer) bean, beanFactory); + } + return bean; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingKafkaProducerBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingKafkaProducerBeanPostProcessor.java new file mode 100644 index 000000000..9ff34bd3e --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingKafkaProducerBeanPostProcessor.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.kafka; + +import org.apache.kafka.clients.producer.Producer; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaProducer; + +/** + * Bean post processor for {@link org.apache.kafka.clients.producer.Producer}. + * + * @author Anders Clausen + * @author Flaviu Muresan + * @since 3.1.0 + */ +public class TracingKafkaProducerBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + public TracingKafkaProducerBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof Producer && !(bean instanceof TracingKafkaProducer)) { + return new TracingKafkaProducer<>((Producer) bean, beanFactory); + } + return bean; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingReactorKafkaAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingReactorKafkaAutoConfiguration.java new file mode 100644 index 000000000..49615622f --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TracingReactorKafkaAutoConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.kafka; + +import reactor.kafka.receiver.KafkaReceiver; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaConsumerFactory; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaProducerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Reactor Kafka. + * + * @author Anders Clausen + * @author Flaviu Muresan + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(KafkaReceiver.class) +@ConditionalOnBean(Tracer.class) +@AutoConfigureAfter(BraveAutoConfiguration.class) +@ConditionalOnProperty(value = "spring.sleuth.kafka.enabled", matchIfMissing = true) +public class TracingReactorKafkaAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + TracingKafkaProducerFactory tracingKafkaProducerFactory(BeanFactory beanFactory) { + return new TracingKafkaProducerFactory(beanFactory); + } + + @Bean + @ConditionalOnMissingBean + TracingKafkaConsumerFactory tracingKafkaConsumerFactory(BeanFactory beanFactory) { + return new TracingKafkaConsumerFactory(beanFactory); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/TraceSpringMessagingAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/TraceSpringMessagingAutoConfiguration.java index a143bdb75..47f4a2d6f 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/TraceSpringMessagingAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/messaging/TraceSpringMessagingAutoConfiguration.java @@ -45,13 +45,13 @@ class TraceSpringMessagingAutoConfiguration { } @Bean - @ConditionalOnMissingBean + @ConditionalOnMissingBean(value = MessageHeaderAccessor.class, parameterizedContainer = Propagator.Setter.class) Propagator.Setter traceMessagePropagationSetter() { return new MessageHeaderPropagatorSetter(); } @Bean - @ConditionalOnMissingBean + @ConditionalOnMissingBean(value = MessageHeaderAccessor.class, parameterizedContainer = Propagator.Getter.class) Propagator.Getter traceMessagePropagationGetter() { return new MessageHeaderPropagatorGetter(); } diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index c94fec07a..9a894ba23 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -41,6 +41,12 @@ "description": "Enable tracing for WebSockets.", "defaultValue": true }, + { + "name": "spring.sleuth.kafka.enabled", + "type": "java.lang.Boolean", + "description": "Enable instrumenting of Apache Kafka clients.", + "defaultValue": true + }, { "name": "spring.sleuth.async.enabled", "type": "java.lang.Boolean", diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index d91d9d53e..7d7e98a50 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,5 +1,6 @@ # Auto Configuration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.sleuth.autoconfig.instrument.kafka.TracingKafkaAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncCustomAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncDefaultAutoConfiguration,\ diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingCallback.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingCallback.java index 4e8dfd44a..77da0902f 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingCallback.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingCallback.java @@ -31,7 +31,7 @@ import org.springframework.cloud.sleuth.Tracer; * * @author Anders Clausen * @author Flaviu Muresan - * @since 3.0.3 + * @since 3.1.0 */ public class KafkaTracingCallback implements Callback { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumer.java index 9698f15cd..4e655899e 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumer.java @@ -37,8 +37,11 @@ import org.apache.kafka.common.MetricName; import org.apache.kafka.common.PartitionInfo; import org.apache.kafka.common.TopicPartition; +import org.springframework.beans.factory.BeanFactory; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; /** * This decorates a Kafka {@link Consumer}. It creates and completes a @@ -47,21 +50,39 @@ import org.springframework.cloud.sleuth.propagation.Propagator; * * @author Anders Clausen * @author Flaviu Muresan - * @since 3.0.3 + * @since 3.1.0 */ public class TracingKafkaConsumer implements Consumer { + private final BeanFactory beanFactory; + private final Consumer delegate; - private final Propagator propagator; + private Propagator propagator; - private final Propagator.Getter> extractor; + private Propagator.Getter> extractor; - public TracingKafkaConsumer(Consumer consumer, Propagator propagator, - Propagator.Getter> getter) { + public TracingKafkaConsumer(Consumer consumer, BeanFactory beanFactory) { this.delegate = consumer; - this.propagator = propagator; - this.extractor = getter; + this.beanFactory = beanFactory; + } + + private Propagator propagator() { + if (this.propagator == null) { + this.propagator = this.beanFactory.getBean(Propagator.class); + } + return this.propagator; + } + + private Propagator.Getter> extractor() { + if (this.extractor == null) { + this.extractor = (Propagator.Getter>) beanFactory + .getBeanProvider(ResolvableType.forClassWithGenerics(Propagator.Getter.class, + ResolvableType.forType(new ParameterizedTypeReference>() { + }))) + .getIfAvailable(); + } + return this.extractor; } @Override @@ -109,7 +130,7 @@ public class TracingKafkaConsumer implements Consumer { public ConsumerRecords poll(long l) { ConsumerRecords consumerRecords = this.delegate.poll(l); for (ConsumerRecord consumerRecord : consumerRecords) { - KafkaTracingUtils.buildAndFinishSpan(consumerRecord, this.propagator, this.extractor); + KafkaTracingUtils.buildAndFinishSpan(consumerRecord, propagator(), extractor()); } return consumerRecords; } @@ -118,7 +139,7 @@ public class TracingKafkaConsumer implements Consumer { public ConsumerRecords poll(Duration duration) { ConsumerRecords consumerRecords = this.delegate.poll(duration); for (ConsumerRecord consumerRecord : consumerRecords) { - KafkaTracingUtils.buildAndFinishSpan(consumerRecord, this.propagator, this.extractor); + KafkaTracingUtils.buildAndFinishSpan(consumerRecord, propagator(), extractor()); } return consumerRecords; } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumerFactory.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumerFactory.java new file mode 100644 index 000000000..342b669b0 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumerFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import org.apache.kafka.clients.consumer.Consumer; +import reactor.kafka.receiver.KafkaReceiver; +import reactor.kafka.receiver.ReceiverOptions; +import reactor.kafka.receiver.internals.ConsumerFactory; + +import org.springframework.beans.factory.BeanFactory; + +/** + * This decorates a Reactor Kafka {@link ConsumerFactory} to create decorated consumers of + * type {@link TracingKafkaConsumer}. This can be used by the {@link KafkaReceiver} + * factory methods to create instrumented receivers. + * + * @author Anders Clausen + * @author Flaviu Muresan + * @since 3.1.0 + */ +public class TracingKafkaConsumerFactory extends ConsumerFactory { + + private final BeanFactory beanFactory; + + public TracingKafkaConsumerFactory(BeanFactory beanFactory) { + super(); + this.beanFactory = beanFactory; + } + + @Override + public Consumer createConsumer(ReceiverOptions receiverOptions) { + return new TracingKafkaConsumer<>(super.createConsumer(receiverOptions), beanFactory); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducer.java index 51d778076..9a75e6748 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducer.java @@ -35,9 +35,12 @@ import org.apache.kafka.common.PartitionInfo; import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.errors.ProducerFencedException; +import org.springframework.beans.factory.BeanFactory; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; /** * This decorates a Kafka {@link Producer} and creates a {@link Span.Kind#PRODUCER} span @@ -46,26 +49,50 @@ import org.springframework.cloud.sleuth.propagation.Propagator; * * @author Anders Clausen * @author Flaviu Muresan - * @since 3.0.3 + * @since 3.1.0 */ public class TracingKafkaProducer implements Producer { private static final Log log = LogFactory.getLog(TracingKafkaProducer.class); + private final BeanFactory beanFactory; + private final Producer delegate; - private final Tracer tracer; + private Tracer tracer; - private final Propagator propagator; + private Propagator propagator; - private final Propagator.Setter> injector; + private Propagator.Setter> injector; - public TracingKafkaProducer(Producer producer, Tracer tracer, Propagator propagator, - Propagator.Setter> setter) { + public TracingKafkaProducer(Producer producer, BeanFactory beanFactory) { this.delegate = producer; - this.tracer = tracer; - this.propagator = propagator; - this.injector = setter; + this.beanFactory = beanFactory; + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + + private Propagator propagator() { + if (this.propagator == null) { + this.propagator = this.beanFactory.getBean(Propagator.class); + } + return this.propagator; + } + + private Propagator.Setter> injector() { + if (this.injector == null) { + this.injector = (Propagator.Setter>) beanFactory + .getBeanProvider(ResolvableType.forClassWithGenerics(Propagator.Setter.class, + ResolvableType.forType(new ParameterizedTypeReference>() { + }))) + .getIfAvailable(); + } + return this.injector; } @Override @@ -107,15 +134,15 @@ public class TracingKafkaProducer implements Producer { @Override public Future send(ProducerRecord producerRecord, Callback callback) { - Span.Builder spanBuilder = tracer.spanBuilder().kind(Span.Kind.PRODUCER).name("kafka.produce") + Span.Builder spanBuilder = tracer().spanBuilder().kind(Span.Kind.PRODUCER).name("kafka.produce") .tag("kafka.topic", producerRecord.topic()); Span span = spanBuilder.start(); - this.propagator.inject(span.context(), producerRecord, this.injector); - try (Tracer.SpanInScope spanInScope = tracer.withSpan(span)) { + propagator().inject(span.context(), producerRecord, injector()); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { if (log.isDebugEnabled()) { log.debug("Created producer span " + span); } - return this.delegate.send(producerRecord, new KafkaTracingCallback(callback, tracer, span)); + return this.delegate.send(producerRecord, new KafkaTracingCallback(callback, tracer(), span)); } } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerFactory.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerFactory.java index 184df69b7..0f2200483 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerFactory.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerFactory.java @@ -21,8 +21,7 @@ import reactor.kafka.sender.KafkaSender; import reactor.kafka.sender.SenderOptions; import reactor.kafka.sender.internals.ProducerFactory; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.beans.factory.BeanFactory; /** * This decorates a Reactor Kafka {@link ProducerFactory} to create decorated producers of @@ -31,24 +30,20 @@ import org.springframework.cloud.sleuth.propagation.Propagator; * * @author Anders Clausen * @author Flaviu Muresan - * @since 3.0.3 + * @since 3.1.0 */ public class TracingKafkaProducerFactory extends ProducerFactory { - private final Tracer tracer; + private final BeanFactory beanFactory; - private final Propagator propagator; - - public TracingKafkaProducerFactory(Tracer tracer, Propagator propagator) { + public TracingKafkaProducerFactory(BeanFactory beanFactory) { super(); - this.tracer = tracer; - this.propagator = propagator; + this.beanFactory = beanFactory; } @Override public Producer createProducer(SenderOptions senderOptions) { - return new TracingKafkaProducer<>(super.createProducer(senderOptions), tracer, propagator, - new TracingKafkaPropagatorSetter()); + return new TracingKafkaProducer<>(super.createProducer(senderOptions), beanFactory); } } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorGetter.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorGetter.java index e09988e5c..b5a96cebf 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorGetter.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorGetter.java @@ -30,7 +30,7 @@ import org.springframework.cloud.sleuth.propagation.Propagator; * * @author Anders Clausen * @author Flaviu Muresan - * @since 3.0.3 + * @since 3.1.0 */ public class TracingKafkaPropagatorGetter implements Propagator.Getter> { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorSetter.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorSetter.java index afdb9b955..d327235ca 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorSetter.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaPropagatorSetter.java @@ -26,7 +26,7 @@ import org.springframework.cloud.sleuth.propagation.Propagator; * * @author Anders Clausen * @author Flaviu Muresan - * @since 3.0.3 + * @since 3.1.0 */ public class TracingKafkaPropagatorSetter implements Propagator.Setter> { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiver.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiver.java deleted file mode 100644 index f855b5595..000000000 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiver.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.kafka; - -import java.util.function.Function; - -import org.apache.kafka.clients.consumer.Consumer; -import org.apache.kafka.clients.consumer.ConsumerRecord; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.kafka.receiver.KafkaReceiver; -import reactor.kafka.receiver.ReceiverRecord; -import reactor.kafka.sender.TransactionManager; - -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.propagation.Propagator; - -/** - * This decorates a reactive {@link KafkaReceiver} and creates and completes a - * {@link Span.Kind#CONSUMER} span for each record received. This span will be a child - * span of the one extracted from the record headers. - * - * @author Anders Clausen - * @author Flaviu Muresan - * @since 3.0.3 - */ -public class TracingKafkaReceiver implements KafkaReceiver { - - private final KafkaReceiver delegate; - - private final Propagator propagator; - - private final Propagator.Getter> extractor; - - public TracingKafkaReceiver(KafkaReceiver receiver, Propagator propagator, - Propagator.Getter> getter) { - this.delegate = receiver; - this.propagator = propagator; - this.extractor = getter; - } - - @Override - public Flux> receive(Integer integer) { - return buildAndFinishSpanOnNextReceiverRecord(this.delegate.receive(integer)); - } - - @Override - public Flux> receive() { - return buildAndFinishSpanOnNextReceiverRecord(this.delegate.receive()); - } - - @Override - public Flux>> receiveAutoAck(Integer integer) { - return this.delegate.receiveAutoAck(integer).map(this::buildAndFinishSpanOnNextConsumerRecord); - } - - @Override - public Flux>> receiveAutoAck() { - return this.delegate.receiveAutoAck().map(this::buildAndFinishSpanOnNextConsumerRecord); - } - - @Override - public Flux> receiveAtmostOnce(Integer integer) { - return this.buildAndFinishSpanOnNextConsumerRecord(this.delegate.receiveAtmostOnce(integer)); - } - - @Override - public Flux> receiveAtmostOnce() { - return this.buildAndFinishSpanOnNextConsumerRecord(this.delegate.receiveAtmostOnce()); - } - - @Override - public Flux>> receiveExactlyOnce(TransactionManager transactionManager) { - return this.delegate.receiveExactlyOnce(transactionManager).map(this::buildAndFinishSpanOnNextConsumerRecord); - } - - @Override - public Flux>> receiveExactlyOnce(TransactionManager transactionManager, Integer integer) { - return this.delegate.receiveExactlyOnce(transactionManager, integer) - .map(this::buildAndFinishSpanOnNextConsumerRecord); - } - - @Override - public Mono doOnConsumer(Function, ? extends T> function) { - return this.delegate.doOnConsumer(function); - } - - private Flux> buildAndFinishSpanOnNextConsumerRecord(Flux> flux) { - return flux.doOnNext(consumerRecord -> KafkaTracingUtils.buildAndFinishSpan(consumerRecord, this.propagator, - this.extractor)); - } - - private Flux> buildAndFinishSpanOnNextReceiverRecord(Flux> flux) { - return flux.doOnNext(consumerRecord -> KafkaTracingUtils.buildAndFinishSpan(consumerRecord, this.propagator, - this.extractor)); - } - -} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumerTest.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumerTest.java index c04c22d72..dcac1f099 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumerTest.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumerTest.java @@ -35,6 +35,8 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; import org.springframework.cloud.sleuth.propagation.Propagator; import static org.mockito.ArgumentMatchers.eq; @@ -48,6 +50,9 @@ public class TracingKafkaConsumerTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) Propagator propagator; + @Mock + Propagator.Getter> extractor; + @Test void should_delegate_poll_calls() { Duration pollTimeout = Duration.of(5, ChronoUnit.SECONDS); @@ -57,11 +62,18 @@ public class TracingKafkaConsumerTest { ConsumerRecords records = new ConsumerRecords<>(map); BDDMockito.given(kafkaConsumer.poll(pollTimeout)).willReturn(records); TracingKafkaConsumer tracingKafkaConsumer = new TracingKafkaConsumer<>(kafkaConsumer, - propagator, new TracingKafkaPropagatorGetter()); + beanFactory()); tracingKafkaConsumer.poll(pollTimeout); Mockito.verify(kafkaConsumer).poll(eq(pollTimeout)); } + private BeanFactory beanFactory() { + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); + beanFactory.addBean("propagator", this.propagator); + beanFactory.addBean("extractor", this.extractor); + return beanFactory; + } + } diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerTest.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerTest.java index dcb3bab8b..568968727 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerTest.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerTest.java @@ -28,6 +28,8 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.propagation.Propagator; import org.springframework.test.util.ReflectionTestUtils; @@ -52,8 +54,8 @@ public class TracingKafkaProducerTest { ProducerRecord testRecord = new ProducerRecord<>("test", "test"); Callback callback = (record, ex) -> { }; - TracingKafkaProducer tracingKafkaProducer = new TracingKafkaProducer<>(kafkaProducer, tracer, - propagator, new TracingKafkaPropagatorSetter()); + TracingKafkaProducer tracingKafkaProducer = new TracingKafkaProducer<>(kafkaProducer, + beanFactory()); tracingKafkaProducer.send(testRecord, callback); @@ -65,8 +67,8 @@ public class TracingKafkaProducerTest { ProducerRecord testRecord = new ProducerRecord<>("test", "test"); Callback callback = (record, ex) -> { }; - TracingKafkaProducer tracingKafkaProducer = new TracingKafkaProducer<>(kafkaProducer, tracer, - propagator, new TracingKafkaPropagatorSetter()); + TracingKafkaProducer tracingKafkaProducer = new TracingKafkaProducer<>(kafkaProducer, + beanFactory()); tracingKafkaProducer.send(testRecord, callback); @@ -76,4 +78,11 @@ public class TracingKafkaProducerTest { BDDAssertions.then(ReflectionTestUtils.getField(callbackArgument.getValue(), "callback")).isEqualTo(callback); } + private BeanFactory beanFactory() { + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); + beanFactory.addBean("tracer", this.tracer); + beanFactory.addBean("propagator", this.propagator); + return beanFactory; + } + } diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiverTest.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiverTest.java deleted file mode 100644 index 6707aeee6..000000000 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaReceiverTest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.kafka; - -import java.util.function.Predicate; - -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Answers; -import org.mockito.BDDMockito; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; -import reactor.core.publisher.Flux; -import reactor.kafka.receiver.KafkaReceiver; -import reactor.kafka.receiver.ReceiverOffset; -import reactor.kafka.receiver.ReceiverRecord; -import reactor.test.StepVerifier; - -import org.springframework.cloud.sleuth.propagation.Propagator; - -@ExtendWith(MockitoExtension.class) -public class TracingKafkaReceiverTest { - - @Mock - KafkaReceiver kafkaReceiver; - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - Propagator propagator; - - @Test - void should_delegate_receive_calls() { - ReceiverOffset receiverOffset = BDDMockito.mock(ReceiverOffset.class); - ConsumerRecord record = new ConsumerRecord<>("topic", 0, 1, "test-key", "test-value"); - ReceiverRecord receiverRecord = new ReceiverRecord<>(record, receiverOffset); - BDDMockito.given(kafkaReceiver.receive()).willReturn(Flux.just(receiverRecord)); - TracingKafkaReceiver tracingKafkaReceiver = new TracingKafkaReceiver<>(kafkaReceiver, - propagator, new TracingKafkaPropagatorGetter()); - - StepVerifier.create(tracingKafkaReceiver.receive()).expectNextMatches(Predicate.isEqual(receiverRecord)) - .verifyComplete(); - - Mockito.verify(kafkaReceiver).receive(); - } - -} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/pom.xml index 6faff3f17..5692dd97b 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/pom.xml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/pom.xml @@ -70,7 +70,10 @@ io.projectreactor.kafka reactor-kafka - true + + + io.projectreactor + reactor-test org.testcontainers diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaConsumerTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaConsumerTest.java new file mode 100644 index 000000000..fbe5cca26 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaConsumerTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.kafka; + +import java.time.Duration; + +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.brave.BraveTestTracing; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.instrument.kafka.KafkaTestUtils; +import org.springframework.cloud.sleuth.test.TestTracingAware; + +import static org.awaitility.Awaitility.await; + +public class KafkaConsumerTest extends org.springframework.cloud.sleuth.instrument.kafka.KafkaConsumerTest { + + BraveTestTracing testTracing; + + @Override + public TestTracingAware tracerTest() { + if (this.testTracing == null) { + this.testTracing = new BraveTestTracing(); + } + return this.testTracing; + } + + @Test + public void should_consider_native_headers() { + KafkaProducer kafkaProducer = KafkaTestUtils + .buildTestKafkaProducer(kafkaContainer.getBootstrapServers()); + ProducerRecord producerRecord = new ProducerRecord<>(testTopic, "test", "test"); + producerRecord.headers().add("b3", "000000000000000a-000000000000000b-1-000000000000000a".getBytes()); + kafkaProducer.send(producerRecord); + kafkaProducer.close(); + + await().atMost(Duration.ofSeconds(5)).until(() -> receivedCounter.intValue() == 1); + + BDDAssertions.then(this.tracer.currentSpan()).isNull(); + BDDAssertions.then(this.spans).hasSize(1); + FinishedSpan span = this.spans.get(0); + BDDAssertions.then(span.getKind()).isEqualTo(Span.Kind.CONSUMER); + BDDAssertions.then(span.getTraceId()).isEqualTo("000000000000000a"); + BDDAssertions.then(span.getParentId()).isEqualTo("000000000000000b"); + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java index b19b6efc3..78f9e7a6e 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java @@ -16,6 +16,15 @@ package org.springframework.cloud.sleuth.brave.instrument.kafka; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.header.Header; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + import org.springframework.cloud.sleuth.brave.BraveTestTracing; import org.springframework.cloud.sleuth.test.TestTracingAware; @@ -31,4 +40,23 @@ public class KafkaProducerTest extends org.springframework.cloud.sleuth.instrume return this.testTracing; } + @Test + public void should_inject_native_headers() throws InterruptedException { + ProducerRecord producerRecord = new ProducerRecord<>(testTopic, "test", "test"); + startKafkaConsumer(); + + this.kafkaProducer.send(producerRecord); + ConsumerRecord consumerRecord = consumerRecords.poll(5, TimeUnit.SECONDS); + + BDDAssertions.then(consumerRecord).isNotNull(); + BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-TraceId")).isNotNull(); + BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-SpanId")).isNotNull(); + BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-Sampled")).isNotNull(); + } + + private static String getHeaderValueOrNull(ConsumerRecord consumerRecord, String header) { + return Optional.ofNullable(consumerRecord).map(ConsumerRecord::headers) + .map(headers -> headers.lastHeader(header)).map(Header::value).map(String::new).orElse(null); + } + } diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaReceiverTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaReceiverTest.java new file mode 100644 index 000000000..5a3f14d75 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaReceiverTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.kafka; + +import java.time.Duration; + +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.brave.BraveTestTracing; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.instrument.kafka.KafkaTestUtils; +import org.springframework.cloud.sleuth.test.TestTracingAware; + +import static org.awaitility.Awaitility.await; + +public class KafkaReceiverTest extends org.springframework.cloud.sleuth.instrument.kafka.KafkaReceiverTest { + + BraveTestTracing testTracing; + + @Override + public TestTracingAware tracerTest() { + if (this.testTracing == null) { + this.testTracing = new BraveTestTracing(); + } + return this.testTracing; + } + + @Test + public void should_consider_native_headers() { + KafkaProducer kafkaProducer = KafkaTestUtils + .buildTestKafkaProducer(kafkaContainer.getBootstrapServers()); + ProducerRecord producerRecord = new ProducerRecord<>(testTopic, "test", "test"); + producerRecord.headers().add("b3", "000000000000000a-000000000000000b-1-000000000000000a".getBytes()); + kafkaProducer.send(producerRecord); + + await().atMost(Duration.ofSeconds(5)).until(() -> receivedCounter.intValue() == 1); + + BDDAssertions.then(this.tracer.currentSpan()).isNull(); + BDDAssertions.then(this.spans).hasSize(1); + FinishedSpan span = this.spans.get(0); + BDDAssertions.then(span.getKind()).isEqualTo(Span.Kind.CONSUMER); + BDDAssertions.then(span.getTraceId()).isEqualTo("000000000000000a"); + BDDAssertions.then(span.getParentId()).isEqualTo("000000000000000b"); + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaSenderTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaSenderTest.java new file mode 100644 index 000000000..fb88a5b99 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaSenderTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.kafka; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.header.Header; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.kafka.sender.SenderRecord; +import reactor.kafka.sender.SenderResult; +import reactor.test.StepVerifier; + +import org.springframework.cloud.sleuth.brave.BraveTestTracing; +import org.springframework.cloud.sleuth.test.TestTracingAware; + +public class KafkaSenderTest extends org.springframework.cloud.sleuth.instrument.kafka.KafkaSenderTest { + + BraveTestTracing testTracing; + + @Override + public TestTracingAware tracerTest() { + if (this.testTracing == null) { + this.testTracing = new BraveTestTracing(); + } + return this.testTracing; + } + + @Test + public void should_inject_native_headers() throws InterruptedException { + ProducerRecord producerRecord = new ProducerRecord<>(testTopic, "test", "test"); + startKafkaConsumer(); + + Flux> senderResultFlux = this.kafkaSender + .send(Mono.just(SenderRecord.create(producerRecord, null))); + StepVerifier.create(senderResultFlux).expectNextCount(1).verifyComplete(); + ConsumerRecord consumerRecord = consumerRecords.poll(5, TimeUnit.SECONDS); + + BDDAssertions.then(consumerRecord).isNotNull(); + BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-TraceId")).isNotNull(); + BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-SpanId")).isNotNull(); + BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-Sampled")).isNotNull(); + } + + private static String getHeaderValueOrNull(ConsumerRecord consumerRecord, String header) { + return Optional.ofNullable(consumerRecord).map(ConsumerRecord::headers) + .map(headers -> headers.lastHeader(header)).map(Header::value).map(String::new).orElse(null); + } + +} diff --git a/tests/common/pom.xml b/tests/common/pom.xml index a4f029dc5..3f6a1176d 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -149,6 +149,11 @@ brave-tests true + + io.projectreactor + reactor-test + true + io.projectreactor.kafka reactor-kafka diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaConsumerTest.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaConsumerTest.java new file mode 100644 index 000000000..cd84d04b3 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaConsumerTest.java @@ -0,0 +1,159 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Pattern; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.BDDMockito; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.cloud.sleuth.test.TestTracingAwareSupplier; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; + +import static org.awaitility.Awaitility.await; + +@Testcontainers +@ExtendWith(MockitoExtension.class) +public abstract class KafkaConsumerTest implements TestTracingAwareSupplier { + + protected String testTopic; + + protected Tracer tracer = tracerTest().tracing().tracer(); + + protected Propagator propagator = tracerTest().tracing().propagator(); + + protected TestSpanHandler spans = tracerTest().handler(); + + protected TracingKafkaConsumer kafkaConsumer; + + private final AtomicBoolean consumerRun = new AtomicBoolean(); + + protected final AtomicInteger receivedCounter = new AtomicInteger(0); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + BeanFactory beanFactory; + + @Container + protected static final KafkaContainer kafkaContainer = new KafkaContainer( + DockerImageName.parse("confluentinc/cp-kafka:6.1.1")).withExposedPorts(9093) + .waitingFor(Wait.forListeningPort()); + + @BeforeAll + static void setupAll() { + kafkaContainer.start(); + } + + @AfterAll + static void destroyAll() { + kafkaContainer.stop(); + } + + @BeforeEach + void setup() { + BDDMockito.given(this.beanFactory.getBean(Propagator.class)).willReturn(this.propagator); + BDDMockito.given(this.beanFactory.getBeanProvider(ResolvableType.forClassWithGenerics(Propagator.Getter.class, + ResolvableType.forType(new ParameterizedTypeReference>() { + }))).getIfAvailable()).willReturn(new TracingKafkaPropagatorGetter()); + testTopic = UUID.randomUUID().toString(); + Map consumerProperties = new HashMap<>(); + consumerProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers()); + consumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG, "test-consumer-group"); + consumerProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + consumerProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + kafkaConsumer = new TracingKafkaConsumer<>(new KafkaConsumer<>(consumerProperties), beanFactory); + consumerRun.set(true); + Executors.newSingleThreadExecutor().execute(() -> doStartKafkaConsumer(receivedCounter)); + } + + @AfterEach + void destroy() { + consumerRun.set(false); + } + + @Test + public void should_create_and_finish_consumer_span() { + KafkaProducer kafkaProducer = KafkaTestUtils + .buildTestKafkaProducer(kafkaContainer.getBootstrapServers()); + ProducerRecord producerRecord = new ProducerRecord<>(testTopic, "test", "test"); + kafkaProducer.send(producerRecord); + kafkaProducer.close(); + + await().atMost(Duration.ofSeconds(5)).until(() -> receivedCounter.intValue() == 1); + + BDDAssertions.then(this.tracer.currentSpan()).isNull(); + BDDAssertions.then(this.spans).hasSize(1); + FinishedSpan span = this.spans.get(0); + BDDAssertions.then(span.getKind()).isEqualTo(Span.Kind.CONSUMER); + BDDAssertions.then(span.getTags()).isNotEmpty(); + BDDAssertions.then(span.getTags().get("kafka.topic")).isEqualTo(testTopic); + BDDAssertions.then(span.getTags().get("kafka.offset")).isEqualTo("0"); + BDDAssertions.then(span.getTags().get("kafka.partition")).isEqualTo("0"); + } + + private void doStartKafkaConsumer(AtomicInteger receivedCounter) { + this.kafkaConsumer.subscribe(Pattern.compile(this.testTopic)); + while (this.consumerRun.get()) { + ConsumerRecords records = this.kafkaConsumer.poll(Duration.ofSeconds(1)); + for (ConsumerRecord record : records) { + receivedCounter.incrementAndGet(); + } + } + this.kafkaConsumer.close(); + } + + @Override + public void cleanUpTracing() { + this.spans.clear(); + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaProducerTest.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaProducerTest.java index c541eac73..f399eb190 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaProducerTest.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaProducerTest.java @@ -19,34 +19,56 @@ package org.springframework.cloud.sleuth.instrument.kafka; import java.time.Duration; import java.util.HashMap; import java.util.Map; +import java.util.UUID; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; import org.apache.kafka.clients.producer.Callback; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.serialization.StringSerializer; import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.BDDMockito; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.testcontainers.containers.KafkaContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; +import org.springframework.beans.factory.BeanFactory; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; import org.springframework.cloud.sleuth.propagation.Propagator; import org.springframework.cloud.sleuth.test.TestSpanHandler; import org.springframework.cloud.sleuth.test.TestTracingAwareSupplier; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; import static org.awaitility.Awaitility.await; @Testcontainers +@ExtendWith(MockitoExtension.class) public abstract class KafkaProducerTest implements TestTracingAwareSupplier { + protected String testTopic; + protected Tracer tracer = tracerTest().tracing().tracer(); protected Propagator propagator = tracerTest().tracing().propagator(); @@ -55,39 +77,82 @@ public abstract class KafkaProducerTest implements TestTracingAwareSupplier { protected TracingKafkaProducer kafkaProducer; + private final AtomicBoolean consumerRun = new AtomicBoolean(); + + protected final BlockingQueue> consumerRecords = new LinkedBlockingQueue<>(); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + BeanFactory beanFactory; + @Container - protected final KafkaContainer kafkaContainer = new KafkaContainer( - DockerImageName.parse("confluentinc/cp-kafka:5.2.1")).withExposedPorts(9093) + protected static final KafkaContainer kafkaContainer = new KafkaContainer( + DockerImageName.parse("confluentinc/cp-kafka:6.1.1")).withExposedPorts(9093) .waitingFor(Wait.forListeningPort()); + @BeforeAll + static void setupAll() { + kafkaContainer.start(); + } + + @AfterAll + static void destroyAll() { + kafkaContainer.stop(); + } + @BeforeEach void setup() { - kafkaContainer.start(); - Map properties = new HashMap<>(); - properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers()); - properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); - kafkaProducer = new TracingKafkaProducer<>(new KafkaProducer<>(properties), tracer, propagator, - new TracingKafkaPropagatorSetter()); + BDDMockito.given(this.beanFactory.getBean(Tracer.class)).willReturn(this.tracer); + BDDMockito.given(this.beanFactory.getBean(Propagator.class)).willReturn(this.propagator); + BDDMockito.given(this.beanFactory.getBeanProvider(ResolvableType.forClassWithGenerics(Propagator.Setter.class, + ResolvableType.forType(new ParameterizedTypeReference>() { + }))).getIfAvailable()).willReturn(new TracingKafkaPropagatorSetter()); + testTopic = UUID.randomUUID().toString(); + Map producerProperties = new HashMap<>(); + producerProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers()); + producerProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + producerProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + kafkaProducer = new TracingKafkaProducer<>(new KafkaProducer<>(producerProperties), beanFactory); + consumerRun.set(true); + consumerRecords.clear(); } @AfterEach void destroy() { - kafkaContainer.stop(); + this.kafkaProducer.close(); + consumerRun.set(false); } @Test public void should_create_and_finish_producer_span() { AtomicBoolean acknowledged = new AtomicBoolean(false); Callback callback = (metadata, ex) -> acknowledged.set(true); - ProducerRecord producerRecord = new ProducerRecord<>("spring-cloud-sleuth-otel-topic", "test", - "test"); + ProducerRecord producerRecord = new ProducerRecord<>(testTopic, "test", "test"); + this.kafkaProducer.send(producerRecord, callback); await().atMost(Duration.ofSeconds(5)).until(acknowledged::get); BDDAssertions.then(this.tracer.currentSpan()).isNull(); - BDDAssertions.then(this.spans).isNotEmpty(); - BDDAssertions.then(this.spans.get(0).getKind()).isEqualTo(Span.Kind.PRODUCER); + BDDAssertions.then(this.spans).hasSize(1); + FinishedSpan span = this.spans.get(0); + BDDAssertions.then(span.getKind()).isEqualTo(Span.Kind.PRODUCER); + BDDAssertions.then(span.getTags().get("kafka.topic")).isEqualTo(testTopic); + } + + protected void startKafkaConsumer() { + Executors.newSingleThreadExecutor().execute(this::doStartKafkaConsumer); + } + + private void doStartKafkaConsumer() { + KafkaConsumer kafkaConsumer = KafkaTestUtils + .buildTestKafkaConsumer(kafkaContainer.getBootstrapServers()); + kafkaConsumer.subscribe(Pattern.compile(testTopic)); + while (consumerRun.get()) { + ConsumerRecords records = kafkaConsumer.poll(Duration.ofSeconds(1)); + for (ConsumerRecord record : records) { + consumerRecords.offer(record); + } + } + kafkaConsumer.close(); } @Override diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaReceiverTest.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaReceiverTest.java new file mode 100644 index 000000000..e33f3eb7c --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaReceiverTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.BDDMockito; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import reactor.core.Disposable; +import reactor.core.scheduler.Schedulers; +import reactor.kafka.receiver.KafkaReceiver; +import reactor.kafka.receiver.ReceiverOptions; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.cloud.sleuth.test.TestTracingAwareSupplier; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; + +import static org.awaitility.Awaitility.await; + +@Testcontainers +@ExtendWith(MockitoExtension.class) +public abstract class KafkaReceiverTest implements TestTracingAwareSupplier { + + protected String testTopic; + + protected Tracer tracer = tracerTest().tracing().tracer(); + + protected Propagator propagator = tracerTest().tracing().propagator(); + + protected TestSpanHandler spans = tracerTest().handler(); + + private Disposable consumerSubscription; + + protected final AtomicInteger receivedCounter = new AtomicInteger(0); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + BeanFactory beanFactory; + + @Container + protected static final KafkaContainer kafkaContainer = new KafkaContainer( + DockerImageName.parse("confluentinc/cp-kafka:6.1.1")).withExposedPorts(9093) + .waitingFor(Wait.forListeningPort()); + + @BeforeAll + static void setupAll() { + kafkaContainer.start(); + } + + @AfterAll + static void destroyAll() { + kafkaContainer.stop(); + } + + @BeforeEach + void setup() { + BDDMockito.given(this.beanFactory.getBean(Propagator.class)).willReturn(this.propagator); + BDDMockito.given(this.beanFactory.getBeanProvider(ResolvableType.forClassWithGenerics(Propagator.Getter.class, + ResolvableType.forType(new ParameterizedTypeReference>() { + }))).getIfAvailable()).willReturn(new TracingKafkaPropagatorGetter()); + testTopic = UUID.randomUUID().toString(); + Map consumerProperties = new HashMap<>(); + consumerProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers()); + consumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG, "test-consumer-group"); + consumerProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + consumerProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + ReceiverOptions options = ReceiverOptions.create(consumerProperties); + options = options.withKeyDeserializer(new StringDeserializer()).withValueDeserializer(new StringDeserializer()) + .subscription(Collections.singletonList(testTopic)); + KafkaReceiver kafkaReceiver = KafkaReceiver.create(new TracingKafkaConsumerFactory(beanFactory), + options); + this.consumerSubscription = kafkaReceiver.receive().subscribeOn(Schedulers.single()) + .subscribe(record -> receivedCounter.incrementAndGet()); + this.receivedCounter.set(0); + } + + @AfterEach + void destroy() { + this.consumerSubscription.dispose(); + } + + @Test + public void should_create_and_finish_consumer_span() { + KafkaProducer kafkaProducer = KafkaTestUtils + .buildTestKafkaProducer(kafkaContainer.getBootstrapServers()); + ProducerRecord producerRecord = new ProducerRecord<>(testTopic, "test", "test"); + kafkaProducer.send(producerRecord); + + await().atMost(Duration.ofSeconds(5)).until(() -> receivedCounter.intValue() == 1); + + BDDAssertions.then(this.tracer.currentSpan()).isNull(); + BDDAssertions.then(this.spans).hasSize(1); + FinishedSpan span = this.spans.get(0); + BDDAssertions.then(span.getKind()).isEqualTo(Span.Kind.CONSUMER); + BDDAssertions.then(span.getTags()).isNotEmpty(); + BDDAssertions.then(span.getTags().get("kafka.topic")).isEqualTo(testTopic); + BDDAssertions.then(span.getTags().get("kafka.offset")).isEqualTo("0"); + BDDAssertions.then(span.getTags().get("kafka.partition")).isEqualTo("0"); + } + + @Override + public void cleanUpTracing() { + this.spans.clear(); + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaSenderTest.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaSenderTest.java new file mode 100644 index 000000000..67db8e9cc --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaSenderTest.java @@ -0,0 +1,169 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.StringSerializer; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.BDDMockito; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.kafka.sender.KafkaSender; +import reactor.kafka.sender.SenderOptions; +import reactor.kafka.sender.SenderRecord; +import reactor.kafka.sender.SenderResult; +import reactor.test.StepVerifier; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.cloud.sleuth.test.TestTracingAwareSupplier; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; + +@Testcontainers +@ExtendWith(MockitoExtension.class) +public abstract class KafkaSenderTest implements TestTracingAwareSupplier { + + protected String testTopic; + + protected Tracer tracer = tracerTest().tracing().tracer(); + + protected Propagator propagator = tracerTest().tracing().propagator(); + + protected TestSpanHandler spans = tracerTest().handler(); + + protected KafkaSender kafkaSender; + + private final AtomicBoolean consumerRun = new AtomicBoolean(); + + protected final BlockingQueue> consumerRecords = new LinkedBlockingQueue<>(); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + BeanFactory beanFactory; + + @Container + protected static final KafkaContainer kafkaContainer = new KafkaContainer( + DockerImageName.parse("confluentinc/cp-kafka:6.1.1")).withExposedPorts(9093) + .waitingFor(Wait.forListeningPort()); + + @BeforeAll + static void setupAll() { + kafkaContainer.start(); + } + + @AfterAll + static void destroyAll() { + kafkaContainer.stop(); + } + + @BeforeEach + void setup() { + BDDMockito.given(this.beanFactory.getBean(Tracer.class)).willReturn(this.tracer); + BDDMockito.given(this.beanFactory.getBean(Propagator.class)).willReturn(this.propagator); + BDDMockito.given(this.beanFactory.getBeanProvider(ResolvableType.forClassWithGenerics(Propagator.Setter.class, + ResolvableType.forType(new ParameterizedTypeReference>() { + }))).getIfAvailable()).willReturn(new TracingKafkaPropagatorSetter()); + testTopic = UUID.randomUUID().toString(); + Map producerProperties = new HashMap<>(); + producerProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaContainer.getBootstrapServers()); + producerProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + producerProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + this.kafkaSender = KafkaSender.create(new TracingKafkaProducerFactory(beanFactory), + SenderOptions.create(producerProperties)); + consumerRun.set(true); + consumerRecords.clear(); + } + + @AfterEach + void destroy() { + consumerRun.set(false); + } + + @Test + public void should_create_and_finish_producer_span() throws InterruptedException { + ProducerRecord producerRecord = new ProducerRecord<>(testTopic, "test", "test"); + startKafkaConsumer(); + + Flux> senderResultFlux = this.kafkaSender + .send(Mono.just(SenderRecord.create(producerRecord, null))); + StepVerifier.create(senderResultFlux).expectNextCount(1).verifyComplete(); + consumerRecords.poll(5, TimeUnit.SECONDS); + + BDDAssertions.then(this.tracer.currentSpan()).isNull(); + BDDAssertions.then(this.spans).hasSize(1); + FinishedSpan span = this.spans.get(0); + BDDAssertions.then(span.getKind()).isEqualTo(Span.Kind.PRODUCER); + BDDAssertions.then(span.getTags().get("kafka.topic")).isEqualTo(testTopic); + + } + + protected void startKafkaConsumer() { + Executors.newSingleThreadExecutor().execute(this::doStartKafkaConsumer); + } + + private void doStartKafkaConsumer() { + KafkaConsumer kafkaConsumer = KafkaTestUtils + .buildTestKafkaConsumer(kafkaContainer.getBootstrapServers()); + kafkaConsumer.subscribe(Pattern.compile(this.testTopic)); + while (this.consumerRun.get()) { + ConsumerRecords records = kafkaConsumer.poll(Duration.ofSeconds(1)); + for (ConsumerRecord record : records) { + this.consumerRecords.offer(record); + } + } + kafkaConsumer.close(); + } + + @Override + public void cleanUpTracing() { + this.spans.clear(); + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTestUtils.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTestUtils.java new file mode 100644 index 000000000..4d58a2306 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTestUtils.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; + +public final class KafkaTestUtils { + + private KafkaTestUtils() { + } + + public static KafkaProducer buildTestKafkaProducer(String bootstrapServers) { + Map producerProperties = new HashMap<>(); + producerProperties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + producerProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + producerProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + return new KafkaProducer<>(producerProperties); + } + + public static KafkaConsumer buildTestKafkaConsumer(String bootstrapServers) { + Map consumerProperties = new HashMap<>(); + consumerProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + consumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG, "test-consumer-group"); + consumerProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + consumerProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + consumerProperties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + return new KafkaConsumer<>(consumerProperties); + } + +} From ac729f141d0f2d7ea483a999cacc1cbfeb49e9bc Mon Sep 17 00:00:00 2001 From: Jason Hinch <594559+jhinch@users.noreply.github.com> Date: Sat, 8 May 2021 06:21:54 +1000 Subject: [PATCH 22/59] Modify WebClient tracing to include the uri template in generated span names (#1940) Spans names are in the form ' ' (e.g. 'GET /path/{id}'). This naming strategy matches the span names generated by TraceWebFilter. The improve naming strategy is not applied to RestTemplate as the uri template is not readily available to interceptors and RestTemplate is in maintenance mode --- .../client/TraceExchangeFilterFunction.java | 36 +++++++++++++++++-- ...FilterFunctionHttpClientResponseTests.java | 4 +-- .../integration/sampled/WebClientTests.java | 25 +++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/TraceExchangeFilterFunction.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/TraceExchangeFilterFunction.java index 487148e1e..9efc62798 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/TraceExchangeFilterFunction.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/TraceExchangeFilterFunction.java @@ -38,10 +38,12 @@ import org.springframework.cloud.sleuth.http.HttpClientRequest; import org.springframework.cloud.sleuth.http.HttpClientResponse; import org.springframework.cloud.sleuth.instrument.reactor.TraceContextPropagator; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.http.HttpMethod; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.WebClient; /** * Trace representation of {@link ExchangeFilterFunction}. @@ -53,6 +55,8 @@ public final class TraceExchangeFilterFunction implements ExchangeFilterFunction private static final Log log = LogFactory.getLog(TraceExchangeFilterFunction.class); + private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate"; + final ConfigurableApplicationContext springContext; // Lazy initialized fields @@ -155,12 +159,18 @@ public final class TraceExchangeFilterFunction implements ExchangeFilterFunction final CurrentTraceContext currentTraceContext; + final HttpMethod method; + + final String httpRoute; + TraceWebClientSubscriber(CoreSubscriber actual, Context ctx, Span clientSpan, TraceContext parent, MonoWebClientTrace mono) { this.actual = actual; this.parent = parent; this.handler = mono.handler; this.currentTraceContext = mono.currentTraceContext; + this.method = mono.request.method(); + this.httpRoute = (String) mono.request.attribute(URI_TEMPLATE_ATTRIBUTE).orElse(null); this.context = this.parent != null && !this.parent.equals(ctx.getOrDefault(TraceContext.class, null)) ? ctx.put(TraceContext.class, this.parent) : ctx; set(clientSpan); @@ -186,8 +196,7 @@ public final class TraceExchangeFilterFunction implements ExchangeFilterFunction if (log.isTraceEnabled()) { log.trace("OnNext finally"); } - // TODO: is there a way to read the request at response time? - this.handler.handleReceive(new ClientResponseWrapper(response), span); + this.handler.handleReceive(new ClientResponseWrapper(response, this.method, this.httpRoute), span); } } } @@ -330,6 +339,11 @@ public final class TraceExchangeFilterFunction implements ExchangeFilterFunction return delegate.url().getPath(); } + @Override + public String route() { + return (String) delegate.attribute(URI_TEMPLATE_ATTRIBUTE).orElse(null); + } + @Override public String url() { return delegate.url().toString(); @@ -355,8 +369,24 @@ public final class TraceExchangeFilterFunction implements ExchangeFilterFunction final ClientResponse delegate; - ClientResponseWrapper(ClientResponse delegate) { + final HttpMethod method; + + final String httpRoute; + + ClientResponseWrapper(ClientResponse delegate, HttpMethod method, String httpRoute) { this.delegate = delegate; + this.method = method; + this.httpRoute = httpRoute; + } + + @Override + public String method() { + return method.name(); + } + + @Override + public String route() { + return httpRoute; } @Override diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/TraceExchangeFilterFunctionHttpClientResponseTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/TraceExchangeFilterFunctionHttpClientResponseTests.java index be159e3a4..04b85469f 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/TraceExchangeFilterFunctionHttpClientResponseTests.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/web/client/TraceExchangeFilterFunctionHttpClientResponseTests.java @@ -29,7 +29,7 @@ public class TraceExchangeFilterFunctionHttpClientResponseTests { public void should_return_0_when_invalid_status_code_is_returned() { ClientResponse clientResponse = BDDMockito.mock(ClientResponse.class); BDDMockito.given(clientResponse.rawStatusCode()).willReturn(-1); - ClientResponseWrapper response = new ClientResponseWrapper(clientResponse); + ClientResponseWrapper response = new ClientResponseWrapper(clientResponse, null, null); Integer statusCode = response.statusCode(); @@ -40,7 +40,7 @@ public class TraceExchangeFilterFunctionHttpClientResponseTests { public void should_return_status_code_when_valid_status_code_is_returned() { ClientResponse clientResponse = BDDMockito.mock(ClientResponse.class); BDDMockito.given(clientResponse.rawStatusCode()).willReturn(200); - ClientResponseWrapper response = new ClientResponseWrapper(clientResponse); + ClientResponseWrapper response = new ClientResponseWrapper(clientResponse, null, null); Integer statusCode = response.statusCode(); diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/integration/sampled/WebClientTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/integration/sampled/WebClientTests.java index 89fad7d8f..6f9f9c9ab 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/integration/sampled/WebClientTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/integration/sampled/WebClientTests.java @@ -68,6 +68,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -252,6 +253,25 @@ public abstract class WebClientTests { .collect(Collectors.toList())).isNotEmpty().contains("CLIENT"); } + @Test + @SuppressWarnings("unchecked") + public void shouldUseUriTemplateInSpanName() { + Span span = this.tracer.nextSpan().name("foo").start(); + + try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { + this.webClientBuilder.baseUrl("http://localhost:" + this.port).build().get() + .uri("/prefix/{variable}/suffix", "value").retrieve().bodyToMono(String.class) + .block(Duration.ofSeconds(5)); + } + finally { + span.end(); + } + + thenThereIsNoCurrentSpan(); + then(this.spans.reportedSpans().stream().filter(r -> r.getKind() == Span.Kind.CLIENT).map(r -> r.getName()) + .collect(Collectors.toList())).isNotEmpty().contains("GET /prefix/{variable}/suffix"); + } + /** * Cancel before {@link Subscription#request(long)} means a network request was never * sent @@ -549,6 +569,11 @@ public abstract class WebClientTests { return "ok"; } + @RequestMapping(value = "/prefix/{variable}/suffix", method = RequestMethod.GET) + String pathVariable(@PathVariable("variable") String variable) { + return "variable = " + variable; + } + } @Configuration(proxyBeanMethods = false) From 4070712f1126d4bc5c60cc7315c2cfd7574181e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flaviu=20Mure=C8=99an?= Date: Sat, 8 May 2021 14:14:57 +0200 Subject: [PATCH 23/59] Added reactor-kafka autoconfig to spring.factories (#1943) --- .../main/resources/META-INF/spring.factories | 1 + .../TraceKafkaAutoConfigurationTests.java | 76 +++++++++++++++++++ ...aceReactorKafkaAutoConfigurationTests.java | 51 +++++++++++++ .../instrument/kafka/KafkaConsumerTest.java | 2 +- .../instrument/kafka/KafkaProducerTest.java | 2 +- .../instrument/kafka/KafkaReceiverTest.java | 2 +- .../instrument/kafka/KafkaSenderTest.java | 2 +- .../TracingKafkaAutoConfigurationTest.java | 74 ++++++++++++++++++ ...cingReactorKafkaAutoConfigurationTest.java | 56 ++++++++++++++ .../instrument/kafka/KafkaConsumerTest.java | 2 +- .../instrument/kafka/KafkaProducerTest.java | 2 +- .../instrument/kafka/KafkaReceiverTest.java | 2 +- .../instrument/kafka/KafkaSenderTest.java | 2 +- 13 files changed, 266 insertions(+), 8 deletions(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TraceKafkaAutoConfigurationTests.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TraceReactorKafkaAutoConfigurationTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/TracingKafkaAutoConfigurationTest.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/TracingReactorKafkaAutoConfigurationTest.java diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index 7d7e98a50..7b8848a99 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,6 +1,7 @@ # Auto Configuration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.cloud.sleuth.autoconfig.instrument.kafka.TracingKafkaAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.kafka.TracingReactorKafkaAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncCustomAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncDefaultAutoConfiguration,\ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TraceKafkaAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TraceKafkaAutoConfigurationTests.java new file mode 100644 index 000000000..ea6560e38 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TraceKafkaAutoConfigurationTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.kafka; + +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.MockConsumer; +import org.apache.kafka.clients.consumer.OffsetResetStrategy; +import org.apache.kafka.clients.producer.MockProducer; +import org.apache.kafka.clients.producer.Producer; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaConsumer; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaProducer; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaPropagatorGetter; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaPropagatorSetter; + +import static org.assertj.core.api.Assertions.assertThat; + +class TraceKafkaAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.sleuth.noop.enabled=true") + .withConfiguration(AutoConfigurations.of(TraceNoOpAutoConfiguration.class, + TracingKafkaAutoConfiguration.class, TracingReactorKafkaAutoConfiguration.class)); + + @Test + void should_inject_beans_for_getter_setter_kafka_propagation() { + this.contextRunner.run(context -> assertThat(context).hasSingleBean(TracingKafkaPropagatorGetter.class) + .hasSingleBean(TracingKafkaPropagatorSetter.class)); + } + + @Test + void should_decorate_kafka_producer() { + this.contextRunner.withBean(Producer.class, MockProducer::new) + .run(context -> assertThat(context).hasSingleBean(TracingKafkaProducer.class)); + } + + @Test + void should_decorate_kafka_consumer() { + this.contextRunner.withBean(Consumer.class, () -> new MockConsumer<>(OffsetResetStrategy.NONE)) + .run(context -> assertThat(context).hasSingleBean(TracingKafkaConsumer.class)); + } + + @Test + void should_not_decorate_tracing_kafka_consumer() { + TracingKafkaConsumer kafkaConsumer = new TracingKafkaConsumer<>( + new MockConsumer<>(OffsetResetStrategy.NONE), null); + this.contextRunner.withBean(TracingKafkaConsumer.class, () -> kafkaConsumer) + .run(context -> assertThat(context).getBean(Consumer.class).isEqualTo(kafkaConsumer)); + } + + @Test + void should_not_decorate_tracing_kafka_producer() { + TracingKafkaProducer kafkaProducer = new TracingKafkaProducer<>(new MockProducer<>(), null); + this.contextRunner.withBean(TracingKafkaProducer.class, () -> kafkaProducer) + .run(context -> assertThat(context).getBean(Producer.class).isEqualTo(kafkaProducer)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TraceReactorKafkaAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TraceReactorKafkaAutoConfigurationTests.java new file mode 100644 index 000000000..4386651e8 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/kafka/TraceReactorKafkaAutoConfigurationTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.kafka; + +import org.junit.jupiter.api.Test; +import reactor.kafka.receiver.KafkaReceiver; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaConsumerFactory; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaProducerFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +class TraceReactorKafkaAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.sleuth.noop.enabled=true") + .withConfiguration(AutoConfigurations.of(TraceNoOpAutoConfiguration.class, + TracingKafkaAutoConfiguration.class, TracingReactorKafkaAutoConfiguration.class)); + + @Test + void should_not_create_factories_when_reactor_kafka_not_on_classpath() { + this.contextRunner.withClassLoader(new FilteredClassLoader(KafkaReceiver.class)) + .run(context -> assertThat(context).doesNotHaveBean(TracingKafkaProducerFactory.class) + .doesNotHaveBean(TracingKafkaConsumerFactory.class)); + } + + @Test + void should_create_factories_when_reactor_kafka_on_classpath() { + this.contextRunner.run(context -> assertThat(context).hasSingleBean(TracingKafkaProducerFactory.class) + .hasSingleBean(TracingKafkaConsumerFactory.class)); + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaConsumerTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaConsumerTest.java index fbe5cca26..46a7aaff3 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaConsumerTest.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaConsumerTest.java @@ -52,7 +52,7 @@ public class KafkaConsumerTest extends org.springframework.cloud.sleuth.instrume kafkaProducer.send(producerRecord); kafkaProducer.close(); - await().atMost(Duration.ofSeconds(5)).until(() -> receivedCounter.intValue() == 1); + await().atMost(Duration.ofSeconds(15)).until(() -> receivedCounter.intValue() == 1); BDDAssertions.then(this.tracer.currentSpan()).isNull(); BDDAssertions.then(this.spans).hasSize(1); diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java index 78f9e7a6e..79b25ed2b 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java @@ -46,7 +46,7 @@ public class KafkaProducerTest extends org.springframework.cloud.sleuth.instrume startKafkaConsumer(); this.kafkaProducer.send(producerRecord); - ConsumerRecord consumerRecord = consumerRecords.poll(5, TimeUnit.SECONDS); + ConsumerRecord consumerRecord = consumerRecords.poll(15, TimeUnit.SECONDS); BDDAssertions.then(consumerRecord).isNotNull(); BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-TraceId")).isNotNull(); diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaReceiverTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaReceiverTest.java index 5a3f14d75..6b26069bf 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaReceiverTest.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaReceiverTest.java @@ -51,7 +51,7 @@ public class KafkaReceiverTest extends org.springframework.cloud.sleuth.instrume producerRecord.headers().add("b3", "000000000000000a-000000000000000b-1-000000000000000a".getBytes()); kafkaProducer.send(producerRecord); - await().atMost(Duration.ofSeconds(5)).until(() -> receivedCounter.intValue() == 1); + await().atMost(Duration.ofSeconds(15)).until(() -> receivedCounter.intValue() == 1); BDDAssertions.then(this.tracer.currentSpan()).isNull(); BDDAssertions.then(this.spans).hasSize(1); diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaSenderTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaSenderTest.java index fb88a5b99..52b763679 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaSenderTest.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaSenderTest.java @@ -53,7 +53,7 @@ public class KafkaSenderTest extends org.springframework.cloud.sleuth.instrument Flux> senderResultFlux = this.kafkaSender .send(Mono.just(SenderRecord.create(producerRecord, null))); StepVerifier.create(senderResultFlux).expectNextCount(1).verifyComplete(); - ConsumerRecord consumerRecord = consumerRecords.poll(5, TimeUnit.SECONDS); + ConsumerRecord consumerRecord = consumerRecords.poll(15, TimeUnit.SECONDS); BDDAssertions.then(consumerRecord).isNotNull(); BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-TraceId")).isNotNull(); diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/TracingKafkaAutoConfigurationTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/TracingKafkaAutoConfigurationTest.java new file mode 100644 index 000000000..a0d0dd3cb --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/TracingKafkaAutoConfigurationTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.kafka; + +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.MockConsumer; +import org.apache.kafka.clients.consumer.OffsetResetStrategy; +import org.apache.kafka.clients.producer.MockProducer; +import org.apache.kafka.clients.producer.Producer; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaConsumer; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaProducer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.BDDAssertions.then; + +@SpringBootTest(classes = TracingKafkaAutoConfigurationTest.Config.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +public class TracingKafkaAutoConfigurationTest { + + @Autowired + Consumer kafkaConsumer; + + @Autowired + Producer kafkaProducer; + + @Test + public void should_wrap_kafka_consumer() { + then(this.kafkaConsumer).isNotNull(); + then(this.kafkaConsumer).isInstanceOf(TracingKafkaConsumer.class); + } + + @Test + public void should_wrap_kafka_producer() { + then(this.kafkaProducer).isNotNull(); + then(this.kafkaProducer).isInstanceOf(TracingKafkaProducer.class); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + protected static class Config { + + @Bean + Consumer kafkaConsumer() { + return new MockConsumer<>(OffsetResetStrategy.NONE); + } + + @Bean + Producer kafkaProducer() { + return new MockProducer<>(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/TracingReactorKafkaAutoConfigurationTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/TracingReactorKafkaAutoConfigurationTest.java new file mode 100644 index 000000000..4992bc281 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/TracingReactorKafkaAutoConfigurationTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.kafka; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaConsumerFactory; +import org.springframework.cloud.sleuth.instrument.kafka.TracingKafkaProducerFactory; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.BDDAssertions.then; + +@SpringBootTest(classes = TracingReactorKafkaAutoConfigurationTest.Config.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +public class TracingReactorKafkaAutoConfigurationTest { + + @Autowired + TracingKafkaConsumerFactory kafkaConsumerFactory; + + @Autowired + TracingKafkaProducerFactory kafkaProducerFactory; + + @Test + public void should_register_consumer_factory() { + then(this.kafkaConsumerFactory).isNotNull(); + } + + @Test + public void should_register_producer_factory() { + then(this.kafkaProducerFactory).isNotNull(); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + protected static class Config { + + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaConsumerTest.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaConsumerTest.java index cd84d04b3..9dc76c744 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaConsumerTest.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaConsumerTest.java @@ -128,7 +128,7 @@ public abstract class KafkaConsumerTest implements TestTracingAwareSupplier { kafkaProducer.send(producerRecord); kafkaProducer.close(); - await().atMost(Duration.ofSeconds(5)).until(() -> receivedCounter.intValue() == 1); + await().atMost(Duration.ofSeconds(15)).until(() -> receivedCounter.intValue() == 1); BDDAssertions.then(this.tracer.currentSpan()).isNull(); BDDAssertions.then(this.spans).hasSize(1); diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaProducerTest.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaProducerTest.java index f399eb190..8efc776be 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaProducerTest.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaProducerTest.java @@ -129,7 +129,7 @@ public abstract class KafkaProducerTest implements TestTracingAwareSupplier { ProducerRecord producerRecord = new ProducerRecord<>(testTopic, "test", "test"); this.kafkaProducer.send(producerRecord, callback); - await().atMost(Duration.ofSeconds(5)).until(acknowledged::get); + await().atMost(Duration.ofSeconds(15)).until(acknowledged::get); BDDAssertions.then(this.tracer.currentSpan()).isNull(); BDDAssertions.then(this.spans).hasSize(1); diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaReceiverTest.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaReceiverTest.java index e33f3eb7c..24a700620 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaReceiverTest.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaReceiverTest.java @@ -130,7 +130,7 @@ public abstract class KafkaReceiverTest implements TestTracingAwareSupplier { ProducerRecord producerRecord = new ProducerRecord<>(testTopic, "test", "test"); kafkaProducer.send(producerRecord); - await().atMost(Duration.ofSeconds(5)).until(() -> receivedCounter.intValue() == 1); + await().atMost(Duration.ofSeconds(15)).until(() -> receivedCounter.intValue() == 1); BDDAssertions.then(this.tracer.currentSpan()).isNull(); BDDAssertions.then(this.spans).hasSize(1); diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaSenderTest.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaSenderTest.java index 67db8e9cc..896132dbe 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaSenderTest.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaSenderTest.java @@ -134,7 +134,7 @@ public abstract class KafkaSenderTest implements TestTracingAwareSupplier { Flux> senderResultFlux = this.kafkaSender .send(Mono.just(SenderRecord.create(producerRecord, null))); StepVerifier.create(senderResultFlux).expectNextCount(1).verifyComplete(); - consumerRecords.poll(5, TimeUnit.SECONDS); + consumerRecords.poll(15, TimeUnit.SECONDS); BDDAssertions.then(this.tracer.currentSpan()).isNull(); BDDAssertions.then(this.spans).hasSize(1); From 3c8e93ebcadaa62eb9a244655511ab0ecaf3ebe3 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 10 May 2021 09:24:39 +0200 Subject: [PATCH 24/59] Disables tx for task --- .../task/SpringCloudTaskIntegrationTests.java | 214 +++++++++--------- 1 file changed, 107 insertions(+), 107 deletions(-) diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/task/SpringCloudTaskIntegrationTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/task/SpringCloudTaskIntegrationTests.java index e1c5f83f3..16722a222 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/task/SpringCloudTaskIntegrationTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/task/SpringCloudTaskIntegrationTests.java @@ -1,107 +1,107 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.task; - -import java.util.Iterator; -import java.util.Set; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.sleuth.exporter.FinishedSpan; -import org.springframework.cloud.sleuth.test.TestSpanHandler; -import org.springframework.cloud.task.configuration.EnableTask; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; - -import static org.assertj.core.api.BDDAssertions.then; - -@ContextConfiguration(classes = SpringCloudTaskIntegrationTests.TestConfig.class) -@TestPropertySource(properties = "spring.application.name=MyApplication") -public abstract class SpringCloudTaskIntegrationTests { - - @Autowired - TestSpanHandler spans; - - @Test - public void should_pass_tracing_information_when_using_spring_cloud_task() { - Set traceIds = this.spans.reportedSpans().stream().map(FinishedSpan::getTraceId) - .collect(Collectors.toSet()); - then(traceIds).as("There's one traceid").hasSize(1); - Set spanIds = this.spans.reportedSpans().stream().map(FinishedSpan::getSpanId) - .collect(Collectors.toSet()); - - then(spanIds).as("There are 3 spans").hasSize(3); - Iterator spanIterator = this.spans.reportedSpans().iterator(); - - FinishedSpan first = spanIterator.next(); - FinishedSpan second = spanIterator.next(); - FinishedSpan third = spanIterator.next(); - then(first.getName()).isEqualTo("myApplicationRunner"); - then(second.getName()).isEqualTo("myCommandLineRunner"); - then(third.getName()).isEqualTo("MyApplication"); - } - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration - @EnableTask - public static class TestConfig { - - @Bean - MyCommandLineRunner myCommandLineRunner() { - return new MyCommandLineRunner(); - } - - @Bean - MyApplicationRunner myApplicationRunner() { - return new MyApplicationRunner(); - } - - } - - static class MyCommandLineRunner implements CommandLineRunner { - - private static final Log log = LogFactory.getLog(MyCommandLineRunner.class); - - @Override - public void run(String... args) throws Exception { - log.info("Ran MyCommandLineRunner"); - } - - } - - static class MyApplicationRunner implements ApplicationRunner { - - private static final Log log = LogFactory.getLog(MyApplicationRunner.class); - - @Override - public void run(ApplicationArguments args) throws Exception { - log.info("Ran MyApplicationRunner"); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.task; + +import java.util.Iterator; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.cloud.task.configuration.EnableTask; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.BDDAssertions.then; + +@ContextConfiguration(classes = SpringCloudTaskIntegrationTests.TestConfig.class) +@TestPropertySource(properties = { "spring.application.name=MyApplication", "spring.sleuth.tx.enabled=false" }) +public abstract class SpringCloudTaskIntegrationTests { + + @Autowired + TestSpanHandler spans; + + @Test + public void should_pass_tracing_information_when_using_spring_cloud_task() { + Set traceIds = this.spans.reportedSpans().stream().map(FinishedSpan::getTraceId) + .collect(Collectors.toSet()); + then(traceIds).as("There's one traceid").hasSize(1); + Set spanIds = this.spans.reportedSpans().stream().map(FinishedSpan::getSpanId) + .collect(Collectors.toSet()); + + then(spanIds).as("There are 3 spans").hasSize(3); + Iterator spanIterator = this.spans.reportedSpans().iterator(); + + FinishedSpan first = spanIterator.next(); + FinishedSpan second = spanIterator.next(); + FinishedSpan third = spanIterator.next(); + then(first.getName()).isEqualTo("myApplicationRunner"); + then(second.getName()).isEqualTo("myCommandLineRunner"); + then(third.getName()).isEqualTo("MyApplication"); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @EnableTask + public static class TestConfig { + + @Bean + MyCommandLineRunner myCommandLineRunner() { + return new MyCommandLineRunner(); + } + + @Bean + MyApplicationRunner myApplicationRunner() { + return new MyApplicationRunner(); + } + + } + + static class MyCommandLineRunner implements CommandLineRunner { + + private static final Log log = LogFactory.getLog(MyCommandLineRunner.class); + + @Override + public void run(String... args) throws Exception { + log.info("Ran MyCommandLineRunner"); + } + + } + + static class MyApplicationRunner implements ApplicationRunner { + + private static final Log log = LogFactory.getLog(MyApplicationRunner.class); + + @Override + public void run(ApplicationArguments args) throws Exception { + log.info("Ran MyApplicationRunner"); + } + + } + +} From 742c23abdc646becacaa11c8c529da7a691f3349 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 10 May 2021 10:33:59 +0200 Subject: [PATCH 25/59] Paramterize the name in tests --- docs/pom.xml | 164 ++++---- spring-cloud-sleuth-dependencies/pom.xml | 356 +++++++++--------- .../task/SpringCloudTaskIntegrationTests.java | 214 +++++------ .../integration/sampled/WebClientTests.java | 6 +- 4 files changed, 363 insertions(+), 377 deletions(-) diff --git a/docs/pom.xml b/docs/pom.xml index cf86faac6..da80c56ff 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -1,82 +1,82 @@ - - - - 4.0.0 - - org.springframework.cloud - spring-cloud-sleuth - 3.1.0-SNAPSHOT - - spring-cloud-sleuth-docs - jar - Spring Cloud Sleuth Docs - Spring Sleuth Docs - - spring-cloud-sleuth - ${basedir}/.. - spring.sleuth.*|spring.zipkin.* - deploy - - - - ${project.groupId} - spring-cloud-starter-sleuth - - - ${project.groupId} - spring-cloud-sleuth-zipkin - - - - src/main/asciidoc - - - - docs - - - - pl.project13.maven - git-commit-id-plugin - - - maven-dependency-plugin - - - maven-resources-plugin - - - org.codehaus.mojo - exec-maven-plugin - - - org.asciidoctor - asciidoctor-maven-plugin - - - maven-antrun-plugin - - - maven-deploy-plugin - - - - - - + + + + 4.0.0 + + org.springframework.cloud + spring-cloud-sleuth + 3.1.0-SNAPSHOT + + spring-cloud-sleuth-docs + jar + Spring Cloud Sleuth Docs + Spring Sleuth Docs + + spring-cloud-sleuth + ${basedir}/.. + spring.sleuth.*|spring.zipkin.* + deploy + + + + ${project.groupId} + spring-cloud-starter-sleuth + + + ${project.groupId} + spring-cloud-sleuth-zipkin + + + + src/main/asciidoc + + + + docs + + + + pl.project13.maven + git-commit-id-plugin + + + maven-dependency-plugin + + + maven-resources-plugin + + + org.codehaus.mojo + exec-maven-plugin + + + org.asciidoctor + asciidoctor-maven-plugin + + + maven-antrun-plugin + + + maven-deploy-plugin + + + + + + diff --git a/spring-cloud-sleuth-dependencies/pom.xml b/spring-cloud-sleuth-dependencies/pom.xml index 3f2160aa4..696cbadf2 100644 --- a/spring-cloud-sleuth-dependencies/pom.xml +++ b/spring-cloud-sleuth-dependencies/pom.xml @@ -1,187 +1,169 @@ - - - - - 4.0.0 - - spring-cloud-dependencies-parent - org.springframework.cloud - 3.0.2 - - - spring-cloud-sleuth-dependencies - 3.1.0-SNAPSHOT - pom - spring-cloud-sleuth-dependencies - Spring Cloud Sleuth Dependencies - - - https://github.com/spring-cloud/spring-cloud-sleuth - scm:git:git://github.com/spring-cloud/spring-cloud-sleuth.git - - - scm:git:ssh://git@github.com/spring-cloud/spring-cloud-sleuth.git - - HEAD - - - - 5.13.2 - 0.37.4 - 4.2.2 - - - - - org.springframework.cloud - spring-cloud-sleuth-autoconfigure - ${project.version} - - - org.springframework.cloud - spring-cloud-sleuth-api - ${project.version} - - - org.springframework.cloud - spring-cloud-sleuth-instrumentation - ${project.version} - - - org.springframework.cloud - spring-cloud-sleuth-brave - ${project.version} - - - org.springframework.cloud - spring-cloud-sleuth-zipkin - ${project.version} - - - org.springframework.cloud - spring-cloud-starter-sleuth - ${project.version} - - - - org.springframework.cloud - spring-cloud-sleuth-tests-common - ${project.version} - - - - io.zipkin.brave - brave-bom - ${brave.version} - pom - import - - - io.opentracing.brave - brave-opentracing - ${brave.opentracing.version} - - - io.zipkin.brave - * - - - - - - io.github.lognet - grpc-spring-boot-starter - ${grpc.spring.boot.version} - - - - - - spring - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - false - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - - - - - - false - - bintray-open-telemetry-maven - bintray - https://dl.bintray.com/open-telemetry/maven - - - - true - - bintray-open-telemetry-maven-snapshot - bintray - https://oss.jfrog.org/oss-snapshot-local/ - - - + + + + + 4.0.0 + + spring-cloud-dependencies-parent + org.springframework.cloud + 3.0.2 + + + spring-cloud-sleuth-dependencies + 3.1.0-SNAPSHOT + pom + spring-cloud-sleuth-dependencies + Spring Cloud Sleuth Dependencies + + + https://github.com/spring-cloud/spring-cloud-sleuth + scm:git:git://github.com/spring-cloud/spring-cloud-sleuth.git + + + scm:git:ssh://git@github.com/spring-cloud/spring-cloud-sleuth.git + + HEAD + + + + 5.13.2 + 0.37.4 + 4.2.2 + + + + + org.springframework.cloud + spring-cloud-sleuth-autoconfigure + ${project.version} + + + org.springframework.cloud + spring-cloud-sleuth-api + ${project.version} + + + org.springframework.cloud + spring-cloud-sleuth-instrumentation + ${project.version} + + + org.springframework.cloud + spring-cloud-sleuth-brave + ${project.version} + + + org.springframework.cloud + spring-cloud-sleuth-zipkin + ${project.version} + + + org.springframework.cloud + spring-cloud-starter-sleuth + ${project.version} + + + + org.springframework.cloud + spring-cloud-sleuth-tests-common + ${project.version} + + + + io.zipkin.brave + brave-bom + ${brave.version} + pom + import + + + io.opentracing.brave + brave-opentracing + ${brave.opentracing.version} + + + io.zipkin.brave + * + + + + + + io.github.lognet + grpc-spring-boot-starter + ${grpc.spring.boot.version} + + + + + + spring + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/task/SpringCloudTaskIntegrationTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/task/SpringCloudTaskIntegrationTests.java index 16722a222..cd55d5174 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/task/SpringCloudTaskIntegrationTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/task/SpringCloudTaskIntegrationTests.java @@ -1,107 +1,107 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.task; - -import java.util.Iterator; -import java.util.Set; -import java.util.stream.Collectors; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.cloud.sleuth.exporter.FinishedSpan; -import org.springframework.cloud.sleuth.test.TestSpanHandler; -import org.springframework.cloud.task.configuration.EnableTask; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; - -import static org.assertj.core.api.BDDAssertions.then; - -@ContextConfiguration(classes = SpringCloudTaskIntegrationTests.TestConfig.class) -@TestPropertySource(properties = { "spring.application.name=MyApplication", "spring.sleuth.tx.enabled=false" }) -public abstract class SpringCloudTaskIntegrationTests { - - @Autowired - TestSpanHandler spans; - - @Test - public void should_pass_tracing_information_when_using_spring_cloud_task() { - Set traceIds = this.spans.reportedSpans().stream().map(FinishedSpan::getTraceId) - .collect(Collectors.toSet()); - then(traceIds).as("There's one traceid").hasSize(1); - Set spanIds = this.spans.reportedSpans().stream().map(FinishedSpan::getSpanId) - .collect(Collectors.toSet()); - - then(spanIds).as("There are 3 spans").hasSize(3); - Iterator spanIterator = this.spans.reportedSpans().iterator(); - - FinishedSpan first = spanIterator.next(); - FinishedSpan second = spanIterator.next(); - FinishedSpan third = spanIterator.next(); - then(first.getName()).isEqualTo("myApplicationRunner"); - then(second.getName()).isEqualTo("myCommandLineRunner"); - then(third.getName()).isEqualTo("MyApplication"); - } - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration - @EnableTask - public static class TestConfig { - - @Bean - MyCommandLineRunner myCommandLineRunner() { - return new MyCommandLineRunner(); - } - - @Bean - MyApplicationRunner myApplicationRunner() { - return new MyApplicationRunner(); - } - - } - - static class MyCommandLineRunner implements CommandLineRunner { - - private static final Log log = LogFactory.getLog(MyCommandLineRunner.class); - - @Override - public void run(String... args) throws Exception { - log.info("Ran MyCommandLineRunner"); - } - - } - - static class MyApplicationRunner implements ApplicationRunner { - - private static final Log log = LogFactory.getLog(MyApplicationRunner.class); - - @Override - public void run(ApplicationArguments args) throws Exception { - log.info("Ran MyApplicationRunner"); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.task; + +import java.util.Iterator; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.cloud.task.configuration.EnableTask; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.BDDAssertions.then; + +@ContextConfiguration(classes = SpringCloudTaskIntegrationTests.TestConfig.class) +@TestPropertySource(properties = { "spring.application.name=MyApplication", "spring.sleuth.tx.enabled=false" }) +public abstract class SpringCloudTaskIntegrationTests { + + @Autowired + TestSpanHandler spans; + + @Test + public void should_pass_tracing_information_when_using_spring_cloud_task() { + Set traceIds = this.spans.reportedSpans().stream().map(FinishedSpan::getTraceId) + .collect(Collectors.toSet()); + then(traceIds).as("There's one traceid").hasSize(1); + Set spanIds = this.spans.reportedSpans().stream().map(FinishedSpan::getSpanId) + .collect(Collectors.toSet()); + + then(spanIds).as("There are 3 spans").hasSize(3); + Iterator spanIterator = this.spans.reportedSpans().iterator(); + + FinishedSpan first = spanIterator.next(); + FinishedSpan second = spanIterator.next(); + FinishedSpan third = spanIterator.next(); + then(first.getName()).isEqualTo("myApplicationRunner"); + then(second.getName()).isEqualTo("myCommandLineRunner"); + then(third.getName()).isEqualTo("MyApplication"); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @EnableTask + public static class TestConfig { + + @Bean + MyCommandLineRunner myCommandLineRunner() { + return new MyCommandLineRunner(); + } + + @Bean + MyApplicationRunner myApplicationRunner() { + return new MyApplicationRunner(); + } + + } + + static class MyCommandLineRunner implements CommandLineRunner { + + private static final Log log = LogFactory.getLog(MyCommandLineRunner.class); + + @Override + public void run(String... args) throws Exception { + log.info("Ran MyCommandLineRunner"); + } + + } + + static class MyApplicationRunner implements ApplicationRunner { + + private static final Log log = LogFactory.getLog(MyApplicationRunner.class); + + @Override + public void run(ApplicationArguments args) throws Exception { + log.info("Ran MyApplicationRunner"); + } + + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/integration/sampled/WebClientTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/integration/sampled/WebClientTests.java index 6f9f9c9ab..7af97c32b 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/integration/sampled/WebClientTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/integration/sampled/WebClientTests.java @@ -269,7 +269,11 @@ public abstract class WebClientTests { thenThereIsNoCurrentSpan(); then(this.spans.reportedSpans().stream().filter(r -> r.getKind() == Span.Kind.CLIENT).map(r -> r.getName()) - .collect(Collectors.toList())).isNotEmpty().contains("GET /prefix/{variable}/suffix"); + .collect(Collectors.toList())).isNotEmpty().contains(templatedName()); + } + + protected String templatedName() { + return "GET /prefix/{variable}/suffix"; } /** From 181c789020063ac9e6fdf53d6ebab2ffb398d13c Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 13 May 2021 09:12:04 +0200 Subject: [PATCH 26/59] Adds R2DBC support (#1946) fixes gh-1524 --- docs/src/main/asciidoc/integrations.adoc | 8 + .../springframework/cloud/sleuth/Span.java | 435 +++++----- spring-cloud-sleuth-autoconfigure/pom.xml | 5 + ...aceConnectionFactoryBeanPostProcessor.java | 53 ++ .../TraceProxyConnectionFactoryWrapper.java | 54 ++ .../r2dbc/TraceR2dbcAutoConfiguration.java | 51 ++ ...itional-spring-configuration-metadata.json | 6 + .../main/resources/META-INF/spring.factories | 1 + ...eRpcAutoConfigurationIntegrationTests.java | 5 +- .../instrument/web/client/WebClientTests.java | 6 +- ...aceAsyncDefaultAutoConfigurationTests.java | 5 +- .../TraceQuartzAutoConfigurationTest.java | 5 +- ...nnectionFactoryBeanPostProcessorTests.java | 54 ++ .../TraceR2dbcAutoConfigurationTests.java | 46 ++ .../BraveWebClientAutoConfigurationTests.java | 5 +- .../instrument/web/client/GH846Tests.java | 5 +- .../client/TraceWebClientDisabledTests.java | 5 +- .../zipkin2/ZipkinSamplerTests.java | 5 +- .../src/test/resources/application.yml | 6 +- spring-cloud-sleuth-brave/pom.xml | 750 +++++++++--------- .../sleuth/brave/bridge/BraveSpanBuilder.java | 269 ++++--- spring-cloud-sleuth-instrumentation/pom.xml | 5 + .../r2dbc/TraceProxyExecutionListener.java | 120 +++ .../tx/TraceReactiveTransactionManager.java | 2 +- .../TraceProxyExecutionListenerTests.java | 141 ++++ .../cloud/sleuth/tracer/SimpleSpan.java | 13 + .../sleuth/tracer/SimpleSpanBuilder.java | 105 +++ .../cloud/sleuth/tracer/SimpleTracer.java | 14 +- tests/brave/pom.xml | 1 + .../baggage/MultipleHopsIntegrationTests.java | 6 +- .../JmsTracingConfigurationTest.java | 6 +- .../issue_943/HelloSpringIntegration.java | 5 +- .../pom.xml | 94 +++ .../r2dbc/R2dbcIntegrationTests.java | 52 ++ .../src/test/resources/application.yml | 1 + tests/common/pom.xml | 10 + .../r2dbc/R2dbcIntegrationTests.java | 100 +++ .../ReactiveContinuedTransactionService.java | 58 ++ .../instrument/r2dbc/ReactiveCustomer.java | 56 ++ .../r2dbc/ReactiveCustomerRepository.java | 30 + .../ReactiveNestedTransactionService.java | 44 + .../r2dbc/ReactiveNewTransactionService.java | 57 ++ tests/common/src/main/resources/schema.sql | 1 + 43 files changed, 1967 insertions(+), 733 deletions(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceConnectionFactoryBeanPostProcessor.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceProxyConnectionFactoryWrapper.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceR2dbcAutoConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceConnectionFactoryBeanPostProcessorTests.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceR2dbcAutoConfigurationTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListener.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListenerTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/pom.xml create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/r2dbc/R2dbcIntegrationTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/src/test/resources/application.yml create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/R2dbcIntegrationTests.java create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveContinuedTransactionService.java create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveCustomer.java create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveCustomerRepository.java create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveNestedTransactionService.java create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveNewTransactionService.java create mode 100644 tests/common/src/main/resources/schema.sql diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 12670a2b0..04f70fe95 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -626,3 +626,11 @@ This feature is available for all tracer implementations. If you have Spring Tx on the classpath we will instrument the `PlatformTransactionManager` and the `ReactiveTransactionManager` to create a span whenever a new transaction is created. In order to disable this instrumentation set `spring.sleuth.tx.enabled` to `false`. + +[[sleuth-r2dbc-integration]] +== R2DBC + +This feature is available for all tracer implementations. + +If you have R2DBC Proxy on the classpath we will instrument the `ConnectionFactory`so that it contains a custom `ProxyExecutionListener`. +In order to disable this instrumentation set `spring.sleuth.r2dbc.enabled` to `false`. diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Span.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Span.java index 74373c54d..6cc8c84c3 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Span.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Span.java @@ -1,213 +1,222 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth; - -import org.springframework.cloud.sleuth.propagation.Propagator; - -/** - * - * This API was heavily influenced by Brave. Parts of its documentation were taken - * directly from Brave. - * - * Span is a single unit of work that needs to be started and stopped. Contains timing - * information and events and tags. - * - * @author OpenZipkin Brave Authors - * @author Marcin Grzejszczak - * @since 3.0.0 - */ -public interface Span extends SpanCustomizer { - - /** - * @return {@code true} when no recording is done and nothing is reported to an - * external system. However, this span should still be injected into outgoing - * requests. Use this flag to avoid performing expensive computation. - */ - boolean isNoop(); - - /** - * @return {@link TraceContext} corresponding to this span. - */ - TraceContext context(); - - /** - * Starts this span. - * @return this span - */ - Span start(); - - /** - * Sets a name on this span. - * @param name name to set on the span - * @return this span - */ - Span name(String name); - - /** - * Sets an event on this span. - * @param value event name to set on the span - * @return this span - */ - Span event(String value); - - /** - * Sets a tag on this span. - * @param key tag key - * @param value tag value - * @return this span - */ - Span tag(String key, String value); - - /** - * Records an exception for this span. - * @param throwable to record - * @return this span - */ - Span error(Throwable throwable); - - /** - * Ends the span. The span gets stopped and recorded if not noop. - */ - void end(); - - /** - * Ends the span. The span gets stopped but does not get recorded. - */ - void abandon(); - - /** - * Sets the remote service name for the span. - * @param remoteServiceName remote service name - * @return this span - * @since 3.0.3 - */ - default Span remoteServiceName(String remoteServiceName) { - return this; - } - - /** - * Type of span. Can be used to specify additional relationships between spans in - * addition to a parent/child relationship. - * - * Documentation of the enum taken from OpenTelemetry. - */ - enum Kind { - - /** - * Indicates that the span covers server-side handling of an RPC or other remote - * request. - */ - SERVER, - - /** - * Indicates that the span covers the client-side wrapper around an RPC or other - * remote request. - */ - CLIENT, - - /** - * Indicates that the span describes producer sending a message to a broker. - * Unlike client and server, there is no direct critical path latency relationship - * between producer and consumer spans. - */ - PRODUCER, - - /** - * Indicates that the span describes consumer receiving a message from a broker. - * Unlike client and server, there is no direct critical path latency relationship - * between producer and consumer spans. - */ - CONSUMER - - } - - /** - * In some cases (e.g. when dealing with - * {@link Propagator#extract(Object, Propagator.Getter)}'s we want to create a span - * that has not yet been started, yet it's heavily configurable (some options are not - * possible to be set when a span has already been started). We can achieve that by - * using a builder. - * - * Inspired by OpenZipkin Brave and OpenTelemetry API. - */ - interface Builder { - - /** - * Sets the parent of the built span. - * @param context parent's context - * @return this - */ - Builder setParent(TraceContext context); - - /** - * Sets no parent of the built span. - * @return this - */ - Builder setNoParent(); - - /** - * Sets the name of the span. - * @param name span name - * @return this - */ - Builder name(String name); - - /** - * Sets an event on the span. - * @param value event value - * @return this - */ - Builder event(String value); - - /** - * Sets a tag on the span. - * @param key tag key - * @param value tag value - * @return this - */ - Builder tag(String key, String value); - - /** - * Sets an error on the span. - * @param throwable error to set - * @return this - */ - Builder error(Throwable throwable); - - /** - * Sets the kind on the span. - * @param spanKind kind of the span - * @return this - */ - Builder kind(Span.Kind spanKind); - - /** - * Sets the remote service name for the span. - * @param remoteServiceName remote service name - * @return this - */ - Builder remoteServiceName(String remoteServiceName); - - /** - * Builds and starts the span. - * @return started span - */ - Span start(); - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth; + +import org.springframework.cloud.sleuth.propagation.Propagator; + +/** + * + * This API was heavily influenced by Brave. Parts of its documentation were taken + * directly from Brave. + * + * Span is a single unit of work that needs to be started and stopped. Contains timing + * information and events and tags. + * + * @author OpenZipkin Brave Authors + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +public interface Span extends SpanCustomizer { + + /** + * @return {@code true} when no recording is done and nothing is reported to an + * external system. However, this span should still be injected into outgoing + * requests. Use this flag to avoid performing expensive computation. + */ + boolean isNoop(); + + /** + * @return {@link TraceContext} corresponding to this span. + */ + TraceContext context(); + + /** + * Starts this span. + * @return this span + */ + Span start(); + + /** + * Sets a name on this span. + * @param name name to set on the span + * @return this span + */ + Span name(String name); + + /** + * Sets an event on this span. + * @param value event name to set on the span + * @return this span + */ + Span event(String value); + + /** + * Sets a tag on this span. + * @param key tag key + * @param value tag value + * @return this span + */ + Span tag(String key, String value); + + /** + * Records an exception for this span. + * @param throwable to record + * @return this span + */ + Span error(Throwable throwable); + + /** + * Ends the span. The span gets stopped and recorded if not noop. + */ + void end(); + + /** + * Ends the span. The span gets stopped but does not get recorded. + */ + void abandon(); + + /** + * Sets the remote service name for the span. + * @param remoteServiceName remote service name + * @return this span + * @since 3.0.3 + */ + default Span remoteServiceName(String remoteServiceName) { + return this; + } + + /** + * Type of span. Can be used to specify additional relationships between spans in + * addition to a parent/child relationship. + * + * Documentation of the enum taken from OpenTelemetry. + */ + enum Kind { + + /** + * Indicates that the span covers server-side handling of an RPC or other remote + * request. + */ + SERVER, + + /** + * Indicates that the span covers the client-side wrapper around an RPC or other + * remote request. + */ + CLIENT, + + /** + * Indicates that the span describes producer sending a message to a broker. + * Unlike client and server, there is no direct critical path latency relationship + * between producer and consumer spans. + */ + PRODUCER, + + /** + * Indicates that the span describes consumer receiving a message from a broker. + * Unlike client and server, there is no direct critical path latency relationship + * between producer and consumer spans. + */ + CONSUMER + + } + + /** + * In some cases (e.g. when dealing with + * {@link Propagator#extract(Object, Propagator.Getter)}'s we want to create a span + * that has not yet been started, yet it's heavily configurable (some options are not + * possible to be set when a span has already been started). We can achieve that by + * using a builder. + * + * Inspired by OpenZipkin Brave and OpenTelemetry API. + */ + interface Builder { + + /** + * Sets the parent of the built span. + * @param context parent's context + * @return this + */ + Builder setParent(TraceContext context); + + /** + * Sets no parent of the built span. + * @return this + */ + Builder setNoParent(); + + /** + * Sets the name of the span. + * @param name span name + * @return this + */ + Builder name(String name); + + /** + * Sets an event on the span. + * @param value event value + * @return this + */ + Builder event(String value); + + /** + * Sets a tag on the span. + * @param key tag key + * @param value tag value + * @return this + */ + Builder tag(String key, String value); + + /** + * Sets an error on the span. + * @param throwable error to set + * @return this + */ + Builder error(Throwable throwable); + + /** + * Sets the kind on the span. + * @param spanKind kind of the span + * @return this + */ + Builder kind(Span.Kind spanKind); + + /** + * Sets the remote service name for the span. + * @param remoteServiceName remote service name + * @return this + */ + Builder remoteServiceName(String remoteServiceName); + + /** + * Sets the remote URL for the span. + * @param remoteUrl remote service name + * @return this + */ + default Builder remoteUrl(String remoteUrl) { + return this; + } + + /** + * Builds and starts the span. + * @return started span + */ + Span start(); + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index 970d8d5a7..4be621272 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -133,6 +133,11 @@ rxjava true + + io.r2dbc + r2dbc-proxy + true + io.github.openfeign feign-okhttp diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceConnectionFactoryBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceConnectionFactoryBeanPostProcessor.java new file mode 100644 index 000000000..cf6cdaf2e --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceConnectionFactoryBeanPostProcessor.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.r2dbc; + +import io.r2dbc.spi.ConnectionFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; + +/** + * Adds a tracing listener to the {@link ConnectionFactory}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceConnectionFactoryBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + public TraceConnectionFactoryBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof ConnectionFactory) { + return wrapConnectionFactory((ConnectionFactory) bean); + } + return bean; + } + + ConnectionFactory wrapConnectionFactory(ConnectionFactory bean) { + TraceProxyConnectionFactoryWrapper proxyPostProcessor = new TraceProxyConnectionFactoryWrapper( + this.beanFactory); + return proxyPostProcessor.apply(bean); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceProxyConnectionFactoryWrapper.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceProxyConnectionFactoryWrapper.java new file mode 100644 index 000000000..453e0d851 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceProxyConnectionFactoryWrapper.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.r2dbc; + +import java.util.function.Function; + +import io.r2dbc.proxy.ProxyConnectionFactory; +import io.r2dbc.proxy.callback.ProxyConfig; +import io.r2dbc.spi.ConnectionFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.sleuth.instrument.r2dbc.TraceProxyExecutionListener; + +class TraceProxyConnectionFactoryWrapper implements Function { + + private final BeanFactory beanFactory; + + private ObjectProvider proxyConfig; + + TraceProxyConnectionFactoryWrapper(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public ConnectionFactory apply(ConnectionFactory connectionFactory) { + ProxyConnectionFactory.Builder builder = ProxyConnectionFactory.builder(connectionFactory); + proxyConfig().ifAvailable(builder::proxyConfig); + builder.listener(new TraceProxyExecutionListener(this.beanFactory, connectionFactory)); + return builder.build(); + } + + private ObjectProvider proxyConfig() { + if (this.proxyConfig == null) { + this.proxyConfig = this.beanFactory.getBeanProvider(ProxyConfig.class); + } + return this.proxyConfig; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceR2dbcAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceR2dbcAutoConfiguration.java new file mode 100644 index 000000000..4e9d1fed1 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceR2dbcAutoConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.r2dbc; + +import io.r2dbc.proxy.callback.ProxyConfig; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} enables Quartz span information propagation. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(ProxyConfig.class) +@ConditionalOnBean(Tracer.class) +@ConditionalOnProperty(value = "spring.sleuth.r2dbc.enabled", matchIfMissing = true) +@AutoConfigureAfter({ BraveAutoConfiguration.class, R2dbcAutoConfiguration.class }) +public class TraceR2dbcAutoConfiguration { + + @Bean + static TraceConnectionFactoryBeanPostProcessor traceConnectionFactoryBeanPostProcessor(BeanFactory beanFactory) { + return new TraceConnectionFactoryBeanPostProcessor(beanFactory); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 9a894ba23..97669ed28 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -172,6 +172,12 @@ "type": "java.lang.Boolean", "description": "Enable Spring Batch instrumentation.", "defaultValue": true + }, + { + "name": "spring.sleuth.r2dbc.enabled", + "type": "java.lang.Boolean", + "description": "Enable R2dbc instrumentation.", + "defaultValue": true } ] } diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index 7b8848a99..e4f77461c 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -23,6 +23,7 @@ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringInte org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringMessagingAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceWebSocketAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.rsocket.TraceRSocketAutoConfiguration, \ +org.springframework.cloud.sleuth.autoconfig.instrument.r2dbc.TraceR2dbcAutoConfiguration, \ org.springframework.cloud.sleuth.autoconfig.instrument.tx.TraceTxAutoConfiguration, \ org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.brave.instrument.web.client.BraveWebClientAutoConfiguration,\ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java index 43c4f9420..0c2ea1e76 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java @@ -29,8 +29,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; import org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration; @@ -59,7 +61,8 @@ public class BraveRpcAutoConfigurationIntegrationTests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, - MongoAutoConfiguration.class, QuartzAutoConfiguration.class }) + MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, + R2dbcDataAutoConfiguration.class }) @Configuration(proxyBeanMethods = false) public static class Config { diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java index d874d7544..0e9cf4255 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java @@ -41,6 +41,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.server.LocalServerPort; import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; @@ -145,8 +147,8 @@ public class WebClientTests { } @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration( - exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class }) + @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, + R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class }) @DisableSecurity public static class TestConfiguration { diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java index 18bf5a1dd..4f001fb06 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java @@ -25,8 +25,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; import org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration; @@ -50,7 +52,8 @@ public class TraceAsyncDefaultAutoConfigurationTests { @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, - MongoAutoConfiguration.class, QuartzAutoConfiguration.class }) + MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, + R2dbcDataAutoConfiguration.class }) static class Config { @Bean diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java index 8ed2cd5be..f5f6435f5 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java @@ -27,8 +27,10 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; import org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration; @@ -116,7 +118,8 @@ public class TraceQuartzAutoConfigurationTest { @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, - MongoAutoConfiguration.class, QuartzAutoConfiguration.class }) + MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, + R2dbcDataAutoConfiguration.class }) public static class EnableAutoConfig { } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceConnectionFactoryBeanPostProcessorTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceConnectionFactoryBeanPostProcessorTests.java new file mode 100644 index 000000000..0e58cafb4 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceConnectionFactoryBeanPostProcessorTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.r2dbc; + +import io.r2dbc.spi.ConnectionFactory; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; + +import org.springframework.beans.factory.support.StaticListableBeanFactory; + +class TraceConnectionFactoryBeanPostProcessorTests { + + @Test + void should_do_nothing_when_bean_not_connection_factory() { + TraceConnectionFactoryBeanPostProcessor processor = new TraceConnectionFactoryBeanPostProcessor(null) { + @Override + ConnectionFactory wrapConnectionFactory(ConnectionFactory bean) { + throw new AssertionError("This method must not be called"); + } + }; + Object before = new Object(); + + Object after = processor.postProcessAfterInitialization(before, ""); + + BDDAssertions.then(after).isSameAs(before); + } + + @Test + void should_modify_the_connection_factory() { + TraceConnectionFactoryBeanPostProcessor processor = new TraceConnectionFactoryBeanPostProcessor( + new StaticListableBeanFactory()); + ConnectionFactory before = BDDMockito.mock(ConnectionFactory.class); + + Object after = processor.postProcessAfterInitialization(before, ""); + + BDDAssertions.then(after).isNotSameAs(before); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceR2dbcAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceR2dbcAutoConfigurationTests.java new file mode 100644 index 000000000..06c87fe45 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/r2dbc/TraceR2dbcAutoConfigurationTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.r2dbc; + +import io.r2dbc.proxy.callback.ProxyConfig; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; + +class TraceR2dbcAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.sleuth.noop.enabled=true").withConfiguration( + AutoConfigurations.of(TraceNoOpAutoConfiguration.class, TraceR2dbcAutoConfiguration.class)); + + @Test + void should_register_trace_bean_post_processor() { + this.contextRunner.run( + context -> Assertions.assertThat(context).hasSingleBean(TraceConnectionFactoryBeanPostProcessor.class)); + } + + @Test + void should_not_create_trace_bean_post_processor_when_no_proxy_on_classpath() { + this.contextRunner.withClassLoader(new FilteredClassLoader(ProxyConfig.class)).run(context -> Assertions + .assertThat(context).doesNotHaveBean(TraceConnectionFactoryBeanPostProcessor.class)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java index 4767c32cf..8306face3 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java @@ -29,8 +29,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; @@ -120,7 +122,8 @@ public class BraveWebClientAutoConfigurationTests { @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, - MongoAutoConfiguration.class, QuartzAutoConfiguration.class }) + MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, + R2dbcDataAutoConfiguration.class }) static class Config { // custom builder diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java index 9e0bf7b62..0fd557aad 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java @@ -23,8 +23,10 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; @@ -54,7 +56,8 @@ public class GH846Tests { @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, - MongoAutoConfiguration.class, QuartzAutoConfiguration.class }) + MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, + R2dbcDataAutoConfiguration.class }) static class App { @Bean diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java index 6875a7dc9..43e9d29d9 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java @@ -20,8 +20,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; import org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration; @@ -43,7 +45,8 @@ public class TraceWebClientDisabledTests { @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, - MongoAutoConfiguration.class, QuartzAutoConfiguration.class }) + MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, + R2dbcDataAutoConfiguration.class }) public static class Config { } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java index d1dcbb7e8..f3497539d 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java @@ -22,8 +22,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; @@ -53,7 +55,8 @@ public class ZipkinSamplerTests { @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, - MongoAutoConfiguration.class, QuartzAutoConfiguration.class }) + MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, + R2dbcDataAutoConfiguration.class }) static class TestConfig { } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml b/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml index 7712f8c1c..1e05c6764 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml +++ b/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml @@ -1,3 +1,3 @@ -logging.level.org.springframework.cloud: DEBUG - -spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration, org.springframework.cloud.gateway.config.GatewayAutoConfiguration, org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration, org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration, org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration +logging.level.org.springframework.cloud: DEBUG + +spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration, org.springframework.cloud.gateway.config.GatewayAutoConfiguration, org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration, org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration, org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration, org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration, org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration diff --git a/spring-cloud-sleuth-brave/pom.xml b/spring-cloud-sleuth-brave/pom.xml index 1069e4dbe..1d66e9487 100644 --- a/spring-cloud-sleuth-brave/pom.xml +++ b/spring-cloud-sleuth-brave/pom.xml @@ -1,375 +1,375 @@ - - - - - 4.0.0 - - spring-cloud-sleuth-brave - jar - Spring Cloud Sleuth Brave - Spring Cloud Sleuth Brave - - - org.springframework.cloud - spring-cloud-sleuth - 3.1.0-SNAPSHOT - .. - - - - - org.springframework.cloud - spring-cloud-sleuth-instrumentation - - - org.springframework.boot - spring-boot-starter-web - true - - - io.micrometer - micrometer-core - true - - - org.springframework.boot - spring-boot-starter-webflux - true - - - io.projectreactor - reactor-core - true - - - io.projectreactor.netty - reactor-netty-http - true - - - org.reactivestreams - reactive-streams - true - - - org.springframework.boot - spring-boot-starter-websocket - true - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.boot - spring-boot-starter-actuator - true - - - org.springframework.cloud - spring-cloud-commons - - - org.springframework.cloud - spring-cloud-stream - true - - - org.springframework.cloud - spring-cloud-starter-gateway - true - - - org.springframework.cloud - spring-cloud-starter-circuitbreaker-resilience4j - true - - - org.springframework.cloud - spring-cloud-starter-openfeign - true - - - org.springframework.cloud - spring-cloud-function-context - true - - - org.springframework.integration - spring-integration-core - true - - - org.springframework.amqp - spring-rabbit - true - - - org.springframework.kafka - spring-kafka - true - - - org.apache.kafka - kafka-streams - true - - - org.springframework.boot - spring-boot-starter-security - true - - - org.springframework - spring-context - - - org.springframework.cloud - spring-cloud-context - true - - - org.springframework.cloud - spring-cloud-starter-loadbalancer - true - - - io.github.openfeign - feign-core - true - - - io.github.openfeign.form - feign-form-spring - true - - - io.reactivex - rxjava - true - - - com.squareup.okhttp3 - okhttp - ${okhttp.version} - true - - - org.apache.httpcomponents - httpclient - true - - - io.github.openfeign - feign-okhttp - true - - - org.springframework.boot - spring-boot-starter-data-mongodb - true - - - org.aspectj - aspectjrt - - - - io.zipkin.brave - brave - - - io.zipkin.reporter2 - * - - - io.zipkin.zipkin2 - * - - - - - io.zipkin.brave - brave-context-slf4j - - - io.zipkin.brave - brave-instrumentation-messaging - - - io.zipkin.brave - brave-instrumentation-rpc - - - io.zipkin.brave - brave-instrumentation-spring-rabbit - - - io.zipkin.brave - brave-instrumentation-kafka-clients - - - io.zipkin.brave - brave-instrumentation-kafka-streams - - - io.zipkin.brave - brave-instrumentation-httpclient - - - io.zipkin.brave - brave-instrumentation-httpasyncclient - - - io.zipkin.brave - brave-instrumentation-jms - - - io.zipkin.brave - brave-instrumentation-mongodb - - - io.zipkin.aws - brave-propagation-aws - - - javax.jms - javax.jms-api - true - - - io.opentracing.brave - brave-opentracing - true - - - org.apache.httpcomponents - httpasyncclient - true - - - org.springframework - spring-jms - true - - - - io.github.lognet - grpc-spring-boot-starter - true - - - org.springframework.boot - spring-boot-starter - - - - - io.zipkin.brave - brave-instrumentation-grpc - true - - - io.zipkin.reporter2 - zipkin-reporter-metrics-micrometer - - - io.micrometer - micrometer-core - - - - - - io.lettuce - lettuce-core - true - - - - org.springframework.boot - spring-boot-starter-quartz - true - - - org.springframework.boot - spring-boot-autoconfigure-processor - true - - - - org.springframework.boot - spring-boot-starter-test - test - - - io.zipkin.brave - brave-instrumentation-http-tests - test - - - com.squareup.okhttp3 - mockwebserver - test - - - org.assertj - assertj-core - test - - - org.awaitility - awaitility - test - - - org.springframework.cloud - spring-cloud-starter-netflix-eureka-client - test - - - com.tngtech.archunit - archunit-junit5 - test - - - - - - fast - - false - - - - - maven-surefire-plugin - - 4 - true - -Xmx1024m -XX:MaxPermSize=256m - - - - - - - - + + + + + 4.0.0 + + spring-cloud-sleuth-brave + jar + Spring Cloud Sleuth Brave + Spring Cloud Sleuth Brave + + + org.springframework.cloud + spring-cloud-sleuth + 3.1.0-SNAPSHOT + .. + + + + + org.springframework.cloud + spring-cloud-sleuth-instrumentation + + + org.springframework.boot + spring-boot-starter-web + true + + + io.micrometer + micrometer-core + true + + + org.springframework.boot + spring-boot-starter-webflux + true + + + io.projectreactor + reactor-core + true + + + io.projectreactor.netty + reactor-netty-http + true + + + org.reactivestreams + reactive-streams + true + + + org.springframework.boot + spring-boot-starter-websocket + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.springframework.cloud + spring-cloud-commons + + + org.springframework.cloud + spring-cloud-stream + true + + + org.springframework.cloud + spring-cloud-starter-gateway + true + + + org.springframework.cloud + spring-cloud-starter-circuitbreaker-resilience4j + true + + + org.springframework.cloud + spring-cloud-starter-openfeign + true + + + org.springframework.cloud + spring-cloud-function-context + true + + + org.springframework.integration + spring-integration-core + true + + + org.springframework.amqp + spring-rabbit + true + + + org.springframework.kafka + spring-kafka + true + + + org.apache.kafka + kafka-streams + true + + + org.springframework.boot + spring-boot-starter-security + true + + + org.springframework + spring-context + + + org.springframework.cloud + spring-cloud-context + true + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + true + + + io.github.openfeign + feign-core + true + + + io.github.openfeign.form + feign-form-spring + true + + + io.reactivex + rxjava + true + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + true + + + org.apache.httpcomponents + httpclient + true + + + io.github.openfeign + feign-okhttp + true + + + org.springframework.boot + spring-boot-starter-data-mongodb + true + + + org.aspectj + aspectjrt + + + + io.zipkin.brave + brave + + + io.zipkin.reporter2 + * + + + io.zipkin.zipkin2 + * + + + + + io.zipkin.brave + brave-context-slf4j + + + io.zipkin.brave + brave-instrumentation-messaging + + + io.zipkin.brave + brave-instrumentation-rpc + + + io.zipkin.brave + brave-instrumentation-spring-rabbit + + + io.zipkin.brave + brave-instrumentation-kafka-clients + + + io.zipkin.brave + brave-instrumentation-kafka-streams + + + io.zipkin.brave + brave-instrumentation-httpclient + + + io.zipkin.brave + brave-instrumentation-httpasyncclient + + + io.zipkin.brave + brave-instrumentation-jms + + + io.zipkin.brave + brave-instrumentation-mongodb + + + io.zipkin.aws + brave-propagation-aws + + + javax.jms + javax.jms-api + true + + + io.opentracing.brave + brave-opentracing + true + + + org.apache.httpcomponents + httpasyncclient + true + + + org.springframework + spring-jms + true + + + + io.github.lognet + grpc-spring-boot-starter + true + + + org.springframework.boot + spring-boot-starter + + + + + io.zipkin.brave + brave-instrumentation-grpc + true + + + io.zipkin.reporter2 + zipkin-reporter-metrics-micrometer + + + io.micrometer + micrometer-core + + + + + + io.lettuce + lettuce-core + true + + + + org.springframework.boot + spring-boot-starter-quartz + true + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + io.zipkin.brave + brave-instrumentation-http-tests + test + + + com.squareup.okhttp3 + mockwebserver + test + + + org.assertj + assertj-core + test + + + org.awaitility + awaitility + test + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + test + + + com.tngtech.archunit + archunit-junit5 + test + + + + + + fast + + false + + + + + maven-surefire-plugin + + 4 + true + -Xmx1024m -XX:MaxPermSize=256m + + + + + + + + diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanBuilder.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanBuilder.java index f47a0d12c..e07c0fbdd 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanBuilder.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanBuilder.java @@ -1,125 +1,144 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.brave.bridge; - -import brave.Tracer; -import brave.propagation.TraceContextOrSamplingFlags; - -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.TraceContext; - -/** - * Brave implementation of a {@link Span.Builder}. - * - * @author Marcin Grzejszczak - * @since 3.0.0 - */ -class BraveSpanBuilder implements Span.Builder { - - brave.Span delegate; - - TraceContextOrSamplingFlags parentContext; - - private final Tracer tracer; - - private long startTimestamp; - - BraveSpanBuilder(Tracer tracer) { - this.tracer = tracer; - } - - BraveSpanBuilder(Tracer tracer, TraceContextOrSamplingFlags parentContext) { - this.tracer = tracer; - this.parentContext = parentContext; - } - - brave.Span span() { - if (this.delegate != null) { - return this.delegate; - } - else if (this.parentContext != null) { - this.delegate = this.tracer.nextSpan(this.parentContext); - } - else { - this.delegate = this.tracer.nextSpan(); - } - return this.delegate; - } - - @Override - public Span.Builder setParent(TraceContext context) { - this.parentContext = TraceContextOrSamplingFlags.create(BraveTraceContext.toBrave(context)); - return this; - } - - @Override - public Span.Builder setNoParent() { - return this; - } - - @Override - public Span.Builder name(String name) { - span().name(name); - return this; - } - - @Override - public Span.Builder event(String value) { - span().annotate(value); - return this; - } - - @Override - public Span.Builder tag(String key, String value) { - span().tag(key, value); - return this; - } - - @Override - public Span.Builder error(Throwable throwable) { - span().error(throwable); - return this; - } - - @Override - public Span.Builder kind(Span.Kind kind) { - span().kind(kind != null ? brave.Span.Kind.valueOf(kind.toString()) : null); - return this; - } - - @Override - public Span.Builder remoteServiceName(String remoteServiceName) { - span().remoteServiceName(remoteServiceName); - return this; - } - - @Override - public Span start() { - if (this.startTimestamp > 0) { - span().start(this.startTimestamp); - } - else { - span().start(); - } - return BraveSpan.fromBrave(this.delegate); - } - - static Span.Builder toBuilder(Tracer tracer, TraceContextOrSamplingFlags context) { - return new BraveSpanBuilder(tracer, context); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.bridge; + +import java.net.URI; + +import brave.Tracer; +import brave.propagation.TraceContextOrSamplingFlags; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; + +/** + * Brave implementation of a {@link Span.Builder}. + * + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +class BraveSpanBuilder implements Span.Builder { + + private static final Log log = LogFactory.getLog(BraveSpanBuilder.class); + + brave.Span delegate; + + TraceContextOrSamplingFlags parentContext; + + private final Tracer tracer; + + private long startTimestamp; + + BraveSpanBuilder(Tracer tracer) { + this.tracer = tracer; + } + + BraveSpanBuilder(Tracer tracer, TraceContextOrSamplingFlags parentContext) { + this.tracer = tracer; + this.parentContext = parentContext; + } + + brave.Span span() { + if (this.delegate != null) { + return this.delegate; + } + else if (this.parentContext != null) { + this.delegate = this.tracer.nextSpan(this.parentContext); + } + else { + this.delegate = this.tracer.nextSpan(); + } + return this.delegate; + } + + @Override + public Span.Builder setParent(TraceContext context) { + this.parentContext = TraceContextOrSamplingFlags.create(BraveTraceContext.toBrave(context)); + return this; + } + + @Override + public Span.Builder setNoParent() { + return this; + } + + @Override + public Span.Builder name(String name) { + span().name(name); + return this; + } + + @Override + public Span.Builder event(String value) { + span().annotate(value); + return this; + } + + @Override + public Span.Builder tag(String key, String value) { + span().tag(key, value); + return this; + } + + @Override + public Span.Builder error(Throwable throwable) { + span().error(throwable); + return this; + } + + @Override + public Span.Builder kind(Span.Kind kind) { + span().kind(kind != null ? brave.Span.Kind.valueOf(kind.toString()) : null); + return this; + } + + @Override + public Span.Builder remoteServiceName(String remoteServiceName) { + span().remoteServiceName(remoteServiceName); + return this; + } + + @Override + public Span.Builder remoteUrl(String remoteUrl) { + try { + URI uri = URI.create(remoteUrl); + span().remoteIpAndPort(uri.getHost(), uri.getPort()); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug("Failed to parse url [" + remoteUrl + "]. Will not set the value", e); + } + } + return this; + } + + @Override + public Span start() { + if (this.startTimestamp > 0) { + span().start(this.startTimestamp); + } + else { + span().start(); + } + return BraveSpan.fromBrave(this.delegate); + } + + static Span.Builder toBuilder(Tracer tracer, TraceContextOrSamplingFlags context) { + return new BraveSpanBuilder(tracer, context); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index 7252a853b..f83080117 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -62,6 +62,11 @@ rsocket-core true + + io.r2dbc + r2dbc-proxy + true + io.projectreactor.kafka reactor-kafka diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListener.java new file mode 100644 index 000000000..67d23c797 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListener.java @@ -0,0 +1,120 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.r2dbc; + +import io.r2dbc.proxy.core.QueryExecutionInfo; +import io.r2dbc.proxy.core.QueryInfo; +import io.r2dbc.proxy.listener.ProxyExecutionListener; +import io.r2dbc.spi.ConnectionFactory; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcProperties; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.util.StringUtils; + +/** + * Trace representation of a {@link ProxyExecutionListener}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceProxyExecutionListener implements ProxyExecutionListener { + + private static final Log log = LogFactory.getLog(TraceProxyExecutionListener.class); + + private final BeanFactory beanFactory; + + private final ConnectionFactory connectionFactory; + + private Tracer tracer; + + public TraceProxyExecutionListener(BeanFactory beanFactory, ConnectionFactory connectionFactory) { + this.beanFactory = beanFactory; + this.connectionFactory = connectionFactory; + } + + @Override + public void beforeQuery(QueryExecutionInfo executionInfo) { + if (tracer().currentSpan() == null) { + return; + } + String name = this.connectionFactory.getMetadata().getName(); + Span span = clientSpan(executionInfo, name); + if (log.isDebugEnabled()) { + log.debug("Created a new child span before query [" + span + "]"); + } + tagQueries(executionInfo, span); + executionInfo.getValueStore().put(Span.class, span); + } + + Span clientSpan(QueryExecutionInfo executionInfo, String name) { + R2dbcProperties r2dbcProperties = this.beanFactory.getBean(R2dbcProperties.class); + String url = r2dbcProperties.getUrl(); + Span.Builder builder = tracer().spanBuilder().kind(Span.Kind.CLIENT).name("query") + .remoteServiceName(name) + .tag("rd2bc.connection", name).tag("rd2bc.thread", executionInfo.getThreadName()); + if (StringUtils.hasText(url)) { + builder.remoteUrl(url); + } + return builder.start(); + } + + private void tagQueries(QueryExecutionInfo executionInfo, Span span) { + int i = 0; + for (QueryInfo queryInfo : executionInfo.getQueries()) { + span.tag("r2dbc.query[" + i + "]", queryInfo.getQuery()); + i = i + 1; + } + } + + @Override + public void afterQuery(QueryExecutionInfo executionInfo) { + Span span = executionInfo.getValueStore().get(Span.class, Span.class); + if (span != null) { + if (log.isDebugEnabled()) { + log.debug("Continued the child span in after query [" + span + "]"); + } + final Throwable throwable = executionInfo.getThrowable(); + if (throwable != null) { + span.error(throwable); + } + span.end(); + } + } + + @Override + public void eachQueryResult(QueryExecutionInfo executionInfo) { + Span span = executionInfo.getValueStore().get(Span.class, Span.class); + if (span != null) { + if (log.isDebugEnabled()) { + log.debug("Marking after query result for span [" + span + "]"); + } + span.event("r2dbc.query_result"); + } + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java index 4a5f8fce2..c8602fcb6 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java @@ -110,7 +110,7 @@ public class TraceReactiveTransactionManager implements ReactiveTransactionManag } private Span spanFromContext(TraceContext traceContext) { - try (CurrentTraceContext.Scope scope = currentTraceContext.maybeScope(traceContext)) { + try (CurrentTraceContext.Scope scope = currentTraceContext().maybeScope(traceContext)) { return tracer().currentSpan(); } } diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListenerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListenerTests.java new file mode 100644 index 000000000..3ad277a1c --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListenerTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.r2dbc; + +import java.util.concurrent.atomic.AtomicReference; + +import io.r2dbc.proxy.core.QueryExecutionInfo; +import io.r2dbc.proxy.core.ValueStore; +import io.r2dbc.proxy.test.MockQueryExecutionInfo; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryMetadata; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.tracer.SimpleSpan; +import org.springframework.cloud.sleuth.tracer.SimpleTracer; + +import static org.assertj.core.api.BDDAssertions.then; + +class TraceProxyExecutionListenerTests { + + SimpleTracer simpleTracer = new SimpleTracer(); + + ConnectionFactory connectionFactory = connectionFactory(); + + TraceProxyExecutionListener listener = new TraceProxyExecutionListener(beanFactory(), connectionFactory); + + @Test + void should_do_nothing_on_before_query_when_there_was_no_previous_span() { + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); + + listener.beforeQuery(queryExecutionInfo); + + then(this.simpleTracer.spans).isEmpty(); + then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); + } + + @Test + void should_do_nothing_on_after_query_when_there_was_no_previous_span() { + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); + + listener.afterQuery(queryExecutionInfo); + + then(this.simpleTracer.spans).isEmpty(); + then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); + } + + @Test + void should_do_nothing_on_each_query_when_there_was_no_previous_span() { + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); + + listener.eachQueryResult(queryExecutionInfo); + + then(this.simpleTracer.spans).isEmpty(); + then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); + } + + @Test + void should_put_a_child_span_in_value_store_when_a_span_was_already_in_context() { + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); + this.simpleTracer.nextSpan().start(); + AtomicReference clientSpan = new AtomicReference<>(); + listener = new TraceProxyExecutionListener(beanFactory(), connectionFactory) { + @Override + Span clientSpan(QueryExecutionInfo executionInfo, String name) { + Span span = super.clientSpan(executionInfo, name); + clientSpan.set(span); + return span; + } + }; + + listener.beforeQuery(queryExecutionInfo); + + then(queryExecutionInfo.getValueStore().get(Span.class)).isSameAs(clientSpan.get()); + } + + @Test + void should_annotate_a_span_on_query_result() { + SimpleSpan span = new SimpleSpan(); + ValueStore valueStore = ValueStore.create(); + valueStore.put(Span.class, span); + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.builder().valueStore(valueStore).build(); + + listener.eachQueryResult(queryExecutionInfo); + + then(span.events).isNotEmpty(); + } + + @Test + void should_end_span_on_after_query() { + SimpleSpan span = new SimpleSpan().start(); + ValueStore valueStore = ValueStore.create(); + valueStore.put(Span.class, span); + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.builder().throwable(new RuntimeException()) + .valueStore(valueStore).build(); + + listener.afterQuery(queryExecutionInfo); + + then(span.throwable).isNotNull(); + then(span.ended).isTrue(); + } + + private ConnectionFactory connectionFactory() { + return new ConnectionFactory() { + @Override + public Publisher create() { + return null; + } + + @Override + public ConnectionFactoryMetadata getMetadata() { + return () -> "my-name"; + } + }; + } + + private BeanFactory beanFactory() { + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); + beanFactory.addBean("tracer", this.simpleTracer); + return beanFactory; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java index ad86a5817..ac609ac27 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java @@ -16,7 +16,9 @@ package org.springframework.cloud.sleuth.tracer; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.springframework.cloud.sleuth.Span; @@ -38,6 +40,14 @@ public class SimpleSpan implements Span { public Throwable throwable; + public String remoteServiceName; + + public Span.Kind spanKind; + + public List events = new ArrayList<>(); + + public String name; + @Override public boolean isNoop() { return true; @@ -56,11 +66,13 @@ public class SimpleSpan implements Span { @Override public SimpleSpan name(String name) { + this.name = name; return this; } @Override public SimpleSpan event(String value) { + this.events.add(value); return this; } @@ -88,6 +100,7 @@ public class SimpleSpan implements Span { @Override public Span remoteServiceName(String remoteServiceName) { + this.remoteServiceName = remoteServiceName; return this; } diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java new file mode 100644 index 000000000..d73faa7fe --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java @@ -0,0 +1,105 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.tracer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; + +/** + * A noop implementation. Does nothing. + * + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +class SimpleSpanBuilder implements Span.Builder { + + List events = new ArrayList<>(); + + Map tags = new HashMap<>(); + + Throwable error; + + Span.Kind spanKind; + + String remoteServiceName; + + String name; + + @Override + public Span.Builder setParent(TraceContext context) { + return this; + } + + @Override + public Span.Builder setNoParent() { + return this; + } + + @Override + public Span.Builder name(String name) { + this.name = name; + return this; + } + + @Override + public Span.Builder event(String value) { + this.events.add(value); + return this; + } + + @Override + public Span.Builder tag(String key, String value) { + this.tags.put(key, value); + return this; + } + + @Override + public Span.Builder error(Throwable throwable) { + this.error = throwable; + return this; + } + + @Override + public Span.Builder kind(Span.Kind spanKind) { + this.spanKind = spanKind; + return this; + } + + @Override + public Span.Builder remoteServiceName(String remoteServiceName) { + this.remoteServiceName = remoteServiceName; + return this; + } + + @Override + public Span start() { + SimpleSpan span = new SimpleSpan(); + this.tags.forEach(span::tag); + this.events.forEach(span::event); + span.remoteServiceName(this.remoteServiceName); + span.error(this.error); + span.spanKind = this.spanKind; + span.name(this.name); + return span; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java index c4cbf633e..d03c15227 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java @@ -53,6 +53,13 @@ public class SimpleTracer implements Tracer { return span; } + public SimpleSpan getLastSpan() { + BDDAssertions.then(this.spans).isNotEmpty(); + SimpleSpan span = this.spans.get(this.spans.size() - 1); + BDDAssertions.then(span.started).as("Span must be started").isTrue(); + return span; + } + @Override public SpanInScope withSpan(Span span) { return new NoOpSpanInScope(); @@ -65,7 +72,10 @@ public class SimpleTracer implements Tracer { @Override public Span currentSpan() { - return new SimpleSpan(); + if (this.spans.isEmpty()) { + return null; + } + return this.spans.get(spans.size() - 1); } @Override @@ -82,7 +92,7 @@ public class SimpleTracer implements Tracer { @Override public Span.Builder spanBuilder() { - return null; + return new SimpleSpanBuilder(); } @Override diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index 23f1b8daa..110bfbfc2 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -52,6 +52,7 @@ spring-cloud-sleuth-instrumentation-quartz-tests spring-cloud-sleuth-instrumentation-reactor-tests spring-cloud-sleuth-instrumentation-rxjava-tests + spring-cloud-sleuth-instrumentation-r2dbc-tests spring-cloud-sleuth-instrumentation-scheduling-tests spring-cloud-sleuth-instrumentation-task-tests spring-cloud-sleuth-instrumentation-webflux-tests diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java index 96dafebfe..f373f8588 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java @@ -21,9 +21,11 @@ import brave.baggage.BaggagePropagationConfig; import brave.sampler.Sampler; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; @@ -72,8 +74,8 @@ public class MultipleHopsIntegrationTests } @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration( - exclude = { MongoAutoConfiguration.class, QuartzAutoConfiguration.class, JmxAutoConfiguration.class }) + @EnableAutoConfiguration(exclude = { MongoAutoConfiguration.class, QuartzAutoConfiguration.class, + JmxAutoConfiguration.class, R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class }) static class Config { @Bean diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java index 781653b0f..b7d5b2105 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java @@ -35,10 +35,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration; import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; import org.springframework.boot.jms.XAConnectionFactoryWrapper; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -166,8 +168,8 @@ public class JmsTracingConfigurationTest { } @Configuration(proxyBeanMethods = false) -@EnableAutoConfiguration( - exclude = { KafkaAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class }) +@EnableAutoConfiguration(exclude = { KafkaAutoConfiguration.class, MongoAutoConfiguration.class, + QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class }) class JmsTestTracingConfiguration { } diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java index 66a907885..734e096d3 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java @@ -22,15 +22,18 @@ import brave.test.TestSpanHandler; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ImportResource; import org.springframework.integration.config.EnableIntegration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.web.client.RestTemplate; -@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class }) +@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class, + R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class }) @ImportResource("classpath:beans/applicationContext.xml") @EnableIntegration @EnableAsync diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/pom.xml new file mode 100644 index 000000000..2758b3100 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/pom.xml @@ -0,0 +1,94 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-r2dbc-tests + jar + Spring Cloud Sleuth Brave R2DBC Instrumentation Tests + Spring Cloud Sleuth Brave R2DBC Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + + + + org.springframework.cloud + spring-cloud-sleuth-tests-common + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-test + + + org.springframework.boot + spring-boot-starter-data-r2dbc + + + com.h2database + h2 + runtime + + + io.r2dbc + r2dbc-h2 + runtime + + + io.r2dbc + r2dbc-proxy + + + io.zipkin.brave + brave-tests + + + org.awaitility + awaitility + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/r2dbc/R2dbcIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/r2dbc/R2dbcIntegrationTests.java new file mode 100644 index 000000000..c3e825fd1 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/r2dbc/R2dbcIntegrationTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.r2dbc; + +import brave.sampler.Sampler; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +@SpringBootTest +@ContextConfiguration(classes = R2dbcIntegrationTests.Config.class) +public class R2dbcIntegrationTests extends org.springframework.cloud.sleuth.instrument.r2dbc.R2dbcIntegrationTests { + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + TestSpanHandler testSpanHandlerSupplier(brave.test.TestSpanHandler testSpanHandler) { + return new BraveTestSpanHandler(testSpanHandler); + } + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + brave.test.TestSpanHandler braveTestSpanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/src/test/resources/application.yml b/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/src/test/resources/application.yml new file mode 100644 index 000000000..be7f4fe29 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/src/test/resources/application.yml @@ -0,0 +1 @@ +logging.level.org.springframework.cloud: DEBUG diff --git a/tests/common/pom.xml b/tests/common/pom.xml index 3f6a1176d..f23cd0d2d 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -159,6 +159,16 @@ reactor-kafka true + + org.springframework.boot + spring-boot-starter-data-r2dbc + true + + + io.r2dbc + r2dbc-proxy + true + org.testcontainers testcontainers diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/R2dbcIntegrationTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/R2dbcIntegrationTests.java new file mode 100644 index 000000000..c44d5a086 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/R2dbcIntegrationTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.r2dbc; + +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import io.r2dbc.spi.ConnectionFactory; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.dao.DataAccessException; +import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer; +import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.BDDAssertions.then; + +@ContextConfiguration(classes = R2dbcIntegrationTests.TestConfig.class) +@TestPropertySource(properties = "spring.application.name=MyApplication") +public abstract class R2dbcIntegrationTests { + + @Autowired + TestSpanHandler spans; + + @Test + public void should_pass_tracing_information_when_using_r2dbc() { + Set traceIds = this.spans.reportedSpans().stream().map(FinishedSpan::getTraceId) + .collect(Collectors.toSet()); + then(traceIds).as("There's one traceid").hasSize(1); + Set spanIds = this.spans.reportedSpans().stream().map(FinishedSpan::getSpanId) + .collect(Collectors.toSet()); + + // 2 transactions - 9 database interactions + then(spanIds).as("There are 11 spans").hasSize(11); + List spanNames = this.spans.reportedSpans().stream().map(FinishedSpan::getName) + .collect(Collectors.toList()); + List remoteServiceNames = this.spans.reportedSpans().stream().map(FinishedSpan::getRemoteServiceName) + .collect(Collectors.toList()); + then(spanNames.stream().filter("tx"::equalsIgnoreCase).collect(Collectors.toList())).hasSize(2); + then(remoteServiceNames.stream().filter("h2"::equalsIgnoreCase).collect(Collectors.toList())).hasSize(9); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @ComponentScan + public static class TestConfig { + + private static final Logger log = LoggerFactory.getLogger(TestConfig.class); + + @Bean + public CommandLineRunner demo(ReactiveNewTransactionService reactiveNewTransactionService) { + return (args) -> { + try { + reactiveNewTransactionService.newTransaction().block(Duration.ofSeconds(50)); + } + catch (DataAccessException e) { + log.info("Expected to throw an exception so that we see if rollback works", e); + } + }; + } + + @Bean + ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) { + ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer(); + initializer.setConnectionFactory(connectionFactory); + initializer.setDatabasePopulator(new ResourceDatabasePopulator(new ClassPathResource("schema.sql"))); + return initializer; + } + + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveContinuedTransactionService.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveContinuedTransactionService.java new file mode 100644 index 000000000..70f850115 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveContinuedTransactionService.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.r2dbc; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ReactiveContinuedTransactionService { + + private static final Logger log = LoggerFactory.getLogger(ReactiveContinuedTransactionService.class); + + private final ReactiveCustomerRepository repository; + + private final ReactiveNestedTransactionService reactiveNestedTransactionService; + + public ReactiveContinuedTransactionService(ReactiveCustomerRepository repository, + ReactiveNestedTransactionService reactiveNestedTransactionService) { + this.repository = repository; + this.reactiveNestedTransactionService = reactiveNestedTransactionService; + } + + @Transactional + public Mono continuedTransaction() { + return Mono.fromRunnable(() -> log.info("Hello from continued transaction")).then(repository.findById(1L)) + .doOnNext(customer -> { + // fetch an individual customer by ID + log.info("Customer found with findById(1L):"); + log.info("--------------------------------"); + log.info(customer.toString()); + log.info(""); + }).doOnNext(customer -> { + // fetch customers by last name + log.info("Customer found with findByLastName('Bauer'):"); + log.info("--------------------------------------------"); + }).flatMapMany(customer -> repository.findByLastName("Bauer")) + .doOnNext(cust -> log.info(cust.toString())).then(this.reactiveNestedTransactionService.requiresNew()); + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveCustomer.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveCustomer.java new file mode 100644 index 000000000..f3cc7d246 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveCustomer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.r2dbc; + +import org.springframework.data.annotation.Id; + +public class ReactiveCustomer { + + @Id + private Long id; + + private final String firstName; + + private final String lastName; + + public ReactiveCustomer(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return this.firstName; + } + + public String getLastName() { + return this.lastName; + } + + @Override + public String toString() { + return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName); + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveCustomerRepository.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveCustomerRepository.java new file mode 100644 index 000000000..98db76017 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveCustomerRepository.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.r2dbc; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.data.repository.reactive.ReactiveCrudRepository; + +public interface ReactiveCustomerRepository extends ReactiveCrudRepository { + + Flux findByLastName(String lastName); + + Mono findById(long id); + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveNestedTransactionService.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveNestedTransactionService.java new file mode 100644 index 000000000..992e8fffe --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveNestedTransactionService.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.r2dbc; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ReactiveNestedTransactionService { + + private static final Logger log = LoggerFactory.getLogger(ReactiveNestedTransactionService.class); + + private final ReactiveCustomerRepository repository; + + public ReactiveNestedTransactionService(ReactiveCustomerRepository repository) { + this.repository = repository; + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public Mono requiresNew() { + return Mono.fromRunnable(() -> log.info("Hello from nested transaction")) + .then(repository.save(new ReactiveCustomer("Hello", "From Propagated Transaction"))).then(); + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveNewTransactionService.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveNewTransactionService.java new file mode 100644 index 000000000..71608c9c3 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/ReactiveNewTransactionService.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.r2dbc; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ReactiveNewTransactionService { + + private static final Logger log = LoggerFactory.getLogger(ReactiveNewTransactionService.class); + + private final ReactiveCustomerRepository repository; + + private final ReactiveContinuedTransactionService reactiveContinuedTransactionService; + + public ReactiveNewTransactionService(ReactiveCustomerRepository repository, + ReactiveContinuedTransactionService reactiveContinuedTransactionService) { + this.repository = repository; + this.reactiveContinuedTransactionService = reactiveContinuedTransactionService; + } + + // 6 database transactions + @Transactional + public Mono newTransaction() { + return Mono.fromRunnable(() -> log.info("Hello from new transaction")) + // save a few customers + .then(repository.save(new ReactiveCustomer("Jack", "Bauer"))) + .then(repository.save(new ReactiveCustomer("Chloe", "O'Brian"))) + .then(repository.save(new ReactiveCustomer("Kim", "Bauer"))) + .then(repository.save(new ReactiveCustomer("David", "Palmer"))) + .then(repository.save(new ReactiveCustomer("Michelle", "Dessler"))).doOnNext(reactiveCustomer -> { + log.info("Customers found with findAll():"); + log.info("-------------------------------"); + }).flatMapMany(reactiveCustomer -> repository.findAll()).doOnNext(cust -> log.info(cust.toString())) + .doOnNext(o -> log.info("")).then(this.reactiveContinuedTransactionService.continuedTransaction()); + } + +} diff --git a/tests/common/src/main/resources/schema.sql b/tests/common/src/main/resources/schema.sql new file mode 100644 index 000000000..2680487e8 --- /dev/null +++ b/tests/common/src/main/resources/schema.sql @@ -0,0 +1 @@ +CREATE TABLE reactive_customer (id SERIAL PRIMARY KEY, first_name VARCHAR(255), last_name VARCHAR(255)); From 4241bca9528b3f8de122fa57f18a8dba0a4d552e Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 13 May 2021 09:16:11 +0200 Subject: [PATCH 27/59] Deployer uses remoteServiceName; fixes gh-1947 --- .../TraceAppDeployerBeanPostProcessor.java | 3 +- .../TraceDeployerAutoConfiguration.java | 1 - .../instrument/deployer/TraceAppDeployer.java | 97 +++++-- .../instrument/reactor/ReactorSleuth.java | 66 ++++- .../deployer/TraceAppDeployerTests.java | 10 + .../cloud/sleuth/tracer/SimpleTracer.java | 256 +++++++++--------- 6 files changed, 274 insertions(+), 159 deletions(-) rename {spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth => spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig}/instrument/deployer/TraceAppDeployerBeanPostProcessor.java (91%) diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceAppDeployerBeanPostProcessor.java similarity index 91% rename from spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerBeanPostProcessor.java rename to spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceAppDeployerBeanPostProcessor.java index 616ba68df..3229300ec 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerBeanPostProcessor.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceAppDeployerBeanPostProcessor.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package org.springframework.cloud.sleuth.instrument.deployer; +package org.springframework.cloud.sleuth.autoconfig.instrument.deployer; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.cloud.deployer.spi.app.AppDeployer; +import org.springframework.cloud.sleuth.instrument.deployer.TraceAppDeployer; import org.springframework.core.env.Environment; /** diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceDeployerAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceDeployerAutoConfiguration.java index 45b7b577c..8c4905724 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceDeployerAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceDeployerAutoConfiguration.java @@ -24,7 +24,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.deployer.spi.app.AppDeployer; import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.deployer.TraceAppDeployerBeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java index 6e2ba7e38..ee63aa998 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java @@ -18,6 +18,7 @@ package org.springframework.cloud.sleuth.instrument.deployer; import java.time.Duration; import java.util.Arrays; +import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -67,17 +68,11 @@ public class TraceAppDeployer implements AppDeployer { @Override public String deploy(AppDeploymentRequest request) { - Span span = tracer().nextSpan().name("deploy"); - // TODO: Is this secure to pass? - // TODO: Does it make sense? - // if (!request.getCommandlineArguments().isEmpty()) { - // span.tag("commandlineArguments", request.getCommandlineArguments().toString()); - // } - // if (!request.getDeploymentProperties().isEmpty()) { - // span.tag("deploymentProperties", request.getDeploymentProperties().toString()); - // } - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - span.event("start"); + Span.Builder spanBuilder = clientSpan("deploy"); + Span span = spanBuilder.start(); + // Span span = tracer().nextSpan().name("deploy"); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { + span.event("deployer.start"); String id = this.delegate.deploy(request); span.tag("deployer.app.id", id); registerListener(span, id); @@ -85,6 +80,56 @@ public class TraceAppDeployer implements AppDeployer { } } + private Span.Builder clientSpan(String name) { + Span.Builder spanBuilder = tracer().spanBuilder(); + Span currentSpan = tracer().currentSpan(); + if (currentSpan != null) { + spanBuilder.setParent(currentSpan.context()); + } + return clientSpanKind(name, spanBuilder); + } + + private Span.Builder clientSpanKind(String name, Span.Builder spanBuilder) { + return spanBuilder.kind(Span.Kind.CLIENT).name(name).remoteServiceName(remoteServiceName()); + } + + private Span.Builder clientSpan(String name, Span parentSpan) { + Span.Builder spanBuilder = tracer().spanBuilder(); + Span currentSpan = parentSpan != null ? parentSpan : tracer().currentSpan(); + if (currentSpan != null) { + spanBuilder.setParent(currentSpan.context()); + } + Map platformSpecificInfo = environmentInfo().getPlatformSpecificInfo(); + addCfTags(spanBuilder, platformSpecificInfo); + addK8sTags(spanBuilder, platformSpecificInfo); + return clientSpanKind(name, spanBuilder); + } + + private void addCfTags(Span.Builder spanBuilder, Map platformSpecificInfo) { + if (platformSpecificInfo.containsKey("API Endpoint")) { + spanBuilder.tag("deployer.platform.cf.url", platformSpecificInfo.get("API Endpoint")); + } + if (platformSpecificInfo.containsKey("Organization")) { + spanBuilder.tag("deployer.platform.cf.org", platformSpecificInfo.get("Organization")); + } + if (platformSpecificInfo.containsKey("Space")) { + spanBuilder.tag("deployer.platform.cf.space", platformSpecificInfo.get("Space")); + } + } + + private void addK8sTags(Span.Builder spanBuilder, Map platformSpecificInfo) { + if (platformSpecificInfo.containsKey("master-url")) { + spanBuilder.tag("deployer.platform.k8s.url", platformSpecificInfo.get("master-url")); + } + if (platformSpecificInfo.containsKey("namespace")) { + spanBuilder.tag("deployer.platform.k8s.namespace", platformSpecificInfo.get("namespace")); + } + } + + private String remoteServiceName() { + return environmentInfo().getPlatformType(); + } + private void registerListener(Span span, String id) { PreviousAndCurrentStatus previousAndCurrentStatus = new PreviousAndCurrentStatus(span); // @formatter:off @@ -102,10 +147,11 @@ public class TraceAppDeployer implements AppDeployer { @Override public void undeploy(String id) { - Span span = tracer().nextSpan().name("undeploy"); + Span.Builder spanBuilder = clientSpan("undeploy"); + Span span = spanBuilder.start(); span.tag("deployer.app.id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - span.event("start"); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { + span.event("deployer.start"); this.delegate.undeploy(id); registerListener(span, id); } @@ -116,7 +162,8 @@ public class TraceAppDeployer implements AppDeployer { @Override public AppStatus status(String id) { - Span span = tracer().nextSpan().name("status"); + Span.Builder spanBuilder = clientSpan("status"); + Span span = spanBuilder.start(); span.tag("deployer.app.id", id); try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { return this.delegate.status(id); @@ -129,14 +176,16 @@ public class TraceAppDeployer implements AppDeployer { @Override public Mono statusReactive(String id) { return ReactorSleuth.tracedMono(tracer(), currentTraceContext(), "status", - () -> this.delegate.statusReactive(id), (o, span) -> span.tag("deployer.app.id", id)); + () -> this.delegate.statusReactive(id), (o, span) -> span.tag("deployer.app.id", id), + span -> clientSpan("status", span).start()); } @Override public Flux statusesReactive(String... ids) { return ReactorSleuth.tracedFlux(tracer(), currentTraceContext(), "statuses", () -> this.delegate.statusesReactive(ids), - (o, span) -> span.tag("deployer.app.ids", Arrays.toString(ids))); + (o, span) -> span.tag("deployer.app.ids", Arrays.toString(ids)), + span -> clientSpan("statuses", span).start()); } @Override @@ -146,9 +195,10 @@ public class TraceAppDeployer implements AppDeployer { @Override public String getLog(String id) { - Span span = tracer().nextSpan().name("getLog"); + Span.Builder spanBuilder = clientSpan("getLog"); + Span span = spanBuilder.start(); span.tag("deployer.app.id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { return this.delegate.getLog(id); } finally { @@ -158,15 +208,10 @@ public class TraceAppDeployer implements AppDeployer { @Override public void scale(AppScaleRequest appScaleRequest) { - Span span = tracer().nextSpan().name("scale"); + Span.Builder spanBuilder = clientSpan("scale"); + Span span = spanBuilder.start(); span.tag("deployer.scale.deploymentId", appScaleRequest.getDeploymentId()); span.tag("deployer.scale.count", String.valueOf(appScaleRequest.getCount())); - // TODO: Is this secure to pass? - // TODO: Does it make sense? - // if (appScaleRequest.getProperties().isPresent() && - // !appScaleRequest.getProperties().get().isEmpty()) { - // span.tag("properties", appScaleRequest.getProperties().get().toString()); - // } try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { this.delegate.scale(appScaleRequest); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java index 45965a763..d81da13ac 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java @@ -338,6 +338,25 @@ public abstract class ReactorSleuth { context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); } + /** + * Wraps the given Mono in a trace representation. Retrieves the span from context, + * creates a child span with the given name. + * @param tracer - Tracer bean + * @param currentTraceContext - CurrentTraceContext bean + * @param childSpanName - name of the created child span + * @param supplier - supplier of a {@link Mono} to be wrapped in tracing + * @param - type returned by the Mono + * @param spanCustomizer - customizer for the child span + * @param spanFunction - function that creates a new or child span + * @return traced Mono + */ + public static Mono tracedMono(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, + @NonNull String childSpanName, @NonNull Supplier> supplier, + @NonNull BiConsumer spanCustomizer, @NonNull Function spanFunction) { + return runMonoSupplierInScope(supplier, spanCustomizer).contextWrite(context -> ReactorSleuth + .enhanceContext(tracer, currentTraceContext, context, childSpanName, spanFunction)); + } + private static Mono runMonoSupplierInScope(Supplier> supplier, BiConsumer spanCustomizer) { return Mono.deferContextual(contextView -> { Span span = contextView.get(Span.class); @@ -407,6 +426,25 @@ public abstract class ReactorSleuth { context -> ReactorSleuth.enhanceContext(tracer, currentTraceContext, context, childSpanName)); } + /** + * Wraps the given Flux in a trace representation. Retrieves the span from context, + * creates a child span with the given name. + * @param tracer - Tracer bean + * @param currentTraceContext - CurrentTraceContext bean + * @param childSpanName - name of the created child span + * @param supplier - supplier of a {@link Flux} to be wrapped in tracing + * @param - type returned by the Flux + * @param spanCustomizer - customizer for the child span + * @param spanFunction - function that creates a new or child span + * @return traced Flux + */ + public static Flux tracedFlux(@NonNull Tracer tracer, @NonNull CurrentTraceContext currentTraceContext, + @NonNull String childSpanName, @NonNull Supplier> supplier, + @NonNull BiConsumer spanCustomizer, @NonNull Function spanFunction) { + return runFluxSupplierInScope(supplier, spanCustomizer).contextWrite(context -> ReactorSleuth + .enhanceContext(tracer, currentTraceContext, context, childSpanName, spanFunction)); + } + /** * Wraps the given Flux in a trace representation. Retrieves the span from context, * creates a child span with the given name. @@ -461,10 +499,16 @@ public abstract class ReactorSleuth { private static Span childSpanFromContext(Tracer tracer, CurrentTraceContext currentTraceContext, reactor.util.context.Context context, String childSpanName) { + return childSpanFromContext(currentTraceContext, context, childSpanName, + span -> span == null ? tracer.nextSpan() : tracer.nextSpan(span)); + } + + private static Span childSpanFromContext(CurrentTraceContext currentTraceContext, + reactor.util.context.Context context, String childSpanName, Function spanSupplier) { TraceContext traceContext = context.getOrDefault(TraceContext.class, null); Span span = context.getOrDefault(Span.class, null); if (traceContext == null && span == null) { - span = tracer.nextSpan(); + span = spanSupplier.apply(null); if (log.isDebugEnabled()) { log.debug("There was no previous span in reactor context, created a new one [" + span + "]"); } @@ -475,7 +519,7 @@ public abstract class ReactorSleuth { if (log.isDebugEnabled()) { log.debug("Found a trace context in reactor context [" + traceContext + "]"); } - span = tracer.nextSpan(); + span = spanSupplier.apply(null); if (log.isDebugEnabled()) { log.debug("Created a child span [" + span + "]"); } @@ -485,7 +529,7 @@ public abstract class ReactorSleuth { if (log.isDebugEnabled()) { log.debug("Found a span in reactor context [" + span + "]"); } - span = tracer.nextSpan(span); + span = spanSupplier.apply(span); if (log.isDebugEnabled()) { log.debug("Created a child span [" + span + "]"); } @@ -493,6 +537,22 @@ public abstract class ReactorSleuth { return span.name(childSpanName).start(); } + /** + * Updates the Reactor context with tracing information. Creates a new span if there + * is no current span. Creates a child span if there was an entry in the context + * already. + * @param currentTraceContext current trace context + * @param context Reactor context + * @param childSpanName child span name when there is no span in context + * @param spanSupplier function that creates a new or child span + * @return updated Reactor context + */ + public static Context enhanceContext(Tracer tracer, CurrentTraceContext currentTraceContext, + reactor.util.context.Context context, String childSpanName, Function spanSupplier) { + Span span = childSpanFromContext(currentTraceContext, context, childSpanName, spanSupplier); + return putSpanInScope(tracer, context, span); + } + /** * Updates the Reactor context with tracing information. Creates a new span if there * is no current span. Creates a child span if there was an entry in the context diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java index 008ff5b59..208abe3f3 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.BDDMockito; import reactor.core.publisher.Flux; @@ -34,6 +35,7 @@ import org.springframework.cloud.deployer.spi.app.AppScaleRequest; import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; +import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; import org.springframework.cloud.sleuth.tracer.NoOpCurrentTraceContext; import org.springframework.cloud.sleuth.tracer.SimpleTracer; import org.springframework.core.env.Environment; @@ -48,6 +50,14 @@ class TraceAppDeployerTests { TraceAppDeployer traceAppDeployer = new TraceAppDeployer(this.delegate, beanFactory(), environment()); + @BeforeEach + void setup() { + BDDMockito.given(this.delegate.environmentInfo()) + .willReturn(new RuntimeEnvironmentInfo.Builder().spiClass(Object.class).implementationName("asd") + .implementationVersion("asd").platformType("asd").platformApiVersion("asd") + .platformClientVersion("asd").platformHostVersion("asd").build()); + } + @Test void should_trace_deploy() { BDDMockito.given(this.delegate.statusReactive(BDDMockito.any())) diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java index d03c15227..beeb01728 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java @@ -1,128 +1,128 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.tracer; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.assertj.core.api.BDDAssertions; - -import org.springframework.cloud.sleuth.BaggageInScope; -import org.springframework.cloud.sleuth.ScopedSpan; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.SpanCustomizer; -import org.springframework.cloud.sleuth.TraceContext; -import org.springframework.cloud.sleuth.Tracer; - -/** - * A noop implementation. Does nothing. - * - * @author Marcin Grzejszczak - * @since 3.0.0 - */ -public class SimpleTracer implements Tracer { - - public List spans = new ArrayList<>(); - - @Override - public Span nextSpan(Span parent) { - return new SimpleSpan(); - } - - public SimpleSpan getOnlySpan() { - BDDAssertions.then(this.spans).hasSize(1); - SimpleSpan span = this.spans.get(0); - BDDAssertions.then(span.started).as("Span must be started").isTrue(); - BDDAssertions.then(span.ended).as("Span must be finished").isTrue(); - return span; - } - - public SimpleSpan getLastSpan() { - BDDAssertions.then(this.spans).isNotEmpty(); - SimpleSpan span = this.spans.get(this.spans.size() - 1); - BDDAssertions.then(span.started).as("Span must be started").isTrue(); - return span; - } - - @Override - public SpanInScope withSpan(Span span) { - return new NoOpSpanInScope(); - } - - @Override - public SpanCustomizer currentSpanCustomizer() { - return null; - } - - @Override - public Span currentSpan() { - if (this.spans.isEmpty()) { - return null; - } - return this.spans.get(spans.size() - 1); - } - - @Override - public SimpleSpan nextSpan() { - final SimpleSpan span = new SimpleSpan(); - this.spans.add(span); - return span; - } - - @Override - public ScopedSpan startScopedSpan(String name) { - return null; - } - - @Override - public Span.Builder spanBuilder() { - return new SimpleSpanBuilder(); - } - - @Override - public TraceContext.Builder traceContextBuilder() { - return null; - } - - @Override - public Map getAllBaggage() { - return new HashMap<>(); - } - - @Override - public BaggageInScope getBaggage(String name) { - return null; - } - - @Override - public BaggageInScope getBaggage(TraceContext traceContext, String name) { - return null; - } - - @Override - public BaggageInScope createBaggage(String name) { - return null; - } - - @Override - public BaggageInScope createBaggage(String name, String value) { - return null; - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.tracer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.BDDAssertions; + +import org.springframework.cloud.sleuth.BaggageInScope; +import org.springframework.cloud.sleuth.ScopedSpan; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanCustomizer; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; + +/** + * A noop implementation. Does nothing. + * + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +public class SimpleTracer implements Tracer { + + public List spans = new ArrayList<>(); + + @Override + public Span nextSpan(Span parent) { + return new SimpleSpan(); + } + + public SimpleSpan getOnlySpan() { + BDDAssertions.then(this.spans).hasSize(1); + SimpleSpan span = this.spans.get(0); + BDDAssertions.then(span.started).as("Span must be started").isTrue(); + BDDAssertions.then(span.ended).as("Span must be finished").isTrue(); + return span; + } + + public SimpleSpan getLastSpan() { + BDDAssertions.then(this.spans).isNotEmpty(); + SimpleSpan span = this.spans.get(this.spans.size() - 1); + BDDAssertions.then(span.started).as("Span must be started").isTrue(); + return span; + } + + @Override + public SpanInScope withSpan(Span span) { + return new NoOpSpanInScope(); + } + + @Override + public SpanCustomizer currentSpanCustomizer() { + return null; + } + + @Override + public Span currentSpan() { + if (this.spans.isEmpty()) { + return null; + } + return this.spans.get(spans.size() - 1); + } + + @Override + public SimpleSpan nextSpan() { + final SimpleSpan span = new SimpleSpan(); + this.spans.add(span); + return span; + } + + @Override + public ScopedSpan startScopedSpan(String name) { + return null; + } + + @Override + public Span.Builder spanBuilder() { + return new SimpleSpanBuilder(); + } + + @Override + public TraceContext.Builder traceContextBuilder() { + return null; + } + + @Override + public Map getAllBaggage() { + return new HashMap<>(); + } + + @Override + public BaggageInScope getBaggage(String name) { + return null; + } + + @Override + public BaggageInScope getBaggage(TraceContext traceContext, String name) { + return null; + } + + @Override + public BaggageInScope createBaggage(String name) { + return null; + } + + @Override + public BaggageInScope createBaggage(String name, String value) { + return null; + } + +} From 6b2d1b907d0e61573208ec166a0b52cc6aba5e06 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 13 May 2021 09:30:12 +0200 Subject: [PATCH 28/59] Fixed the build --- .../tx/TracePlatformTransactionManager.java | 348 ++++++++-------- .../tx/TraceReactiveTransactionManager.java | 376 +++++++++--------- .../sleuth/tracer/SimpleSpanBuilder.java | 218 +++++----- .../cloud/sleuth/tracer/SimpleTracer.java | 2 +- 4 files changed, 476 insertions(+), 468 deletions(-) diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManager.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManager.java index e2d60723c..28a43cfe1 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManager.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManager.java @@ -1,173 +1,175 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.tx; - -import javax.annotation.PostConstruct; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.SpanAndScope; -import org.springframework.cloud.sleuth.ThreadLocalSpan; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionException; -import org.springframework.transaction.TransactionStatus; - -/** - * A trace representation of a {@link PlatformTransactionManager}. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -public class TracePlatformTransactionManager implements PlatformTransactionManager { - - private static final Log log = LogFactory.getLog(TracePlatformTransactionManager.class); - - private final PlatformTransactionManager delegate; - - private final BeanFactory beanFactory; - - private Tracer tracer; - - volatile ThreadLocalSpan threadLocalSpan; - - public TracePlatformTransactionManager(PlatformTransactionManager delegate, BeanFactory beanFactory) { - this.delegate = delegate; - this.beanFactory = beanFactory; - } - - @PostConstruct - void initialize() { - if (this.threadLocalSpan == null) { - this.threadLocalSpan = new ThreadLocalSpan(tracer()); - } - } - - @Override - public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { - initialize(); - SpanAndScope spanAndScope = this.threadLocalSpan.get(); - Span currentSpan = spanAndScope != null ? spanAndScope.getSpan() : tracer().currentSpan(); - Span span = fallbackSpan(); - try { - TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults()); - TransactionStatus status = this.delegate.getTransaction(definition); - span = taggedSpan(currentSpan, span, def, status); - return status; - } - catch (Exception e) { - if (log.isDebugEnabled()) { - log.debug( - "Exception occurred while trying to get a transaction, will mark the span with error and report it"); - } - span.error(e); - span.end(); - throw e; - } - } - - Span fallbackSpan() { - return tracer().nextSpan().name("tx").start(); - } - - private Span taggedSpan(Span currentSpan, Span span, TransactionDefinition def, TransactionStatus status) { - if (status.isNewTransaction() || currentSpan == null) { - log.info("Creating new span cause a new transaction is started"); - TracePlatformTransactionManagerTags.tag(span, def, this.delegate.getClass()); - } - else { - span = currentSpan; - } - this.threadLocalSpan.set(span); - return span; - } - - @Override - public void commit(TransactionStatus status) throws TransactionException { - SpanAndScope spanAndScope = this.threadLocalSpan.get(); - if (spanAndScope == null) { - if (log.isDebugEnabled()) { - log.debug("No span and scope found - this shouldn't happen, sth is wrong"); - } - this.delegate.commit(status); - return; - } - Exception ex = null; - Span span = spanAndScope.getSpan(); - try { - if (log.isDebugEnabled()) { - log.debug("Wrapping commit"); - } - this.delegate.commit(status); - } - catch (Exception e) { - ex = e; - span.error(e); - throw e; - } - finally { - span.event("tx commit"); - span.end(); - if (ex == null) { - if (log.isDebugEnabled()) { - log.debug("No exception was found - will clear thread local span"); - } - this.threadLocalSpan.remove(); - } - } - } - - @Override - public void rollback(TransactionStatus status) throws TransactionException { - SpanAndScope spanAndScope = this.threadLocalSpan.get(); - if (spanAndScope == null) { - if (log.isDebugEnabled()) { - log.debug("No span and scope found - this shouldn't happen, sth is wrong"); - } - this.delegate.rollback(status); - return; - } - Span span = spanAndScope.getSpan(); - try { - if (log.isDebugEnabled()) { - log.debug("Wrapping rollback"); - } - this.delegate.rollback(status); - } - catch (Exception e) { - span.error(e); - throw e; - } - finally { - span.event("tx rollback"); - span.end(); - this.threadLocalSpan.remove(); - } - } - - private Tracer tracer() { - if (this.tracer == null) { - this.tracer = this.beanFactory.getBean(Tracer.class); - } - return this.tracer; - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.tx; + +import javax.annotation.PostConstruct; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.ThreadLocalSpan; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; + +/** + * A trace representation of a {@link PlatformTransactionManager}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TracePlatformTransactionManager implements PlatformTransactionManager { + + private static final Log log = LogFactory.getLog(TracePlatformTransactionManager.class); + + private final PlatformTransactionManager delegate; + + private final BeanFactory beanFactory; + + private Tracer tracer; + + volatile ThreadLocalSpan threadLocalSpan; + + public TracePlatformTransactionManager(PlatformTransactionManager delegate, BeanFactory beanFactory) { + this.delegate = delegate; + this.beanFactory = beanFactory; + } + + @PostConstruct + void initialize() { + if (this.threadLocalSpan == null) { + this.threadLocalSpan = new ThreadLocalSpan(tracer()); + } + } + + @Override + public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { + initialize(); + SpanAndScope spanAndScope = this.threadLocalSpan.get(); + Span currentSpan = spanAndScope != null ? spanAndScope.getSpan() : tracer().currentSpan(); + Span span = fallbackSpan(); + try { + TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults()); + TransactionStatus status = this.delegate.getTransaction(definition); + span = taggedSpan(currentSpan, span, def, status); + return status; + } + catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug( + "Exception occurred while trying to get a transaction, will mark the span with error and report it"); + } + span.error(e); + span.end(); + throw e; + } + } + + Span fallbackSpan() { + return tracer().nextSpan().name("tx").start(); + } + + private Span taggedSpan(Span currentSpan, Span span, TransactionDefinition def, TransactionStatus status) { + if (status.isNewTransaction() || currentSpan == null) { + if (log.isDebugEnabled()) { + log.debug("Creating new span cause a new transaction is started"); + } + TracePlatformTransactionManagerTags.tag(span, def, this.delegate.getClass()); + } + else { + span = currentSpan; + } + this.threadLocalSpan.set(span); + return span; + } + + @Override + public void commit(TransactionStatus status) throws TransactionException { + SpanAndScope spanAndScope = this.threadLocalSpan.get(); + if (spanAndScope == null) { + if (log.isDebugEnabled()) { + log.debug("No span and scope found - this shouldn't happen, sth is wrong"); + } + this.delegate.commit(status); + return; + } + Exception ex = null; + Span span = spanAndScope.getSpan(); + try { + if (log.isDebugEnabled()) { + log.debug("Wrapping commit"); + } + this.delegate.commit(status); + } + catch (Exception e) { + ex = e; + span.error(e); + throw e; + } + finally { + span.event("tx.commit"); + span.end(); + if (ex == null) { + if (log.isDebugEnabled()) { + log.debug("No exception was found - will clear thread local span"); + } + this.threadLocalSpan.remove(); + } + } + } + + @Override + public void rollback(TransactionStatus status) throws TransactionException { + SpanAndScope spanAndScope = this.threadLocalSpan.get(); + if (spanAndScope == null) { + if (log.isDebugEnabled()) { + log.debug("No span and scope found - this shouldn't happen, sth is wrong"); + } + this.delegate.rollback(status); + return; + } + Span span = spanAndScope.getSpan(); + try { + if (log.isDebugEnabled()) { + log.debug("Wrapping rollback"); + } + this.delegate.rollback(status); + } + catch (Exception e) { + span.error(e); + throw e; + } + finally { + span.event("tx.rollback"); + span.end(); + this.threadLocalSpan.remove(); + } + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java index c8602fcb6..a9f79a6f5 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java @@ -1,189 +1,187 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.tx; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Mono; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.SpanAndScope; -import org.springframework.cloud.sleuth.TraceContext; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.transaction.ReactiveTransaction; -import org.springframework.transaction.ReactiveTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionException; - -/** - * A trace representation of a {@link ReactiveTransactionManager}. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -public class TraceReactiveTransactionManager implements ReactiveTransactionManager { - - private static final Log log = LogFactory.getLog(TraceReactiveTransactionManager.class); - - private final ReactiveTransactionManager delegate; - - private final BeanFactory beanFactory; - - private Tracer tracer; - - private CurrentTraceContext currentTraceContext; - - public TraceReactiveTransactionManager(ReactiveTransactionManager delegate, BeanFactory beanFactory) { - this.delegate = delegate; - this.beanFactory = beanFactory; - } - - private Tracer tracer() { - if (this.tracer == null) { - this.tracer = this.beanFactory.getBean(Tracer.class); - } - return this.tracer; - } - - private CurrentTraceContext currentTraceContext() { - if (this.currentTraceContext == null) { - this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); - } - return this.currentTraceContext; - } - - @Override - public Mono getReactiveTransaction(TransactionDefinition definition) - throws TransactionException { - return Mono.deferContextual(contextView -> { - return this.delegate.getReactiveTransaction(definition).map(tx -> { - Span span = span(contextView); - if (tx.isNewTransaction() || span == null) { - if (span == null) { - span = tracer().nextSpan().name("tx").start(); - } - else { - span = tracer().nextSpan(span).name("tx").start(); - } - TracePlatformTransactionManagerTags.tag(span, definition, this.delegate.getClass()); - } - Tracer.SpanInScope withSpan = tracer().withSpan(span); - SpanAndScope spanAndScope = new SpanAndScope(span, withSpan); - return new TraceReactiveTransaction(tx, spanAndScope); - }); - }); - } - - private Span span(reactor.util.context.ContextView contextView) { - Span span = contextView.getOrDefault(Span.class, null); - if (span == null) { - TraceContext traceContext = contextView.getOrDefault(TraceContext.class, null); - if (traceContext == null) { - Span currentSpan = tracer().currentSpan(); - if (log.isDebugEnabled()) { - log.debug("There's no Span or TraceContext in the reactor context. Current span is [" + currentSpan - + "]"); - } - span = currentSpan; - } - else { - span = spanFromContext(traceContext); - } - } - return span; - } - - private Span spanFromContext(TraceContext traceContext) { - try (CurrentTraceContext.Scope scope = currentTraceContext().maybeScope(traceContext)) { - return tracer().currentSpan(); - } - } - - @Override - public Mono commit(ReactiveTransaction transaction) throws TransactionException { - if (!(transaction instanceof TraceReactiveTransaction)) { - return this.delegate.commit(transaction); - } - TraceReactiveTransaction reactiveTransaction = (TraceReactiveTransaction) transaction; - SpanAndScope spanAndScope = reactiveTransaction.spanAndScope; - Span span = spanAndScope.getSpan(); - Tracer.SpanInScope scope = spanAndScope.getScope(); - return this.delegate.commit(reactiveTransaction.delegate) - // TODO: Fix me when this is resolved in Reactor - // .doOnSubscribe(__ -> scope.close()) - .doOnError(span::error).doOnSuccess(signalType -> { - span.end(); - scope.close(); - }); - } - - @Override - public Mono rollback(ReactiveTransaction transaction) throws TransactionException { - if (!(transaction instanceof TraceReactiveTransaction)) { - return this.delegate.rollback(transaction); - } - TraceReactiveTransaction reactiveTransaction = (TraceReactiveTransaction) transaction; - SpanAndScope spanAndScope = reactiveTransaction.spanAndScope; - Span span = spanAndScope.getSpan(); - Tracer.SpanInScope scope = spanAndScope.getScope(); - return this.delegate.rollback(reactiveTransaction.delegate) - // TODO: Fix me when this is resolved in Reactor - // .doOnSubscribe(__ -> scope.close()) - .doOnError(span::error).doFinally(signalType -> { - span.end(); - if (scope != null) { - scope.close(); - } - }); - } - - static class TraceReactiveTransaction implements ReactiveTransaction { - - final ReactiveTransaction delegate; - - final SpanAndScope spanAndScope; - - TraceReactiveTransaction(ReactiveTransaction delegate, SpanAndScope spanAndScope) { - this.delegate = delegate; - this.spanAndScope = spanAndScope; - } - - @Override - public boolean isNewTransaction() { - return this.delegate.isNewTransaction(); - } - - @Override - public void setRollbackOnly() { - this.delegate.setRollbackOnly(); - } - - @Override - public boolean isRollbackOnly() { - return this.delegate.isRollbackOnly(); - } - - @Override - public boolean isCompleted() { - return this.delegate.isCompleted(); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.tx; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.transaction.ReactiveTransaction; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; + +/** + * A trace representation of a {@link ReactiveTransactionManager}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceReactiveTransactionManager implements ReactiveTransactionManager { + + private static final Log log = LogFactory.getLog(TraceReactiveTransactionManager.class); + + private final ReactiveTransactionManager delegate; + + private final BeanFactory beanFactory; + + private Tracer tracer; + + private CurrentTraceContext currentTraceContext; + + public TraceReactiveTransactionManager(ReactiveTransactionManager delegate, BeanFactory beanFactory) { + this.delegate = delegate; + this.beanFactory = beanFactory; + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + + private CurrentTraceContext currentTraceContext() { + if (this.currentTraceContext == null) { + this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); + } + return this.currentTraceContext; + } + + @Override + public Mono getReactiveTransaction(TransactionDefinition definition) + throws TransactionException { + return Mono.deferContextual(contextView -> this.delegate.getReactiveTransaction(definition).map(tx -> { + Span span = span(contextView); + if (tx.isNewTransaction() || span == null) { + if (span == null) { + span = tracer().nextSpan().name("tx").start(); + } + else { + span = tracer().nextSpan(span).name("tx").start(); + } + TracePlatformTransactionManagerTags.tag(span, definition, this.delegate.getClass()); + } + Tracer.SpanInScope withSpan = tracer().withSpan(span); + SpanAndScope spanAndScope = new SpanAndScope(span, withSpan); + return new TraceReactiveTransaction(tx, spanAndScope); + })); + } + + private Span span(reactor.util.context.ContextView contextView) { + Span span = contextView.getOrDefault(Span.class, null); + if (span == null) { + TraceContext traceContext = contextView.getOrDefault(TraceContext.class, null); + if (traceContext == null) { + Span currentSpan = tracer().currentSpan(); + if (log.isDebugEnabled()) { + log.debug("There's no Span or TraceContext in the reactor context. Current span is [" + currentSpan + + "]"); + } + span = currentSpan; + } + else { + span = spanFromContext(traceContext); + } + } + return span; + } + + private Span spanFromContext(TraceContext traceContext) { + try (CurrentTraceContext.Scope scope = currentTraceContext().maybeScope(traceContext)) { + return tracer().currentSpan(); + } + } + + @Override + public Mono commit(ReactiveTransaction transaction) throws TransactionException { + if (!(transaction instanceof TraceReactiveTransaction)) { + return this.delegate.commit(transaction); + } + TraceReactiveTransaction reactiveTransaction = (TraceReactiveTransaction) transaction; + SpanAndScope spanAndScope = reactiveTransaction.spanAndScope; + Span span = spanAndScope.getSpan(); + Tracer.SpanInScope scope = spanAndScope.getScope(); + return this.delegate.commit(reactiveTransaction.delegate) + // TODO: Fix me when this is resolved in Reactor + // .doOnSubscribe(__ -> scope.close()) + .doOnError(span::error).doOnSuccess(signalType -> { + span.end(); + scope.close(); + }); + } + + @Override + public Mono rollback(ReactiveTransaction transaction) throws TransactionException { + if (!(transaction instanceof TraceReactiveTransaction)) { + return this.delegate.rollback(transaction); + } + TraceReactiveTransaction reactiveTransaction = (TraceReactiveTransaction) transaction; + SpanAndScope spanAndScope = reactiveTransaction.spanAndScope; + Span span = spanAndScope.getSpan(); + Tracer.SpanInScope scope = spanAndScope.getScope(); + return this.delegate.rollback(reactiveTransaction.delegate) + // TODO: Fix me when this is resolved in Reactor + // .doOnSubscribe(__ -> scope.close()) + .doOnError(span::error).doFinally(signalType -> { + span.end(); + if (scope != null) { + scope.close(); + } + }); + } + + static class TraceReactiveTransaction implements ReactiveTransaction { + + final ReactiveTransaction delegate; + + final SpanAndScope spanAndScope; + + TraceReactiveTransaction(ReactiveTransaction delegate, SpanAndScope spanAndScope) { + this.delegate = delegate; + this.spanAndScope = spanAndScope; + } + + @Override + public boolean isNewTransaction() { + return this.delegate.isNewTransaction(); + } + + @Override + public void setRollbackOnly() { + this.delegate.setRollbackOnly(); + } + + @Override + public boolean isRollbackOnly() { + return this.delegate.isRollbackOnly(); + } + + @Override + public boolean isCompleted() { + return this.delegate.isCompleted(); + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java index d73faa7fe..a998b9ae8 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java @@ -1,105 +1,113 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.tracer; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.TraceContext; - -/** - * A noop implementation. Does nothing. - * - * @author Marcin Grzejszczak - * @since 3.0.0 - */ -class SimpleSpanBuilder implements Span.Builder { - - List events = new ArrayList<>(); - - Map tags = new HashMap<>(); - - Throwable error; - - Span.Kind spanKind; - - String remoteServiceName; - - String name; - - @Override - public Span.Builder setParent(TraceContext context) { - return this; - } - - @Override - public Span.Builder setNoParent() { - return this; - } - - @Override - public Span.Builder name(String name) { - this.name = name; - return this; - } - - @Override - public Span.Builder event(String value) { - this.events.add(value); - return this; - } - - @Override - public Span.Builder tag(String key, String value) { - this.tags.put(key, value); - return this; - } - - @Override - public Span.Builder error(Throwable throwable) { - this.error = throwable; - return this; - } - - @Override - public Span.Builder kind(Span.Kind spanKind) { - this.spanKind = spanKind; - return this; - } - - @Override - public Span.Builder remoteServiceName(String remoteServiceName) { - this.remoteServiceName = remoteServiceName; - return this; - } - - @Override - public Span start() { - SimpleSpan span = new SimpleSpan(); - this.tags.forEach(span::tag); - this.events.forEach(span::event); - span.remoteServiceName(this.remoteServiceName); - span.error(this.error); - span.spanKind = this.spanKind; - span.name(this.name); - return span; - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.tracer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; + +/** + * A noop implementation. Does nothing. + * + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +class SimpleSpanBuilder implements Span.Builder { + + List events = new ArrayList<>(); + + Map tags = new HashMap<>(); + + Throwable error; + + Span.Kind spanKind; + + String remoteServiceName; + + String name; + + SimpleTracer simpleTracer; + + public SimpleSpanBuilder(SimpleTracer simpleTracer) { + this.simpleTracer = simpleTracer; + } + + @Override + public Span.Builder setParent(TraceContext context) { + return this; + } + + @Override + public Span.Builder setNoParent() { + return this; + } + + @Override + public Span.Builder name(String name) { + this.name = name; + return this; + } + + @Override + public Span.Builder event(String value) { + this.events.add(value); + return this; + } + + @Override + public Span.Builder tag(String key, String value) { + this.tags.put(key, value); + return this; + } + + @Override + public Span.Builder error(Throwable throwable) { + this.error = throwable; + return this; + } + + @Override + public Span.Builder kind(Span.Kind spanKind) { + this.spanKind = spanKind; + return this; + } + + @Override + public Span.Builder remoteServiceName(String remoteServiceName) { + this.remoteServiceName = remoteServiceName; + return this; + } + + @Override + public Span start() { + SimpleSpan span = new SimpleSpan(); + this.tags.forEach(span::tag); + this.events.forEach(span::event); + span.remoteServiceName(this.remoteServiceName); + span.error(this.error); + span.spanKind = this.spanKind; + span.name(this.name); + span.start(); + simpleTracer.spans.add(span); + return span; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java index beeb01728..6735cdbcc 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java @@ -92,7 +92,7 @@ public class SimpleTracer implements Tracer { @Override public Span.Builder spanBuilder() { - return new SimpleSpanBuilder(); + return new SimpleSpanBuilder(this); } @Override From 2461ae15dd173f53ce8df09f970d86dbd70edcd9 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 13 May 2021 09:33:17 +0200 Subject: [PATCH 29/59] Fixed checkstyle --- .../cloud/sleuth/tracer/SimpleSpanBuilder.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java index a998b9ae8..bec2d510b 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java @@ -30,21 +30,21 @@ import org.springframework.cloud.sleuth.TraceContext; * @author Marcin Grzejszczak * @since 3.0.0 */ -class SimpleSpanBuilder implements Span.Builder { +public class SimpleSpanBuilder implements Span.Builder { - List events = new ArrayList<>(); + public List events = new ArrayList<>(); - Map tags = new HashMap<>(); + public Map tags = new HashMap<>(); - Throwable error; + public Throwable error; - Span.Kind spanKind; + public Span.Kind spanKind; - String remoteServiceName; + public String remoteServiceName; - String name; + public String name; - SimpleTracer simpleTracer; + public SimpleTracer simpleTracer; public SimpleSpanBuilder(SimpleTracer simpleTracer) { this.simpleTracer = simpleTracer; From 697a726cde0438949f0ce33cb4a7b12347d207a4 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 13 May 2021 10:35:14 +0200 Subject: [PATCH 30/59] Added new tags to the deployer --- .../instrument/deployer/TraceAppDeployer.java | 626 +++++++++--------- .../TraceProxyExecutionListenerTests.java | 284 ++++---- 2 files changed, 465 insertions(+), 445 deletions(-) diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java index ee63aa998..d03463e19 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java @@ -1,137 +1,155 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.deployer; - -import java.time.Duration; -import java.util.Arrays; -import java.util.Map; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.cloud.deployer.spi.app.AppDeployer; -import org.springframework.cloud.deployer.spi.app.AppScaleRequest; -import org.springframework.cloud.deployer.spi.app.AppStatus; -import org.springframework.cloud.deployer.spi.app.DeploymentState; -import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; -import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; -import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; -import org.springframework.core.env.Environment; - -/** - * Trace representation of an {@link AppDeployer}. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -public class TraceAppDeployer implements AppDeployer { - - private static final Log log = LogFactory.getLog(TraceAppDeployer.class); - - private final AppDeployer delegate; - - private final BeanFactory beanFactory; - - private final Environment environment; - - private Tracer tracer; - - private CurrentTraceContext currentTraceContext; - - private Long pollDelay; - - public TraceAppDeployer(AppDeployer delegate, BeanFactory beanFactory, Environment environment) { - this.delegate = delegate; - this.beanFactory = beanFactory; - this.environment = environment; - } - - @Override - public String deploy(AppDeploymentRequest request) { - Span.Builder spanBuilder = clientSpan("deploy"); - Span span = spanBuilder.start(); - // Span span = tracer().nextSpan().name("deploy"); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { - span.event("deployer.start"); - String id = this.delegate.deploy(request); - span.tag("deployer.app.id", id); - registerListener(span, id); - return id; - } - } - - private Span.Builder clientSpan(String name) { - Span.Builder spanBuilder = tracer().spanBuilder(); - Span currentSpan = tracer().currentSpan(); - if (currentSpan != null) { - spanBuilder.setParent(currentSpan.context()); - } - return clientSpanKind(name, spanBuilder); - } - - private Span.Builder clientSpanKind(String name, Span.Builder spanBuilder) { - return spanBuilder.kind(Span.Kind.CLIENT).name(name).remoteServiceName(remoteServiceName()); - } - - private Span.Builder clientSpan(String name, Span parentSpan) { - Span.Builder spanBuilder = tracer().spanBuilder(); - Span currentSpan = parentSpan != null ? parentSpan : tracer().currentSpan(); - if (currentSpan != null) { - spanBuilder.setParent(currentSpan.context()); - } - Map platformSpecificInfo = environmentInfo().getPlatformSpecificInfo(); - addCfTags(spanBuilder, platformSpecificInfo); - addK8sTags(spanBuilder, platformSpecificInfo); - return clientSpanKind(name, spanBuilder); - } - - private void addCfTags(Span.Builder spanBuilder, Map platformSpecificInfo) { - if (platformSpecificInfo.containsKey("API Endpoint")) { - spanBuilder.tag("deployer.platform.cf.url", platformSpecificInfo.get("API Endpoint")); - } - if (platformSpecificInfo.containsKey("Organization")) { - spanBuilder.tag("deployer.platform.cf.org", platformSpecificInfo.get("Organization")); - } - if (platformSpecificInfo.containsKey("Space")) { - spanBuilder.tag("deployer.platform.cf.space", platformSpecificInfo.get("Space")); - } - } - - private void addK8sTags(Span.Builder spanBuilder, Map platformSpecificInfo) { - if (platformSpecificInfo.containsKey("master-url")) { - spanBuilder.tag("deployer.platform.k8s.url", platformSpecificInfo.get("master-url")); - } - if (platformSpecificInfo.containsKey("namespace")) { - spanBuilder.tag("deployer.platform.k8s.namespace", platformSpecificInfo.get("namespace")); - } - } - - private String remoteServiceName() { - return environmentInfo().getPlatformType(); - } - - private void registerListener(Span span, String id) { - PreviousAndCurrentStatus previousAndCurrentStatus = new PreviousAndCurrentStatus(span); +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.deployer; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.deployer.spi.app.AppDeployer; +import org.springframework.cloud.deployer.spi.app.AppScaleRequest; +import org.springframework.cloud.deployer.spi.app.AppStatus; +import org.springframework.cloud.deployer.spi.app.DeploymentState; +import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; +import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; +import org.springframework.core.env.Environment; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Trace representation of an {@link AppDeployer}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceAppDeployer implements AppDeployer { + + private static final Log log = LogFactory.getLog(TraceAppDeployer.class); + + private final AppDeployer delegate; + + private final BeanFactory beanFactory; + + private final Environment environment; + + private Tracer tracer; + + private CurrentTraceContext currentTraceContext; + + private Long pollDelay; + + public TraceAppDeployer(AppDeployer delegate, BeanFactory beanFactory, Environment environment) { + this.delegate = delegate; + this.beanFactory = beanFactory; + this.environment = environment; + } + + @Override + public String deploy(AppDeploymentRequest request) { + Span.Builder spanBuilder = clientSpan("deploy", request); + Span span = spanBuilder.start(); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { + span.event("deployer.start"); + String id = this.delegate.deploy(request); + span.tag("deployer.app.id", id); + registerListener(span, id); + return id; + } + } + + private Span.Builder clientSpan(String name) { + return clientSpan(name, null, null); + } + + private Span.Builder clientSpan(String name, @Nullable AppDeploymentRequest request) { + return clientSpan(name, null, request); + } + + private Span.Builder clientSpanKind(String name, Span.Builder spanBuilder) { + return spanBuilder.kind(Span.Kind.CLIENT).name(name).remoteServiceName(remoteServiceName()); + } + + private Span.Builder clientSpan(String name, Span parentSpan) { + return clientSpan(name, parentSpan, null); + } + + private Span.Builder clientSpan(String name, @Nullable Span parentSpan, @Nullable AppDeploymentRequest request) { + Span.Builder spanBuilder = tracer().spanBuilder(); + Span currentSpan = parentSpan != null ? parentSpan : tracer().currentSpan(); + if (currentSpan != null) { + spanBuilder.setParent(currentSpan.context()); + } + Map platformSpecificInfo = environmentInfo().getPlatformSpecificInfo(); + if (request != null) { + String platformName = request.getDeploymentProperties().get("spring.cloud.deployer.platformName"); + if (StringUtils.hasText(platformName)) { + spanBuilder.tag("deployer.platform.name", platformName); + } + String appName = request.getDeploymentProperties().get("spring.cloud.deployer.appName"); + if (StringUtils.hasText(appName)) { + spanBuilder.tag("deployer.app.name", appName); + } + String group = request.getDeploymentProperties().get("spring.cloud.deployer.group"); + if (StringUtils.hasText(group)) { + spanBuilder.tag("deployer.app.name", group); + } + } + addCfTags(spanBuilder, platformSpecificInfo); + addK8sTags(spanBuilder, platformSpecificInfo); + return clientSpanKind(name, spanBuilder); + } + + private void addCfTags(Span.Builder spanBuilder, Map platformSpecificInfo) { + if (platformSpecificInfo.containsKey("API Endpoint")) { + spanBuilder.tag("deployer.platform.cf.url", platformSpecificInfo.get("API Endpoint")); + } + if (platformSpecificInfo.containsKey("Organization")) { + spanBuilder.tag("deployer.platform.cf.org", platformSpecificInfo.get("Organization")); + } + if (platformSpecificInfo.containsKey("Space")) { + spanBuilder.tag("deployer.platform.cf.space", platformSpecificInfo.get("Space")); + } + } + + private void addK8sTags(Span.Builder spanBuilder, Map platformSpecificInfo) { + if (platformSpecificInfo.containsKey("master-url")) { + spanBuilder.tag("deployer.platform.k8s.url", platformSpecificInfo.get("master-url")); + } + if (platformSpecificInfo.containsKey("namespace")) { + spanBuilder.tag("deployer.platform.k8s.namespace", platformSpecificInfo.get("namespace")); + } + } + + private String remoteServiceName() { + return environmentInfo().getPlatformType(); + } + + private void registerListener(Span span, String id) { + PreviousAndCurrentStatus previousAndCurrentStatus = new PreviousAndCurrentStatus(span); // @formatter:off this.delegate.statusReactive(id) .map(previousAndCurrentStatus::updateCurrent) @@ -142,173 +160,173 @@ public class TraceAppDeployer implements AppDeployer { .doOnError(span::error) // we will close the span in the reactive part .doFinally(signalType -> span.end()).subscribe(); - // @formatter:on - } - - @Override - public void undeploy(String id) { - Span.Builder spanBuilder = clientSpan("undeploy"); - Span span = spanBuilder.start(); - span.tag("deployer.app.id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { - span.event("deployer.start"); - this.delegate.undeploy(id); - registerListener(span, id); - } - finally { - span.end(); - } - } - - @Override - public AppStatus status(String id) { - Span.Builder spanBuilder = clientSpan("status"); - Span span = spanBuilder.start(); - span.tag("deployer.app.id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - return this.delegate.status(id); - } - finally { - span.end(); - } - } - - @Override - public Mono statusReactive(String id) { - return ReactorSleuth.tracedMono(tracer(), currentTraceContext(), "status", - () -> this.delegate.statusReactive(id), (o, span) -> span.tag("deployer.app.id", id), - span -> clientSpan("status", span).start()); - } - - @Override - public Flux statusesReactive(String... ids) { - return ReactorSleuth.tracedFlux(tracer(), currentTraceContext(), "statuses", - () -> this.delegate.statusesReactive(ids), - (o, span) -> span.tag("deployer.app.ids", Arrays.toString(ids)), - span -> clientSpan("statuses", span).start()); - } - - @Override - public RuntimeEnvironmentInfo environmentInfo() { - return this.delegate.environmentInfo(); - } - - @Override - public String getLog(String id) { - Span.Builder spanBuilder = clientSpan("getLog"); - Span span = spanBuilder.start(); - span.tag("deployer.app.id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { - return this.delegate.getLog(id); - } - finally { - span.end(); - } - } - - @Override - public void scale(AppScaleRequest appScaleRequest) { - Span.Builder spanBuilder = clientSpan("scale"); - Span span = spanBuilder.start(); - span.tag("deployer.scale.deploymentId", appScaleRequest.getDeploymentId()); - span.tag("deployer.scale.count", String.valueOf(appScaleRequest.getCount())); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - this.delegate.scale(appScaleRequest); - } - finally { - span.end(); - } - } - - private Tracer tracer() { - if (this.tracer == null) { - this.tracer = this.beanFactory.getBean(Tracer.class); - } - return this.tracer; - } - - private CurrentTraceContext currentTraceContext() { - if (this.currentTraceContext == null) { - this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); - } - return this.currentTraceContext; - } - - private long pollDelay() { - if (this.pollDelay == null) { - this.pollDelay = this.environment.getProperty("spring.sleuth.deployer.status-poll-delay", Long.class, 500L); - } - return this.pollDelay; - } - - private static final class PreviousAndCurrentStatus { - - private final Span span; - - private AppStatus current; - - private AppStatus previous; - - private PreviousAndCurrentStatus(Span span) { - this.span = span; - if (log.isDebugEnabled()) { - log.debug("Current span is [" + span + "]"); - } - } - - private PreviousAndCurrentStatus updateCurrent(AppStatus current) { - if (log.isTraceEnabled()) { - log.trace("State before change: current [" + this.current + "], previous [" + this.previous + "]"); - } - this.previous = this.current; - this.current = current; - if (log.isTraceEnabled()) { - log.trace("State after change: current [" + this.current + "], previous [" + this.previous + "]"); - } - if (statusChanged()) { - annotateSpan(); - } - else if (log.isTraceEnabled()) { - log.trace("State has not changed, will not annotate the span"); - } - return this; - } - - private void annotateSpan() { - String name = this.current.getState().name(); - if (log.isDebugEnabled()) { - log.debug("Will annotate its state with [" + name + "]"); - } - this.span.event(name); - } - - private boolean statusChanged() { - if (this.previous == null && this.current != null) { - if (log.isDebugEnabled()) { - log.debug("Previous is null, current is not null"); - } - return true; - } - else if (this.current == null) { - throw new IllegalStateException("Current state can't be null"); - } - DeploymentState currentState = this.current.getState(); - DeploymentState previousState = this.previous.getState(); - return currentState != previousState; - } - - private boolean isFinished() { - boolean finished = this.current.getState() == DeploymentState.deployed - || this.current.getState() == DeploymentState.undeployed - || this.current.getState() == DeploymentState.failed - || this.current.getState() == DeploymentState.error - || this.current.getState() == DeploymentState.unknown; - if (log.isTraceEnabled()) { - log.trace("Status is finished [" + finished + "]"); - } - return finished; - } - - } - -} + // @formatter:on + } + + @Override + public void undeploy(String id) { + Span.Builder spanBuilder = clientSpan("undeploy"); + Span span = spanBuilder.start(); + span.tag("deployer.app.id", id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { + span.event("deployer.start"); + this.delegate.undeploy(id); + registerListener(span, id); + } + finally { + span.end(); + } + } + + @Override + public AppStatus status(String id) { + Span.Builder spanBuilder = clientSpan("status"); + Span span = spanBuilder.start(); + span.tag("deployer.app.id", id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + return this.delegate.status(id); + } + finally { + span.end(); + } + } + + @Override + public Mono statusReactive(String id) { + return ReactorSleuth.tracedMono(tracer(), currentTraceContext(), "status", + () -> this.delegate.statusReactive(id), (o, span) -> span.tag("deployer.app.id", id), + span -> clientSpan("status", span).start()); + } + + @Override + public Flux statusesReactive(String... ids) { + return ReactorSleuth.tracedFlux(tracer(), currentTraceContext(), "statuses", + () -> this.delegate.statusesReactive(ids), + (o, span) -> span.tag("deployer.app.ids", Arrays.toString(ids)), + span -> clientSpan("statuses", span).start()); + } + + @Override + public RuntimeEnvironmentInfo environmentInfo() { + return this.delegate.environmentInfo(); + } + + @Override + public String getLog(String id) { + Span.Builder spanBuilder = clientSpan("getLog"); + Span span = spanBuilder.start(); + span.tag("deployer.app.id", id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { + return this.delegate.getLog(id); + } + finally { + span.end(); + } + } + + @Override + public void scale(AppScaleRequest appScaleRequest) { + Span.Builder spanBuilder = clientSpan("scale"); + Span span = spanBuilder.start(); + span.tag("deployer.scale.deploymentId", appScaleRequest.getDeploymentId()); + span.tag("deployer.scale.count", String.valueOf(appScaleRequest.getCount())); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + this.delegate.scale(appScaleRequest); + } + finally { + span.end(); + } + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + + private CurrentTraceContext currentTraceContext() { + if (this.currentTraceContext == null) { + this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); + } + return this.currentTraceContext; + } + + private long pollDelay() { + if (this.pollDelay == null) { + this.pollDelay = this.environment.getProperty("spring.sleuth.deployer.status-poll-delay", Long.class, 500L); + } + return this.pollDelay; + } + + private static final class PreviousAndCurrentStatus { + + private final Span span; + + private AppStatus current; + + private AppStatus previous; + + private PreviousAndCurrentStatus(Span span) { + this.span = span; + if (log.isDebugEnabled()) { + log.debug("Current span is [" + span + "]"); + } + } + + private PreviousAndCurrentStatus updateCurrent(AppStatus current) { + if (log.isTraceEnabled()) { + log.trace("State before change: current [" + this.current + "], previous [" + this.previous + "]"); + } + this.previous = this.current; + this.current = current; + if (log.isTraceEnabled()) { + log.trace("State after change: current [" + this.current + "], previous [" + this.previous + "]"); + } + if (statusChanged()) { + annotateSpan(); + } + else if (log.isTraceEnabled()) { + log.trace("State has not changed, will not annotate the span"); + } + return this; + } + + private void annotateSpan() { + String name = this.current.getState().name(); + if (log.isDebugEnabled()) { + log.debug("Will annotate its state with [" + name + "]"); + } + this.span.event(name); + } + + private boolean statusChanged() { + if (this.previous == null && this.current != null) { + if (log.isDebugEnabled()) { + log.debug("Previous is null, current is not null"); + } + return true; + } + else if (this.current == null) { + throw new IllegalStateException("Current state can't be null"); + } + DeploymentState currentState = this.current.getState(); + DeploymentState previousState = this.previous.getState(); + return currentState != previousState; + } + + private boolean isFinished() { + boolean finished = this.current.getState() == DeploymentState.deployed + || this.current.getState() == DeploymentState.undeployed + || this.current.getState() == DeploymentState.failed + || this.current.getState() == DeploymentState.error + || this.current.getState() == DeploymentState.unknown; + if (log.isTraceEnabled()) { + log.trace("Status is finished [" + finished + "]"); + } + return finished; + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListenerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListenerTests.java index 3ad277a1c..1abf353e9 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListenerTests.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListenerTests.java @@ -1,141 +1,143 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.r2dbc; - -import java.util.concurrent.atomic.AtomicReference; - -import io.r2dbc.proxy.core.QueryExecutionInfo; -import io.r2dbc.proxy.core.ValueStore; -import io.r2dbc.proxy.test.MockQueryExecutionInfo; -import io.r2dbc.spi.Connection; -import io.r2dbc.spi.ConnectionFactory; -import io.r2dbc.spi.ConnectionFactoryMetadata; -import org.junit.jupiter.api.Test; -import org.reactivestreams.Publisher; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.support.StaticListableBeanFactory; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.tracer.SimpleSpan; -import org.springframework.cloud.sleuth.tracer.SimpleTracer; - -import static org.assertj.core.api.BDDAssertions.then; - -class TraceProxyExecutionListenerTests { - - SimpleTracer simpleTracer = new SimpleTracer(); - - ConnectionFactory connectionFactory = connectionFactory(); - - TraceProxyExecutionListener listener = new TraceProxyExecutionListener(beanFactory(), connectionFactory); - - @Test - void should_do_nothing_on_before_query_when_there_was_no_previous_span() { - MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); - - listener.beforeQuery(queryExecutionInfo); - - then(this.simpleTracer.spans).isEmpty(); - then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); - } - - @Test - void should_do_nothing_on_after_query_when_there_was_no_previous_span() { - MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); - - listener.afterQuery(queryExecutionInfo); - - then(this.simpleTracer.spans).isEmpty(); - then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); - } - - @Test - void should_do_nothing_on_each_query_when_there_was_no_previous_span() { - MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); - - listener.eachQueryResult(queryExecutionInfo); - - then(this.simpleTracer.spans).isEmpty(); - then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); - } - - @Test - void should_put_a_child_span_in_value_store_when_a_span_was_already_in_context() { - MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); - this.simpleTracer.nextSpan().start(); - AtomicReference clientSpan = new AtomicReference<>(); - listener = new TraceProxyExecutionListener(beanFactory(), connectionFactory) { - @Override - Span clientSpan(QueryExecutionInfo executionInfo, String name) { - Span span = super.clientSpan(executionInfo, name); - clientSpan.set(span); - return span; - } - }; - - listener.beforeQuery(queryExecutionInfo); - - then(queryExecutionInfo.getValueStore().get(Span.class)).isSameAs(clientSpan.get()); - } - - @Test - void should_annotate_a_span_on_query_result() { - SimpleSpan span = new SimpleSpan(); - ValueStore valueStore = ValueStore.create(); - valueStore.put(Span.class, span); - MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.builder().valueStore(valueStore).build(); - - listener.eachQueryResult(queryExecutionInfo); - - then(span.events).isNotEmpty(); - } - - @Test - void should_end_span_on_after_query() { - SimpleSpan span = new SimpleSpan().start(); - ValueStore valueStore = ValueStore.create(); - valueStore.put(Span.class, span); - MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.builder().throwable(new RuntimeException()) - .valueStore(valueStore).build(); - - listener.afterQuery(queryExecutionInfo); - - then(span.throwable).isNotNull(); - then(span.ended).isTrue(); - } - - private ConnectionFactory connectionFactory() { - return new ConnectionFactory() { - @Override - public Publisher create() { - return null; - } - - @Override - public ConnectionFactoryMetadata getMetadata() { - return () -> "my-name"; - } - }; - } - - private BeanFactory beanFactory() { - StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); - beanFactory.addBean("tracer", this.simpleTracer); - return beanFactory; - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.r2dbc; + +import java.util.concurrent.atomic.AtomicReference; + +import io.r2dbc.proxy.core.QueryExecutionInfo; +import io.r2dbc.proxy.core.ValueStore; +import io.r2dbc.proxy.test.MockQueryExecutionInfo; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryMetadata; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcProperties; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.tracer.SimpleSpan; +import org.springframework.cloud.sleuth.tracer.SimpleTracer; + +import static org.assertj.core.api.BDDAssertions.then; + +class TraceProxyExecutionListenerTests { + + SimpleTracer simpleTracer = new SimpleTracer(); + + ConnectionFactory connectionFactory = connectionFactory(); + + TraceProxyExecutionListener listener = new TraceProxyExecutionListener(beanFactory(), connectionFactory); + + @Test + void should_do_nothing_on_before_query_when_there_was_no_previous_span() { + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); + + listener.beforeQuery(queryExecutionInfo); + + then(this.simpleTracer.spans).isEmpty(); + then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); + } + + @Test + void should_do_nothing_on_after_query_when_there_was_no_previous_span() { + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); + + listener.afterQuery(queryExecutionInfo); + + then(this.simpleTracer.spans).isEmpty(); + then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); + } + + @Test + void should_do_nothing_on_each_query_when_there_was_no_previous_span() { + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); + + listener.eachQueryResult(queryExecutionInfo); + + then(this.simpleTracer.spans).isEmpty(); + then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); + } + + @Test + void should_put_a_child_span_in_value_store_when_a_span_was_already_in_context() { + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); + this.simpleTracer.nextSpan().start(); + AtomicReference clientSpan = new AtomicReference<>(); + listener = new TraceProxyExecutionListener(beanFactory(), connectionFactory) { + @Override + Span clientSpan(QueryExecutionInfo executionInfo, String name) { + Span span = super.clientSpan(executionInfo, name); + clientSpan.set(span); + return span; + } + }; + + listener.beforeQuery(queryExecutionInfo); + + then(queryExecutionInfo.getValueStore().get(Span.class)).isSameAs(clientSpan.get()); + } + + @Test + void should_annotate_a_span_on_query_result() { + SimpleSpan span = new SimpleSpan(); + ValueStore valueStore = ValueStore.create(); + valueStore.put(Span.class, span); + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.builder().valueStore(valueStore).build(); + + listener.eachQueryResult(queryExecutionInfo); + + then(span.events).isNotEmpty(); + } + + @Test + void should_end_span_on_after_query() { + SimpleSpan span = new SimpleSpan().start(); + ValueStore valueStore = ValueStore.create(); + valueStore.put(Span.class, span); + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.builder().throwable(new RuntimeException()) + .valueStore(valueStore).build(); + + listener.afterQuery(queryExecutionInfo); + + then(span.throwable).isNotNull(); + then(span.ended).isTrue(); + } + + private ConnectionFactory connectionFactory() { + return new ConnectionFactory() { + @Override + public Publisher create() { + return null; + } + + @Override + public ConnectionFactoryMetadata getMetadata() { + return () -> "my-name"; + } + }; + } + + private BeanFactory beanFactory() { + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); + beanFactory.addBean("tracer", this.simpleTracer); + beanFactory.addBean("r2dbcProperties", new R2dbcProperties()); + return beanFactory; + } + +} From d2041cbd47ca7cd232b04530a9cf9e468bd3fb40 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 18 May 2021 14:50:04 +0200 Subject: [PATCH 31/59] Automatic tag table generation Without this change we don't even really know how many tags are created and what are their values. With this change we want this information to be automatically rendered. --- docs/pom.xml | 59 +- docs/src/main/asciidoc/_configprops.adoc | 8 +- docs/src/main/asciidoc/_spans.adoc | 407 +++++++++++ docs/src/main/asciidoc/appendix.adoc | 7 + docs/src/main/asciidoc/project-features.adoc | 5 + .../sleuth/documentation/DocsFromSources.java | 74 ++ .../sleuth/documentation/KeyValueEntry.java | 67 ++ .../cloud/sleuth/documentation/SpanEntry.java | 104 +++ .../SpanSearchingFileVisitor.java | 232 +++++++ .../documentation/DocsFromSourcesTests.java | 45 ++ .../documentation/SleuthAnnotationSpan.java | 111 +++ .../sleuth/documentation/SleuthAsyncSpan.java | 95 +++ pom.xml | 8 + .../springframework/cloud/sleuth/Span.java | 444 ++++++------ .../cloud/sleuth/Taggable.java | 36 + .../cloud/sleuth/docs/AssertingSpan.java | 166 +++++ .../sleuth/docs/AssertingSpanBuilder.java | 151 ++++ .../sleuth/docs/AssertingSpanCustomizer.java | 111 +++ .../cloud/sleuth/docs/DocumentedSpan.java | 116 ++++ .../sleuth/docs/DocumentedSpanAssertions.java | 149 ++++ .../cloud/sleuth/docs/EventValue.java | 32 + .../sleuth/docs/ImmutableAssertingSpan.java | 83 +++ .../docs/ImmutableAssertingSpanBuilder.java | 70 ++ .../ImmutableAssertingSpanCustomizer.java | 65 ++ .../cloud/sleuth/docs/TagKey.java | 43 ++ .../docs/DocumentedSpanAssertionsTests.java | 307 +++++++++ .../sleuth/brave/bridge/BraveScopedSpan.java | 12 +- .../cloud/sleuth/brave/bridge/BraveSpan.java | 17 +- .../sleuth/brave/bridge/BraveSpanBuilder.java | 289 ++++---- .../brave/bridge/BraveSpanCustomizer.java | 3 +- .../sleuth/brave/bridge/BraveTracer.java | 4 +- ...stractSleuthMethodInvocationProcessor.java | 11 +- ...eactorSleuthMethodInvocationProcessor.java | 2 +- ...eactorSleuthMethodInvocationProcessor.java | 4 +- .../annotation/SleuthAnnotationSpan.java | 111 +++ .../instrument/async/SleuthAsyncSpan.java | 87 +++ .../instrument/async/TraceAsyncAspect.java | 16 +- .../instrument/async/TraceCallable.java | 3 +- .../instrument/async/TraceRunnable.java | 3 +- .../instrument/batch/SleuthBatchSpan.java | 134 ++++ .../batch/TraceJobExecutionListener.java | 43 +- .../batch/TraceStepExecutionListener.java | 17 +- .../SleuthCircuitBreakerSpan.java | 43 ++ .../circuitbreaker/TraceFunction.java | 9 +- .../instrument/config/SleuthConfigSpan.java | 63 ++ .../TraceEnvironmentRepositoryAspect.java | 11 +- .../deployer/SleuthDeployerSpan.java | 287 ++++++++ .../instrument/deployer/TraceAppDeployer.java | 650 +++++++++--------- .../instrument/kafka/KafkaTracingUtils.java | 12 +- .../instrument/kafka/SleuthKafkaSpan.java | 112 +++ .../kafka/TracingKafkaProducer.java | 7 +- .../DefaultMessageSpanCustomizer.java | 6 +- .../messaging/SleuthMessagingSpan.java | 63 ++ .../messaging/TraceMessageHandler.java | 10 +- .../messaging/TracingChannelInterceptor.java | 2 +- .../instrument/quartz/SleuthQuartzSpan.java | 53 ++ .../instrument/quartz/TracingJobListener.java | 8 +- .../instrument/r2dbc/SleuthR2dbcSpan.java | 93 +++ .../r2dbc/TraceProxyExecutionListener.java | 269 ++++---- .../instrument/rsocket/SleuthRSocketSpan.java | 78 +++ .../rsocket/TracingRequesterRSocketProxy.java | 12 +- .../rsocket/TracingResponderRSocketProxy.java | 5 +- .../rxjava/SleuthRxJavaSchedulersHook.java | 7 +- .../instrument/rxjava/SleuthRxJavaSpan.java | 62 ++ .../scheduling/SleuthSchedulingSpan.java | 66 ++ .../scheduling/TraceSchedulingAspect.java | 17 +- .../instrument/task/SleuthTaskSpan.java | 43 ++ .../task/TraceApplicationRunner.java | 2 +- .../task/TraceCommandLineRunner.java | 2 +- .../task/TraceTaskExecutionListener.java | 3 +- .../sleuth/instrument/tx/SleuthTxSpan.java | 134 ++++ .../tx/TracePlatformTransactionManager.java | 350 +++++----- .../TracePlatformTransactionManagerTags.java | 24 +- .../tx/TraceReactiveTransactionManager.java | 375 +++++----- .../sleuth/instrument/web/SleuthWebSpan.java | 81 +++ .../sleuth/instrument/web/TraceWebFilter.java | 22 +- .../instrument/web/mvc/HandlerParser.java | 14 +- .../instrument/web/mvc/SleuthMvcSpan.java | 64 ++ .../TraceProxyExecutionListenerTests.java | 292 ++++---- .../TracePlatformTransactionManagerTests.java | 3 +- .../sleuth/tracer/SimpleSpanBuilder.java | 226 +++--- .../cloud/sleuth/tracer/SimpleTracer.java | 256 +++---- .../src/test/resources/application.yml | 2 +- .../quartz/TracingJobListenerTest.java | 4 +- 84 files changed, 5902 insertions(+), 1691 deletions(-) create mode 100644 docs/src/main/asciidoc/_spans.adoc create mode 100644 docs/src/main/java/org/springframework/cloud/sleuth/documentation/DocsFromSources.java create mode 100644 docs/src/main/java/org/springframework/cloud/sleuth/documentation/KeyValueEntry.java create mode 100644 docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanEntry.java create mode 100644 docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanSearchingFileVisitor.java create mode 100644 docs/src/test/java/org/springframework/cloud/sleuth/documentation/DocsFromSourcesTests.java create mode 100644 docs/src/test/java/org/springframework/cloud/sleuth/documentation/SleuthAnnotationSpan.java create mode 100644 docs/src/test/java/org/springframework/cloud/sleuth/documentation/SleuthAsyncSpan.java create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Taggable.java create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanCustomizer.java create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/DocumentedSpan.java create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/DocumentedSpanAssertions.java create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/EventValue.java create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/ImmutableAssertingSpan.java create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/ImmutableAssertingSpanBuilder.java create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/ImmutableAssertingSpanCustomizer.java create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/TagKey.java create mode 100644 spring-cloud-sleuth-api/src/test/java/org/springframework/cloud/sleuth/docs/DocumentedSpanAssertionsTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/SleuthAnnotationSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/SleuthAsyncSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/SleuthBatchSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/SleuthCircuitBreakerSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/config/SleuthConfigSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/SleuthDeployerSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/SleuthKafkaSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/SleuthMessagingSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/quartz/SleuthQuartzSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/SleuthR2dbcSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/SleuthRSocketSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rxjava/SleuthRxJavaSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/scheduling/SleuthSchedulingSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/SleuthTaskSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/SleuthTxSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/SleuthWebSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/mvc/SleuthMvcSpan.java diff --git a/docs/pom.xml b/docs/pom.xml index da80c56ff..2481231ef 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -32,6 +32,10 @@ ${basedir}/.. spring.sleuth.*|spring.zipkin.* deploy + + + 2.22.3.Final + .*/spring-cloud-sleuth-instrumentation/.*|.*/spring-cloud-sleuth-brave/.* @@ -42,9 +46,43 @@ ${project.groupId} spring-cloud-sleuth-zipkin + + org.jboss.forge.roaster + roaster-api + ${roaster.version} + + + org.jboss.forge.roaster + roaster-jdt + ${roaster.version} + + + org.springframework.boot + spring-boot-starter-test + test + - src/main/asciidoc + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + generate-sources + + add-source + + + + src/main/asciidoc + + + + + + @@ -64,6 +102,25 @@ org.codehaus.mojo exec-maven-plugin + + + build-tags-and-spans + prepare-package + + java + + + org.springframework.cloud.sleuth.documentation.DocsFromSources + + ${maven.multiModuleProjectDirectory} + ${module.inclusionPattern} + ${maven.multiModuleProjectDirectory}/docs/src/main/asciidoc/ + + true + true + + + org.asciidoctor diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index e2c13e903..9b43bd350 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -9,10 +9,11 @@ |spring.sleuth.baggage.local-fields | | |spring.sleuth.baggage.remote-fields | | List of fields that are referenced the same in-process as it is on the wire. For example, the field "x-vcap-request-id" would be set as-is including the prefix. |spring.sleuth.baggage.tag-fields | | +|spring.sleuth.batch.enabled | `true` | Enable Spring Batch instrumentation. |spring.sleuth.circuitbreaker.enabled | `true` | Enable Spring Cloud CircuitBreaker instrumentation. |spring.sleuth.config.server.enabled | `true` | Enable Spring Cloud Config Server instrumentation. |spring.sleuth.deployer.enabled | `true` | Enable Spring Cloud Deployer instrumentation. -|spring.sleuth.deployer.status-poll-delay | `1000` | Default poll delay to retrieve the deployed application status. +|spring.sleuth.deployer.status-poll-delay | `500` | Default poll delay to retrieve the deployed application status. |spring.sleuth.enabled | `true` | |spring.sleuth.feign.enabled | `true` | Enable span information propagation when using Feign. |spring.sleuth.feign.processor.enabled | `true` | Enable post processor that wraps Feign Context in its tracing representations. @@ -22,6 +23,8 @@ |spring.sleuth.integration.enabled | `true` | Enable Spring Integration instrumentation. |spring.sleuth.integration.patterns | `[!hystrixStreamOutput*, *, !channel*]` | An array of patterns against which channel names will be matched. @see org.springframework.integration.config.GlobalChannelInterceptor#patterns() Defaults to any channel name not matching the Hystrix Stream and functional Stream channel names. |spring.sleuth.integration.websockets.enabled | `true` | Enable tracing for WebSockets. +|spring.sleuth.kafka.enabled | `true` | Enable instrumenting of Apache Kafka clients. +|spring.sleuth.messaging.aspect.enabled | `false` | Should {@link MessageMapping} wrapping be enabled. |spring.sleuth.messaging.enabled | `false` | Should messaging be turned on. |spring.sleuth.messaging.jms.enabled | `true` | Enable tracing of JMS. |spring.sleuth.messaging.jms.remote-service-name | `jms` | JMS remote service name. @@ -35,12 +38,14 @@ |spring.sleuth.opentracing.enabled | `true` | Enables OpenTracing support. |spring.sleuth.propagation.type | | Tracing context propagation types. |spring.sleuth.quartz.enabled | `true` | Enable tracing for Quartz. +|spring.sleuth.r2dbc.enabled | `true` | Enable R2dbc instrumentation. |spring.sleuth.reactor.decorate-on-each | `true` | When true decorates on each operator, will be less performing, but logging will always contain the tracing entries in each operator. When false decorates on last operator, will be more performing, but logging might not always contain the tracing entries. @deprecated use explicit value via {@link SleuthReactorProperties#instrumentationType} |spring.sleuth.reactor.enabled | `true` | When true enables instrumentation for reactor. |spring.sleuth.reactor.instrumentation-type | | |spring.sleuth.redis.enabled | `true` | Enable span information propagation when using Redis. |spring.sleuth.redis.remote-service-name | `redis` | Service name for the remote Redis endpoint. |spring.sleuth.rpc.enabled | `true` | Enable tracing of RPC. +|spring.sleuth.rsocket.enabled | `true` | When true enables instrumentation for rsocket. |spring.sleuth.rxjava.schedulers.hook.enabled | `true` | Enable support for RxJava via RxJavaSchedulersHook. |spring.sleuth.rxjava.schedulers.ignoredthreads | `[HystrixMetricPoller, ^RxComputation.*$]` | Thread names for which spans will not be sampled. |spring.sleuth.sampler.probability | | Probability of requests that should be sampled. E.g. 1.0 - 100% requests should be sampled. The precision is whole-numbers only (i.e. there's no support for 0.1% of the traces). @@ -55,6 +60,7 @@ |spring.sleuth.task.enabled | `true` | Enable Spring Cloud Task instrumentation. |spring.sleuth.trace-id128 | `false` | When true, generate 128-bit trace IDs instead of 64-bit ones. |spring.sleuth.tracer.mode | | Set which tracer implementation should be picked. +|spring.sleuth.tx.enabled | `true` | Enable Spring TX instrumentation. |spring.sleuth.web.additional-skip-pattern | | Additional pattern for URLs that should be skipped in tracing. This will be appended to the {@link SleuthWebProperties#skipPattern}. |spring.sleuth.web.client.enabled | `true` | Enable interceptor injecting into {@link org.springframework.web.client.RestTemplate}. |spring.sleuth.web.client.skip-pattern | | Pattern for URLs that should be skipped in client side tracing. diff --git a/docs/src/main/asciidoc/_spans.adoc b/docs/src/main/asciidoc/_spans.adoc new file mode 100644 index 000000000..b07649e68 --- /dev/null +++ b/docs/src/main/asciidoc/_spans.adoc @@ -0,0 +1,407 @@ +=== Annotation New Or Continue Span + +> Span that wraps a @NewSpan or @ContinueSpan annotations. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.annotation.SleuthAnnotationSpan` + +.Tag Keys +|=== +|Name | Description +|class|Class name where a method got annotated with a Sleuth annotation. +|method|Method name that got annotated with Sleuth annotation. +|=== + +.Event Values +|=== +|Name | Description +|%s.after|Annotated after executing a method annotated with @ContinueSpan or @NewSpan. (since the name contains `%s` the final value will be resolved at runtime) +|%s.afterFailure|Annotated after throwing an exception from a method annotated with @ContinueSpan or @NewSpan. (since the name contains `%s` the final value will be resolved at runtime) +|%s.before|Annotated before executing a method annotated with @ContinueSpan or @NewSpan. (since the name contains `%s` the final value will be resolved at runtime) +|=== + +=== Async Annotation Span + +> Span that wraps a @Async annotation. Either continues an existing one or creates a new one if there was no present one. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.async.SleuthAsyncSpan` + +.Tag Keys +|=== +|Name | Description +|class|Class name where a method got annotated with @Async. +|method|Method name that got annotated with @Async. +|=== + +=== Async Callable Span + +> Span created whenever a Callable needs to be instrumented. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.async.SleuthAsyncSpan` + +=== Async Runnable Span + +> Span created whenever a Runnable needs to be instrumented. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.async.SleuthAsyncSpan` + +=== Batch Job Span + +> Span created around a Job execution. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.batch.SleuthBatchSpan` + +.Tag Keys +|=== +|Name | Description +|batch.job.executionId|ID of the Spring Batch execution. +|batch.job.instanceId|ID of the Spring Batch job instance. +|batch.job.name|Name of the Spring Batch job. +|=== + +=== Batch Step Span + +> Span created around a Job execution. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.batch.SleuthBatchSpan` + +.Tag Keys +|=== +|Name | Description +|batch.job.executionId|ID of the Spring Batch execution. +|batch.step.executionId|ID of the Spring Batch execution. +|batch.step.name|Name of the Spring Batch job. +|batch.step.type|Type of the Spring Batch job. +|=== + +=== Circuit Breaker Function Span + +> Span created when we wrap a Function passed to the CircuitBreaker. as fallback. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.circuitbreaker.SleuthCircuitBreakerSpan` + +=== Circuit Breaker Supplier Span + +> Span created when we wrap a Supplier passed to the CircuitBreaker. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.circuitbreaker.SleuthCircuitBreakerSpan` + +=== Config Span + +> Span created around an EnvironmentRepository. + +**Span name** `find`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.config.SleuthConfigSpan` + +.Tag Keys +|=== +|Name | Description +|config.environment.class|Implementation of the EnvironmentRepository. +|config.environment.method|Method executed on the EnvironmentRepository. +|=== + +=== Deployer Deploy Span + +> Span created upon deploying of an application. + +**Span name** `deploy`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.deployer.SleuthDeployerSpan` + +.Tag Keys +|=== +|Name | Description +|deployer.app.group|Group of the deployed application. +|deployer.app.id|ID of the deployed application. +|deployer.app.name|Name of the deployed application. +|deployer.platform.cf.org|CloudFoundry org. +|deployer.platform.cf.space|CloudFoundry space. +|deployer.platform.cf.url|CloudFoundry API URL. +|deployer.platform.k8s.namespace|Kubernetes namespace. +|deployer.platform.k8s.url|Kubernetes API URL. +|deployer.platform.name|Name of the platform to which apps are being deployed. +|=== + +.Event Values +|=== +|Name | Description +|%s|When deployer changes the state of the deployed application. (since the name contains `%s` the final value will be resolved at runtime) +|deployer.start|When deployer started deploying the application. +|=== + +=== Deployer Get Log Span + +> Span created upon asking for logs of deployed applications. + +**Span name** `getLog`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.deployer.SleuthDeployerSpan` + +.Tag Keys +|=== +|Name | Description +|deployer.app.group|Group of the deployed application. +|deployer.app.id|ID of the deployed application. +|deployer.app.name|Name of the deployed application. +|deployer.platform.cf.org|CloudFoundry org. +|deployer.platform.cf.space|CloudFoundry space. +|deployer.platform.cf.url|CloudFoundry API URL. +|deployer.platform.k8s.namespace|Kubernetes namespace. +|deployer.platform.k8s.url|Kubernetes API URL. +|deployer.platform.name|Name of the platform to which apps are being deployed. +|=== + +.Event Values +|=== +|Name | Description +|%s|When deployer changes the state of the deployed application. (since the name contains `%s` the final value will be resolved at runtime) +|deployer.start|When deployer started deploying the application. +|=== + +=== Deployer Scale Span + +> Span created upon asking for logs of deployed applications. + +**Span name** `scale`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.deployer.SleuthDeployerSpan` + +.Tag Keys +|=== +|Name | Description +|deployer.app.group|Group of the deployed application. +|deployer.app.id|ID of the deployed application. +|deployer.app.name|Name of the deployed application. +|deployer.platform.cf.org|CloudFoundry org. +|deployer.platform.cf.space|CloudFoundry space. +|deployer.platform.cf.url|CloudFoundry API URL. +|deployer.platform.k8s.namespace|Kubernetes namespace. +|deployer.platform.k8s.url|Kubernetes API URL. +|deployer.platform.name|Name of the platform to which apps are being deployed. +|deployer.scale.count|Scale count. +|deployer.scale.deploymentId|Scale command deployment id. +|=== + +.Event Values +|=== +|Name | Description +|%s|When deployer changes the state of the deployed application. (since the name contains `%s` the final value will be resolved at runtime) +|deployer.start|When deployer started deploying the application. +|=== + +=== Deployer Statuses Span + +> Span created upon asking for statuses of deployed applications. + +**Span name** `statuses`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.deployer.SleuthDeployerSpan` + +.Tag Keys +|=== +|Name | Description +|deployer.app.group|Group of the deployed application. +|deployer.app.id|ID of the deployed application. +|deployer.app.name|Name of the deployed application. +|deployer.platform.cf.org|CloudFoundry org. +|deployer.platform.cf.space|CloudFoundry space. +|deployer.platform.cf.url|CloudFoundry API URL. +|deployer.platform.k8s.namespace|Kubernetes namespace. +|deployer.platform.k8s.url|Kubernetes API URL. +|deployer.platform.name|Name of the platform to which apps are being deployed. +|=== + +.Event Values +|=== +|Name | Description +|%s|When deployer changes the state of the deployed application. (since the name contains `%s` the final value will be resolved at runtime) +|deployer.start|When deployer started deploying the application. +|=== + +=== Deployer Status Span + +> Span created upon asking for a status of a deployed application. + +**Span name** `status`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.deployer.SleuthDeployerSpan` + +.Tag Keys +|=== +|Name | Description +|deployer.app.group|Group of the deployed application. +|deployer.app.id|ID of the deployed application. +|deployer.app.name|Name of the deployed application. +|deployer.platform.cf.org|CloudFoundry org. +|deployer.platform.cf.space|CloudFoundry space. +|deployer.platform.cf.url|CloudFoundry API URL. +|deployer.platform.k8s.namespace|Kubernetes namespace. +|deployer.platform.k8s.url|Kubernetes API URL. +|deployer.platform.name|Name of the platform to which apps are being deployed. +|=== + +.Event Values +|=== +|Name | Description +|%s|When deployer changes the state of the deployed application. (since the name contains `%s` the final value will be resolved at runtime) +|deployer.start|When deployer started deploying the application. +|=== + +=== Deployer Undeploy Span + +> Span created upon undeploying of an application. + +**Span name** `undeploy`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.deployer.SleuthDeployerSpan` + +.Tag Keys +|=== +|Name | Description +|deployer.app.group|Group of the deployed application. +|deployer.app.id|ID of the deployed application. +|deployer.app.name|Name of the deployed application. +|deployer.platform.cf.org|CloudFoundry org. +|deployer.platform.cf.space|CloudFoundry space. +|deployer.platform.cf.url|CloudFoundry API URL. +|deployer.platform.k8s.namespace|Kubernetes namespace. +|deployer.platform.k8s.url|Kubernetes API URL. +|deployer.platform.name|Name of the platform to which apps are being deployed. +|=== + +.Event Values +|=== +|Name | Description +|%s|When deployer changes the state of the deployed application. (since the name contains `%s` the final value will be resolved at runtime) +|deployer.start|When deployer started deploying the application. +|=== + +=== Messaging Span + +> Span created when message is sent or received. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.messaging.SleuthMessagingSpan` + +.Tag Keys +|=== +|Name | Description +|%s|User provided keys via customization options. (since the name contains `%s` the final value will be resolved at runtime) +|channel|Name of the Spring Integration channel. +|=== + +=== Mvc Handler Interceptor Span + +> Span around a HandlerInterceptor. Will continue the current span and tag it + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.web.mvc.SleuthMvcSpan` + +.Tag Keys +|=== +|Name | Description +|mvc.controller.class|Class name where a method got annotated with @Scheduled. +|mvc.controller.method|Method name that got annotated with @Scheduled. +|=== + +=== Quartz Trigger Span + +> Span created when trigger is fired and then completed. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.quartz.SleuthQuartzSpan` + +.Tag Keys +|=== +|Name | Description +|quartz.trigger|Name of the trigger. +|=== + +=== Rsocket Responder Span + +> Span created on the RSocket responder side. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.rsocket.SleuthRSocketSpan` + +=== Rx Java Trace Action Span + +> Span that wraps a Rx Java . + +**Span name** `rxjava`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.rxjava.SleuthRxJavaSpan` + +.Tag Keys +|=== +|Name | Description +|thread|Name of the thread. +|=== + +=== Scheduled Annotation Span + +> Span that wraps a annotated method. Either creates a new span or continues an existing one. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.scheduling.SleuthSchedulingSpan` + +.Tag Keys +|=== +|Name | Description +|class|Class name where a method got annotated with @Scheduled. +|method|Method name that got annotated with @Scheduled. +|=== + +=== Task Execution Listener Span + +> Span created within the lifecycle of a task. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.task.SleuthTaskSpan` + +=== Task Runner Span + +> Span created when a task runner is executed. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.task.SleuthTaskSpan` + +=== Web Filter Span + +> Span around a WebFilter. Will continue the current span or create a new one and tag it + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.web.SleuthWebSpan` + +.Tag Keys +|=== +|Name | Description +|http.status_code|Response status code. +|mvc.controller.class|Name of the class that is processing the request. +|mvc.controller.method|Name of the method that is processing the request. +|=== + diff --git a/docs/src/main/asciidoc/appendix.adoc b/docs/src/main/asciidoc/appendix.adoc index b2d7034c2..216338c13 100644 --- a/docs/src/main/asciidoc/appendix.adoc +++ b/docs/src/main/asciidoc/appendix.adoc @@ -1,6 +1,13 @@ :numbered!: [[appendix]] +[[sleuth-spans]] +== Spring Cloud Sleuth Spans + +Below you can find a list of all the spans that are created by Spring Cloud Sleuth. + +include::_spans.adoc[] + [[common-application-properties]] == Common application properties diff --git a/docs/src/main/asciidoc/project-features.adoc b/docs/src/main/asciidoc/project-features.adoc index a84029762..4379349b2 100644 --- a/docs/src/main/asciidoc/project-features.adoc +++ b/docs/src/main/asciidoc/project-features.adoc @@ -480,6 +480,11 @@ That Logback configuration file: NOTE: If you use a custom `logback-spring.xml`, you must pass the `spring.application.name` in the `bootstrap` rather than the `application` property file. Otherwise, your custom logback file does not properly read the property. +[[features-self-documenting-spans]] +== Self Documenting Spans + +A declarative format of representing span configuration was introduced via the `DocumentedSpan` abstraction. By analyzing Sleuth's source code an appendix with all span characteristics is created (including allowed tag keys and event names). You can check the <> for more information. + [[features-whats-next]] == What to Read Next diff --git a/docs/src/main/java/org/springframework/cloud/sleuth/documentation/DocsFromSources.java b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/DocsFromSources.java new file mode 100644 index 000000000..868f2a13d --- /dev/null +++ b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/DocsFromSources.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.documentation; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.TreeSet; +import java.util.regex.Pattern; + +public class DocsFromSources { + + private final File projectRoot; + + private final Pattern inclusionPattern; + + private final File outputDir; + + public DocsFromSources(File projectRoot, Pattern inclusionPattern, File outputDir) { + this.projectRoot = projectRoot; + this.inclusionPattern = inclusionPattern; + this.outputDir = outputDir; + } + + public static void main(String... args) { + String projectRoot = args[0]; + String inclusionPattern = args[1]; + inclusionPattern = inclusionPattern.replace("/", File.separator); + String output = args[2]; + new DocsFromSources(new File(projectRoot), Pattern.compile(inclusionPattern), new File(output)).generate(); + } + + public void generate() { + Path path = this.projectRoot.toPath(); + System.out.println("Inclusion pattern is [" + this.inclusionPattern + "]"); + Collection spanEntries = new TreeSet<>(); + FileVisitor fv = new SpanSearchingFileVisitor(this.inclusionPattern, spanEntries); + try { + Files.walkFileTree(path, fv); + Path output = new File(this.outputDir, "_spans.adoc").toPath(); + StringBuilder stringBuilder = new StringBuilder(); + System.out.println("======================================"); + System.out.println("Summary of sources analysis"); + System.out.println("Found [" + spanEntries.size() + "] spans"); + System.out.println( + "Found [" + spanEntries.stream().flatMap(e -> e.tagKeys.stream()).distinct().count() + "] tags"); + System.out.println( + "Found [" + spanEntries.stream().flatMap(e -> e.events.stream()).distinct().count() + "] events"); + spanEntries.forEach(spanEntry -> stringBuilder.append(spanEntry.toString()).append("\n\n")); + Files.write(output, stringBuilder.toString().getBytes()); + } + catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + +} diff --git a/docs/src/main/java/org/springframework/cloud/sleuth/documentation/KeyValueEntry.java b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/KeyValueEntry.java new file mode 100644 index 000000000..7ac690bd6 --- /dev/null +++ b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/KeyValueEntry.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.documentation; + +import java.util.Objects; + +class KeyValueEntry implements Comparable { + + final String name; + + final String description; + + KeyValueEntry(String name, String description) { + this.name = name; + this.description = description; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + KeyValueEntry tag = (KeyValueEntry) o; + return Objects.equals(name, tag.name) && Objects.equals(description, tag.description); + } + + @Override + public int hashCode() { + return Objects.hash(name, description); + } + + @Override + public int compareTo(KeyValueEntry o) { + return name.compareTo(o.name); + } + + @Override + public String toString() { + return "|" + name + "|" + description(); + } + + private String description() { + String suffix = ""; + if (this.name.contains("%s")) { + suffix = " (since the name contains `%s` the final value will be resolved at runtime)"; + } + return description + suffix; + } + +} diff --git a/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanEntry.java b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanEntry.java new file mode 100644 index 000000000..5788812ae --- /dev/null +++ b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanEntry.java @@ -0,0 +1,104 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.documentation; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Locale; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +class SpanEntry implements Comparable { + + final String name; + + final String enclosingClass; + + final String enumName; + + final String description; + + final Collection tagKeys; + + final Collection events; + + SpanEntry(String name, String enclosingClass, String enumName, String description, + Collection tagKeys, Collection events) { + Assert.isTrue(StringUtils.hasText(name), "Span name must not be empty"); + Assert.isTrue(StringUtils.hasText(description), "Span description must not be empty"); + this.name = name; + this.enclosingClass = enclosingClass; + this.enumName = enumName; + this.description = description; + this.tagKeys = tagKeys; + this.events = events; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SpanEntry spanEntry = (SpanEntry) o; + return Objects.equals(name, spanEntry.name) && Objects.equals(enclosingClass, spanEntry.enclosingClass) + && Objects.equals(enumName, spanEntry.enumName) && Objects.equals(description, spanEntry.description) + && Objects.equals(tagKeys, spanEntry.tagKeys) && Objects.equals(events, spanEntry.events); + } + + @Override + public int hashCode() { + return Objects.hash(name, enclosingClass, enumName, description, tagKeys, events); + } + + @Override + public int compareTo(SpanEntry o) { + return enumName.compareTo(o.enumName); + } + + @Override + //@formatter:off + public String toString() { + StringBuilder text = new StringBuilder() + .append("=== ").append(Arrays.stream(enumName.replace("_", " ").split(" ")).map(s -> StringUtils.capitalize(s.toLowerCase(Locale.ROOT))).collect(Collectors.joining(" "))) + .append("\n\n> ").append(description).append("\n\n") + .append("**Span name** `").append(name).append("`"); + if (name.contains("%s")) { + text.append(" - since it contains `%s`, the name is dynamic and will be resolved at runtime."); + } + else { + text.append("."); + } + text.append("\n\n").append("Fully qualified name of the enclosing class `").append(this.enclosingClass).append("`"); + if (!tagKeys.isEmpty()) { + text.append("\n\n.Tag Keys\n|===\n|Name | Description\n").append(this.tagKeys.stream().map(KeyValueEntry::toString).collect(Collectors.joining("\n"))) + .append("\n|==="); + } + if (!events.isEmpty()) { + text.append("\n\n.Event Values\n|===\n|Name | Description\n").append(this.events.stream().map(KeyValueEntry::toString).collect(Collectors.joining("\n"))) + .append("\n|==="); + } + return text.toString(); + } + //@formatter:on + +} diff --git a/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanSearchingFileVisitor.java b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanSearchingFileVisitor.java new file mode 100644 index 000000000..22c43411e --- /dev/null +++ b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanSearchingFileVisitor.java @@ -0,0 +1,232 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.documentation; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jboss.forge.roaster.Roaster; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.Expression; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.MethodDeclaration; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.MethodInvocation; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.ReturnStatement; +import org.jboss.forge.roaster._shade.org.eclipse.jdt.core.dom.StringLiteral; +import org.jboss.forge.roaster.model.JavaType; +import org.jboss.forge.roaster.model.JavaUnit; +import org.jboss.forge.roaster.model.impl.JavaEnumImpl; +import org.jboss.forge.roaster.model.source.EnumConstantSource; +import org.jboss.forge.roaster.model.source.JavaSource; +import org.jboss.forge.roaster.model.source.MemberSource; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.EventValue; +import org.springframework.cloud.sleuth.docs.TagKey; + +class SpanSearchingFileVisitor extends SimpleFileVisitor { + + private final Pattern pattern; + + private final Collection spanEntries; + + SpanSearchingFileVisitor(Pattern pattern, Collection spanEntries) { + this.pattern = pattern; + this.spanEntries = spanEntries; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (!pattern.matcher(file.toString()).matches()) { + return FileVisitResult.CONTINUE; + } + else if (!file.toString().endsWith(".java")) { + return FileVisitResult.CONTINUE; + } + try (InputStream stream = Files.newInputStream(file)) { + JavaUnit unit = Roaster.parseUnit(stream); + JavaType myClass = unit.getGoverningType(); + if (!(myClass instanceof JavaEnumImpl)) { + return FileVisitResult.CONTINUE; + } + JavaEnumImpl myEnum = (JavaEnumImpl) myClass; + if (!myEnum.getInterfaces().contains(DocumentedSpan.class.getCanonicalName())) { + return FileVisitResult.CONTINUE; + } + System.out.println("Checking [" + myEnum.getName() + "]"); + if (myEnum.getEnumConstants().size() == 0) { + return FileVisitResult.CONTINUE; + } + for (EnumConstantSource enumConstant : myEnum.getEnumConstants()) { + SpanEntry entry = parseSpan(enumConstant, myEnum); + if (entry != null) { + spanEntries.add(entry); + System.out.println( + "Found [" + entry.tagKeys.size() + "] tags and [" + entry.events.size() + "] events"); + } + } + return FileVisitResult.CONTINUE; + } + } + + private SpanEntry parseSpan(EnumConstantSource enumConstant, JavaEnumImpl myEnum) { + List> members = enumConstant.getBody().getMembers(); + if (members.isEmpty()) { + return null; + } + String name = ""; + String description = enumConstant.getJavaDoc().getText(); + Collection tags = new TreeSet<>(); + Collection events = new TreeSet<>(); + for (MemberSource member : members) { + Object internal = member.getInternal(); + if (!(internal instanceof MethodDeclaration)) { + return null; + } + MethodDeclaration methodDeclaration = (MethodDeclaration) internal; + String methodName = methodDeclaration.getName().getIdentifier(); + if ("getName".equals(methodName)) { + name = readStringReturnValue(methodDeclaration); + } + else if ("getTagKeys".equals(methodName)) { + tags.addAll(keyValueEntries(myEnum, methodDeclaration, TagKey.class)); + } + else if ("getEvents".equals(methodName)) { + events.addAll(keyValueEntries(myEnum, methodDeclaration, EventValue.class)); + } + else { + return null; + } + } + return new SpanEntry(name, myEnum.getCanonicalName(), enumConstant.getName(), description, tags, events); + } + + private Collection keyValueEntries(JavaEnumImpl myEnum, MethodDeclaration methodDeclaration, + Class requiredClass) { + Collection enumNames = readClassValue(methodDeclaration); + Collection keyValues = new TreeSet<>(); + enumNames.forEach(enumName -> { + List> nestedTypes = myEnum.getNestedTypes(); + JavaSource nestedSource = nestedTypes.stream() + .filter(javaSource -> javaSource.getName().equals(enumName)).findFirst().orElseThrow( + () -> new IllegalStateException("There's no nested type with name [" + enumName + "]")); + updateKeyValuesFromEnum(myEnum, nestedSource, requiredClass, keyValues); + }); + return keyValues; + } + + private void updateKeyValuesFromEnum(JavaEnumImpl parentEnum, JavaSource source, Class requiredClass, + Collection keyValues) { + if (!(source instanceof JavaEnumImpl)) { + return; + } + JavaEnumImpl myEnum = (JavaEnumImpl) source; + if (!myEnum.getInterfaces().contains(requiredClass.getCanonicalName())) { + return; + } + System.out.println("Checking [" + parentEnum.getName() + "." + myEnum.getName() + "]"); + if (myEnum.getEnumConstants().size() == 0) { + return; + } + for (EnumConstantSource enumConstant : myEnum.getEnumConstants()) { + String keyValue = enumKeyValue(enumConstant); + keyValues.add(new KeyValueEntry(keyValue, enumConstant.getJavaDoc().getText())); + } + } + + private String enumKeyValue(EnumConstantSource enumConstant) { + List> members = enumConstant.getBody().getMembers(); + if (members.isEmpty()) { + System.err.println("No method declarations in the enum."); + return ""; + } + Object internal = members.get(0).getInternal(); + if (!(internal instanceof MethodDeclaration)) { + System.err.println("Can't read the member [" + internal.getClass() + "] as a method declaration."); + return ""; + } + MethodDeclaration methodDeclaration = (MethodDeclaration) internal; + if (methodDeclaration.getBody().statements().isEmpty()) { + System.err.println("Body was empty. Continuing..."); + return ""; + } + return stringFromReturnMethodDeclaration(methodDeclaration); + } + + private String stringFromReturnMethodDeclaration(MethodDeclaration methodDeclaration) { + Object statement = methodDeclaration.getBody().statements().get(0); + if (!(statement instanceof ReturnStatement)) { + System.err.println("Statement [" + statement.getClass() + "] is not a return statement."); + return ""; + } + ReturnStatement returnStatement = (ReturnStatement) statement; + Expression expression = returnStatement.getExpression(); + if (!(expression instanceof StringLiteral)) { + System.err.println("Statement [" + statement.getClass() + "] is not a string literal statement."); + return ""; + } + return ((StringLiteral) expression).getLiteralValue(); + } + + private String readStringReturnValue(MethodDeclaration methodDeclaration) { + return stringFromReturnMethodDeclaration(methodDeclaration); + } + + private Collection readClassValue(MethodDeclaration methodDeclaration) { + Object statement = methodDeclaration.getBody().statements().get(0); + if (!(statement instanceof ReturnStatement)) { + System.err.println("Statement [" + statement.getClass() + "] is not a return statement."); + return Collections.emptyList(); + } + ReturnStatement returnStatement = (ReturnStatement) statement; + Expression expression = returnStatement.getExpression(); + if (!(expression instanceof MethodInvocation)) { + System.err.println("Statement [" + statement.getClass() + "] is not a method invocation."); + return Collections.emptyList(); + } + MethodInvocation methodInvocation = (MethodInvocation) expression; + if ("merge".equals(methodInvocation.getName().getIdentifier())) { + // TODO: There must be a better way to do this... + // TagKey.merge(TestSpanTags.values(),AsyncSpanTags.values()) + String invocationString = methodInvocation.toString(); + Matcher matcher = Pattern.compile("([a-zA-Z]+.values)").matcher(invocationString); + Collection classNames = new TreeSet<>(); + while (matcher.find()) { + String className = matcher.group(1).split("\\.")[0]; + classNames.add(className); + } + return classNames; + } + else if (!methodInvocation.toString().endsWith(".values()")) { + throw new IllegalStateException("You have to use the static .values() method on the enum that implements " + + TagKey.class + " or " + EventValue.class + + " interface or use [TagKey.merge(...)] method to merge multiple values from tags"); + } + // will return Tags + return Collections.singletonList(methodInvocation.getExpression().toString()); + } + +} diff --git a/docs/src/test/java/org/springframework/cloud/sleuth/documentation/DocsFromSourcesTests.java b/docs/src/test/java/org/springframework/cloud/sleuth/documentation/DocsFromSourcesTests.java new file mode 100644 index 000000000..258736614 --- /dev/null +++ b/docs/src/test/java/org/springframework/cloud/sleuth/documentation/DocsFromSourcesTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.documentation; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.regex.Pattern; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +class DocsFromSourcesTests { + + @Test + void should_build_a_table_out_of_enum_tag_key() throws IOException { + File root = new File("."); + File output = new File(root, "target"); + + new DocsFromSources(root, Pattern.compile(".*"), output).generate(); + + BDDAssertions.then(new String(Files.readAllBytes(new File(output, "_spans.adoc").toPath()))) + .contains("=== Async Annotation Span").contains("> Span that wraps a") + .contains("**Span name** `%s` - since").contains("Fully qualified name of") + .contains("|class|Class name where a method got annotated with @Async.") + .contains("=== Annotation New Or Continue Span") + .contains("|%s.before|Annotated before executing a method annotated with @ContinueSpan or @NewSpan.") + .contains("=== Test Span").contains("**Span name** `fixed`.").contains("|foooooo|Test foo"); + } + +} diff --git a/docs/src/test/java/org/springframework/cloud/sleuth/documentation/SleuthAnnotationSpan.java b/docs/src/test/java/org/springframework/cloud/sleuth/documentation/SleuthAnnotationSpan.java new file mode 100644 index 000000000..710768b16 --- /dev/null +++ b/docs/src/test/java/org/springframework/cloud/sleuth/documentation/SleuthAnnotationSpan.java @@ -0,0 +1,111 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.documentation; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.EventValue; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthAnnotationSpan implements DocumentedSpan { + + /** + * Span that wraps a @NewSpan or @ContinueSpan annotations. + */ + ANNOTATION_NEW_OR_CONTINUE_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + @Override + public EventValue[] getEvents() { + return Events.values(); + } + + }; + + /** + * Tags related to Sleuth annotations. + * + * @author Marcin Grzejszczak + * @since 3.0.3 + */ + enum Tags implements TagKey { + + /** + * Class name where a method got annotated with a Sleuth annotation. + */ + CLASS { + @Override + public String getKey() { + return "class"; + } + }, + + /** + * Method name that got annotated with Sleuth annotation. + */ + METHOD { + @Override + public String getKey() { + return "method"; + } + } + + } + + enum Events implements EventValue { + + /** + * Annotated before executing a method annotated with @ContinueSpan or @NewSpan. + */ + BEFORE { + @Override + public String getValue() { + return "%s.before"; + } + }, + + /** + * Annotated after executing a method annotated with @ContinueSpan or @NewSpan. + */ + AFTER { + @Override + public String getValue() { + return "%s.after"; + } + }, + + /** + * Annotated after throwing an exception from a method annotated + * with @ContinueSpan or @NewSpan. + */ + AFTER_FAILURE { + @Override + public String getValue() { + return "%.afterFailure"; + } + } + + } + +} diff --git a/docs/src/test/java/org/springframework/cloud/sleuth/documentation/SleuthAsyncSpan.java b/docs/src/test/java/org/springframework/cloud/sleuth/documentation/SleuthAsyncSpan.java new file mode 100644 index 000000000..b879b7fed --- /dev/null +++ b/docs/src/test/java/org/springframework/cloud/sleuth/documentation/SleuthAsyncSpan.java @@ -0,0 +1,95 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.documentation; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthAsyncSpan implements DocumentedSpan { + + /** + * Span that wraps a @Async annotation. Either continues an existing one or creates a + * new one if there was no present one. + */ + ASYNC_ANNOTATION_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return AsyncSpanTags.values(); + } + + }, + + /** + * Test span. + */ + TEST_SPAN { + @Override + public String getName() { + return "fixed"; + } + + @Override + public TagKey[] getTagKeys() { + return TagKey.merge(TestSpanTags.values(), AsyncSpanTags.values()); + } + + }; + + enum AsyncSpanTags implements TagKey { + + /** + * Class name where a method got annotated with @Async. + */ + CLASS { + @Override + public String getKey() { + return "class"; + } + }, + + /** + * Method name that got annotated with @Async. + */ + METHOD { + @Override + public String getKey() { + return "method"; + } + } + + } + + enum TestSpanTags implements TagKey { + + /** + * Test foo + */ + FOO { + @Override + public String getKey() { + return "foooooo"; + } + } + + } + +} diff --git a/pom.xml b/pom.xml index 3da2a0c29..986d5be7d 100644 --- a/pom.xml +++ b/pom.xml @@ -150,6 +150,14 @@ maven-deploy-plugin 2.8.2 + + maven-surefire-plugin + + + true + + + diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Span.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Span.java index 6cc8c84c3..444b8e1eb 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Span.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Span.java @@ -1,222 +1,222 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth; - -import org.springframework.cloud.sleuth.propagation.Propagator; - -/** - * - * This API was heavily influenced by Brave. Parts of its documentation were taken - * directly from Brave. - * - * Span is a single unit of work that needs to be started and stopped. Contains timing - * information and events and tags. - * - * @author OpenZipkin Brave Authors - * @author Marcin Grzejszczak - * @since 3.0.0 - */ -public interface Span extends SpanCustomizer { - - /** - * @return {@code true} when no recording is done and nothing is reported to an - * external system. However, this span should still be injected into outgoing - * requests. Use this flag to avoid performing expensive computation. - */ - boolean isNoop(); - - /** - * @return {@link TraceContext} corresponding to this span. - */ - TraceContext context(); - - /** - * Starts this span. - * @return this span - */ - Span start(); - - /** - * Sets a name on this span. - * @param name name to set on the span - * @return this span - */ - Span name(String name); - - /** - * Sets an event on this span. - * @param value event name to set on the span - * @return this span - */ - Span event(String value); - - /** - * Sets a tag on this span. - * @param key tag key - * @param value tag value - * @return this span - */ - Span tag(String key, String value); - - /** - * Records an exception for this span. - * @param throwable to record - * @return this span - */ - Span error(Throwable throwable); - - /** - * Ends the span. The span gets stopped and recorded if not noop. - */ - void end(); - - /** - * Ends the span. The span gets stopped but does not get recorded. - */ - void abandon(); - - /** - * Sets the remote service name for the span. - * @param remoteServiceName remote service name - * @return this span - * @since 3.0.3 - */ - default Span remoteServiceName(String remoteServiceName) { - return this; - } - - /** - * Type of span. Can be used to specify additional relationships between spans in - * addition to a parent/child relationship. - * - * Documentation of the enum taken from OpenTelemetry. - */ - enum Kind { - - /** - * Indicates that the span covers server-side handling of an RPC or other remote - * request. - */ - SERVER, - - /** - * Indicates that the span covers the client-side wrapper around an RPC or other - * remote request. - */ - CLIENT, - - /** - * Indicates that the span describes producer sending a message to a broker. - * Unlike client and server, there is no direct critical path latency relationship - * between producer and consumer spans. - */ - PRODUCER, - - /** - * Indicates that the span describes consumer receiving a message from a broker. - * Unlike client and server, there is no direct critical path latency relationship - * between producer and consumer spans. - */ - CONSUMER - - } - - /** - * In some cases (e.g. when dealing with - * {@link Propagator#extract(Object, Propagator.Getter)}'s we want to create a span - * that has not yet been started, yet it's heavily configurable (some options are not - * possible to be set when a span has already been started). We can achieve that by - * using a builder. - * - * Inspired by OpenZipkin Brave and OpenTelemetry API. - */ - interface Builder { - - /** - * Sets the parent of the built span. - * @param context parent's context - * @return this - */ - Builder setParent(TraceContext context); - - /** - * Sets no parent of the built span. - * @return this - */ - Builder setNoParent(); - - /** - * Sets the name of the span. - * @param name span name - * @return this - */ - Builder name(String name); - - /** - * Sets an event on the span. - * @param value event value - * @return this - */ - Builder event(String value); - - /** - * Sets a tag on the span. - * @param key tag key - * @param value tag value - * @return this - */ - Builder tag(String key, String value); - - /** - * Sets an error on the span. - * @param throwable error to set - * @return this - */ - Builder error(Throwable throwable); - - /** - * Sets the kind on the span. - * @param spanKind kind of the span - * @return this - */ - Builder kind(Span.Kind spanKind); - - /** - * Sets the remote service name for the span. - * @param remoteServiceName remote service name - * @return this - */ - Builder remoteServiceName(String remoteServiceName); - - /** - * Sets the remote URL for the span. - * @param remoteUrl remote service name - * @return this - */ - default Builder remoteUrl(String remoteUrl) { - return this; - } - - /** - * Builds and starts the span. - * @return started span - */ - Span start(); - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth; + +import org.springframework.cloud.sleuth.propagation.Propagator; + +/** + * + * This API was heavily influenced by Brave. Parts of its documentation were taken + * directly from Brave. + * + * Span is a single unit of work that needs to be started and stopped. Contains timing + * information and events and tags. + * + * @author OpenZipkin Brave Authors + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +public interface Span extends SpanCustomizer { + + /** + * @return {@code true} when no recording is done and nothing is reported to an + * external system. However, this span should still be injected into outgoing + * requests. Use this flag to avoid performing expensive computation. + */ + boolean isNoop(); + + /** + * @return {@link TraceContext} corresponding to this span. + */ + TraceContext context(); + + /** + * Starts this span. + * @return this span + */ + Span start(); + + /** + * Sets a name on this span. + * @param name name to set on the span + * @return this span + */ + Span name(String name); + + /** + * Sets an event on this span. + * @param value event name to set on the span + * @return this span + */ + Span event(String value); + + /** + * Sets a tag on this span. + * @param key tag key + * @param value tag value + * @return this span + */ + Span tag(String key, String value); + + /** + * Records an exception for this span. + * @param throwable to record + * @return this span + */ + Span error(Throwable throwable); + + /** + * Ends the span. The span gets stopped and recorded if not noop. + */ + void end(); + + /** + * Ends the span. The span gets stopped but does not get recorded. + */ + void abandon(); + + /** + * Sets the remote service name for the span. + * @param remoteServiceName remote service name + * @return this span + * @since 3.0.3 + */ + default Span remoteServiceName(String remoteServiceName) { + return this; + } + + /** + * Type of span. Can be used to specify additional relationships between spans in + * addition to a parent/child relationship. + * + * Documentation of the enum taken from OpenTelemetry. + */ + enum Kind { + + /** + * Indicates that the span covers server-side handling of an RPC or other remote + * request. + */ + SERVER, + + /** + * Indicates that the span covers the client-side wrapper around an RPC or other + * remote request. + */ + CLIENT, + + /** + * Indicates that the span describes producer sending a message to a broker. + * Unlike client and server, there is no direct critical path latency relationship + * between producer and consumer spans. + */ + PRODUCER, + + /** + * Indicates that the span describes consumer receiving a message from a broker. + * Unlike client and server, there is no direct critical path latency relationship + * between producer and consumer spans. + */ + CONSUMER + + } + + /** + * In some cases (e.g. when dealing with + * {@link Propagator#extract(Object, Propagator.Getter)}'s we want to create a span + * that has not yet been started, yet it's heavily configurable (some options are not + * possible to be set when a span has already been started). We can achieve that by + * using a builder. + * + * Inspired by OpenZipkin Brave and OpenTelemetry API. + */ + interface Builder { + + /** + * Sets the parent of the built span. + * @param context parent's context + * @return this + */ + Builder setParent(TraceContext context); + + /** + * Sets no parent of the built span. + * @return this + */ + Builder setNoParent(); + + /** + * Sets the name of the span. + * @param name span name + * @return this + */ + Builder name(String name); + + /** + * Sets an event on the span. + * @param value event value + * @return this + */ + Builder event(String value); + + /** + * Sets a tag on the span. + * @param key tag key + * @param value tag value + * @return this + */ + Builder tag(String key, String value); + + /** + * Sets an error on the span. + * @param throwable error to set + * @return this + */ + Builder error(Throwable throwable); + + /** + * Sets the kind on the span. + * @param spanKind kind of the span + * @return this + */ + Builder kind(Span.Kind spanKind); + + /** + * Sets the remote service name for the span. + * @param remoteServiceName remote service name + * @return this + */ + Builder remoteServiceName(String remoteServiceName); + + /** + * Sets the remote URL for the span. + * @param remoteUrl remote service name + * @return this + */ + default Builder remoteUrl(String remoteUrl) { + return this; + } + + /** + * Builds and starts the span. + * @return started span + */ + Span start(); + + } + +} diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Taggable.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Taggable.java new file mode 100644 index 000000000..79c92512a --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Taggable.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth; + +/** + * + * Describes the behaviour of an object that can be tagged. + * + * @author Marcin Grzejszczak + * @since 3.0.3 + */ +public interface Taggable { + + /** + * Sets a tag. + * @param key tag key + * @param value tag value + * @return this, for chaining + */ + Taggable tag(String key, String value); + +} diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java new file mode 100644 index 000000000..d4ad3b7ff --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java @@ -0,0 +1,166 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.docs; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; + +/** + * {@link Span} that performs additional assertions such as allowed name, tag, event + * verification and upon reporting, whether the span had been started in the first place. + * + * You need to turn on assertions via system properties or environment variables to start + * breaking your tests or production code. Check {@link DocumentedSpanAssertions} for more + * information. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface AssertingSpan extends Span { + + /** + * @return a {@link DocumentedSpan} with span configuration + */ + DocumentedSpan getDocumentedSpan(); + + /** + * @return wrapped {@link Span} + */ + Span getDelegate(); + + /** + * @return {@code true} when this span was started + */ + default boolean isStarted() { + return false; + } + + @Override + default AssertingSpan tag(String key, String value) { + DocumentedSpanAssertions.assertThatKeyIsValid(key, getDocumentedSpan()); + getDelegate().tag(key, value); + return this; + } + + /** + * Tags a span via {@link TagKey}. + * @param key tag key + * @param value tag value + * @return this for chaining + */ + default AssertingSpan tag(TagKey key, String value) { + DocumentedSpanAssertions.assertThatKeyIsValid(key, getDocumentedSpan()); + getDelegate().tag(key.getKey(), value); + return this; + } + + @Override + default AssertingSpan event(String value) { + DocumentedSpanAssertions.assertThatEventIsValid(value, getDocumentedSpan()); + getDelegate().event(value); + return this; + } + + /** + * Annotates with an event via {@link EventValue}. + * @param value event value + * @return this for chaining + */ + default AssertingSpan event(EventValue value) { + DocumentedSpanAssertions.assertThatEventIsValid(value, getDocumentedSpan()); + getDelegate().event(value.getValue()); + return this; + } + + @Override + default AssertingSpan name(String name) { + DocumentedSpanAssertions.assertThatNameIsValid(name, getDocumentedSpan()); + getDelegate().name(name); + return this; + } + + @Override + default boolean isNoop() { + return getDelegate().isNoop(); + } + + @Override + default TraceContext context() { + return getDelegate().context(); + } + + @Override + default AssertingSpan start() { + getDelegate().start(); + return this; + } + + @Override + default AssertingSpan error(Throwable throwable) { + getDelegate().error(throwable); + return this; + } + + @Override + default void end() { + DocumentedSpanAssertions.assertThatSpanStartedBeforeEnd(this); + getDelegate().end(); + } + + @Override + default void abandon() { + getDelegate().abandon(); + } + + @Override + default AssertingSpan remoteServiceName(String remoteServiceName) { + getDelegate().remoteServiceName(remoteServiceName); + return this; + } + + /** + * @param documentedSpan span configuration + * @param span span to wrap in assertions + * @return asserting span + */ + static AssertingSpan of(DocumentedSpan documentedSpan, Span span) { + if (span == null) { + return null; + } + else if (span instanceof AssertingSpan) { + return (AssertingSpan) span; + } + return new ImmutableAssertingSpan(documentedSpan, span); + } + + /** + * Returns the underlying delegate. Used when casting is necessary. + * @param span span to check for wrapping + * @param type extending a span + * @return unwrapped object + */ + static T unwrap(Span span) { + if (span == null) { + return null; + } + else if (span instanceof AssertingSpan) { + return (T) ((AssertingSpan) span).getDelegate(); + } + return (T) span; + } + +} diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java new file mode 100644 index 000000000..3ba8e793c --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java @@ -0,0 +1,151 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.docs; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; + +/** + * A {@link Span.Builder} that can perform assertions on itself. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface AssertingSpanBuilder extends Span.Builder { + + /** + * @return a {@link DocumentedSpan} with span configuration + */ + DocumentedSpan getDocumentedSpan(); + + /** + * @return wrapped {@link Span.Builder} + */ + Span.Builder getDelegate(); + + @Override + default AssertingSpanBuilder tag(String key, String value) { + DocumentedSpanAssertions.assertThatKeyIsValid(key, getDocumentedSpan()); + getDelegate().tag(key, value); + return this; + } + + /** + * Sets a tag on a span. + * @param key tag key + * @param value tag + * @return this, for chaining + */ + default AssertingSpanBuilder tag(TagKey key, String value) { + DocumentedSpanAssertions.assertThatKeyIsValid(key, getDocumentedSpan()); + getDelegate().tag(key.getKey(), value); + return this; + } + + @Override + default AssertingSpanBuilder event(String value) { + DocumentedSpanAssertions.assertThatEventIsValid(value, getDocumentedSpan()); + getDelegate().event(value); + return this; + } + + /** + * Sets an event on a span. + * @param value event + * @return this, for chaining + */ + default AssertingSpanBuilder event(EventValue value) { + DocumentedSpanAssertions.assertThatEventIsValid(value, getDocumentedSpan()); + getDelegate().event(value.getValue()); + return this; + } + + @Override + default AssertingSpanBuilder name(String name) { + DocumentedSpanAssertions.assertThatNameIsValid(name, getDocumentedSpan()); + getDelegate().name(name); + return this; + } + + @Override + default AssertingSpanBuilder error(Throwable throwable) { + getDelegate().error(throwable); + return this; + } + + @Override + default AssertingSpanBuilder remoteServiceName(String remoteServiceName) { + getDelegate().remoteServiceName(remoteServiceName); + return this; + } + + @Override + default AssertingSpanBuilder setParent(TraceContext context) { + getDelegate().setParent(context); + return this; + } + + @Override + default AssertingSpanBuilder setNoParent() { + getDelegate().setNoParent(); + return this; + } + + @Override + default AssertingSpanBuilder kind(Span.Kind spanKind) { + getDelegate().kind(spanKind); + return this; + } + + @Override + default AssertingSpan start() { + Span span = getDelegate().start(); + DocumentedSpan documentedSpan = getDocumentedSpan(); + return new AssertingSpan() { + @Override + public DocumentedSpan getDocumentedSpan() { + return documentedSpan; + } + + @Override + public Span getDelegate() { + return span; + } + + @Override + public boolean isStarted() { + return true; + } + }; + } + + /** + * @param documentedSpan span configuration + * @param builder builder to wrap in assertions + * @return asserting span builder + */ + static AssertingSpanBuilder of(DocumentedSpan documentedSpan, Span.Builder builder) { + if (builder == null) { + return null; + } + else if (builder instanceof AssertingSpanBuilder) { + return (AssertingSpanBuilder) builder; + } + return new ImmutableAssertingSpanBuilder(documentedSpan, builder); + } + +} diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanCustomizer.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanCustomizer.java new file mode 100644 index 000000000..d1267bcaf --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanCustomizer.java @@ -0,0 +1,111 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.docs; + +import org.springframework.cloud.sleuth.SpanCustomizer; + +/** + * A {@link SpanCustomizer} that can perform assertions on itself. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface AssertingSpanCustomizer extends SpanCustomizer { + + /** + * @return a {@link DocumentedSpan} with span configuration + */ + DocumentedSpan getDocumentedSpan(); + + /** + * @return wrapped {@link SpanCustomizer} + */ + SpanCustomizer getDelegate(); + + @Override + default AssertingSpanCustomizer tag(String key, String value) { + DocumentedSpanAssertions.assertThatKeyIsValid(key, getDocumentedSpan()); + getDelegate().tag(key, value); + return this; + } + + /** + * Sets a tag on a span. + * @param key tag key + * @param value tag + * @return this, for chaining + */ + default AssertingSpanCustomizer tag(TagKey key, String value) { + DocumentedSpanAssertions.assertThatKeyIsValid(key, getDocumentedSpan()); + getDelegate().tag(key.getKey(), value); + return this; + } + + @Override + default AssertingSpanCustomizer event(String value) { + DocumentedSpanAssertions.assertThatEventIsValid(value, getDocumentedSpan()); + getDelegate().event(value); + return this; + } + + /** + * Sets an event on a span. + * @param value event + * @return this, for chaining + */ + default AssertingSpanCustomizer event(EventValue value) { + DocumentedSpanAssertions.assertThatEventIsValid(value, getDocumentedSpan()); + getDelegate().event(value.getValue()); + return this; + } + + @Override + default AssertingSpanCustomizer name(String name) { + DocumentedSpanAssertions.assertThatNameIsValid(name, getDocumentedSpan()); + getDelegate().name(name); + return this; + } + + /** + * @param documentedSpan span configuration + * @param span span to wrap in assertions + * @return asserting span customizer + */ + static AssertingSpanCustomizer of(DocumentedSpan documentedSpan, SpanCustomizer span) { + if (span instanceof AssertingSpanCustomizer) { + return (AssertingSpanCustomizer) span; + } + return new ImmutableAssertingSpanCustomizer(documentedSpan, span); + } + + /** + * Returns the underlying delegate. Used when casting is necessary. + * @param span span to check for wrapping + * @param type extending a span + * @return unwrapped object + */ + static T unwrap(SpanCustomizer span) { + if (span == null) { + return null; + } + else if (span instanceof AssertingSpanCustomizer) { + return (T) ((AssertingSpanCustomizer) span).getDelegate(); + } + return (T) span; + } + +} diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/DocumentedSpan.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/DocumentedSpan.java new file mode 100644 index 000000000..463b1b377 --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/DocumentedSpan.java @@ -0,0 +1,116 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.docs; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanCustomizer; + +/** + * In order to describe your spans via e.g. enums instead of Strings you can use this + * interface that returns all the characteristics of a span. In Spring Cloud Sleuth we + * analyze the sources and reuse this information to build a table of known spans, their + * names, tags and events. + * + * We can generate documentation for all created spans but certain requirements need to be + * met + * + * - spans are grouped within an enum - the enum implements the {@link DocumentedSpan} + * interface - if the span contains {@link TagKey} or {@link EventValue} then those need + * to be declared as nested enums - the {@link DocumentedSpan#getTagKeys()} and + * {@link DocumentedSpan#getEvents()} need to call the nested enum's {@code values()} + * method to retrieve the array of allowed keys / events + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface DocumentedSpan { + + /** + * @return span name + */ + String getName(); + + /** + * @return allowed tag keys + */ + default TagKey[] getTagKeys() { + return new TagKey[0]; + } + + /** + * @return allowed events + */ + default EventValue[] getEvents() { + return new EventValue[0]; + } + + /** + * Returns required prefix to be there for events and tags. Example {@code foo.} would + * require the tags and events to have a {code foo} prefix like this for tags: + * {@code foo.bar=true} and {@code foo.started} for events. + * @return required prefix + */ + default String prefix() { + return ""; + } + + /** + * Asserts on tags, names and allowed events. + * @param span to wrap + * @return wrapped span + */ + default AssertingSpan wrap(Span span) { + if (span == null) { + return null; + } + else if (span instanceof AssertingSpan) { + return (AssertingSpan) span; + } + return AssertingSpan.of(this, span); + } + + /** + * Asserts on tags, names and allowed events. + * @param span to wrap + * @return wrapped span + */ + default AssertingSpanCustomizer wrap(SpanCustomizer span) { + if (span == null) { + return null; + } + else if (span instanceof AssertingSpanCustomizer) { + return (AssertingSpanCustomizer) span; + } + return AssertingSpanCustomizer.of(this, span); + } + + /** + * Asserts on tags, names and allowed events. + * @param span builder to wrap + * @return wrapped span + */ + default AssertingSpanBuilder wrap(Span.Builder span) { + if (span == null) { + return null; + } + else if (span instanceof AssertingSpanBuilder) { + return (AssertingSpanBuilder) span; + } + return AssertingSpanBuilder.of(this, span); + } + +} diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/DocumentedSpanAssertions.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/DocumentedSpanAssertions.java new file mode 100644 index 000000000..1e87eaa4a --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/DocumentedSpanAssertions.java @@ -0,0 +1,149 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.docs; + +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.springframework.util.StringUtils; + +/** + * In order to turn on the assertions you need to either turn on the + * {@code spring.cloud.sleuth.assertions.enabled} system property or + * {@code SPRING_CLOUD_SLEUTH_ASSERTIONS_ENABLED} environment variable. + */ +final class DocumentedSpanAssertions { + + static boolean SLEUTH_SPAN_ASSERTIONS_ON = Boolean.parseBoolean(System.getProperty( + "spring.cloud.sleuth.assertions.enabled", System.getenv("SPRING_CLOUD_SLEUTH_ASSERTIONS_ENABLED") != null + ? System.getenv("SPRING_CLOUD_SLEUTH_ASSERTIONS_ENABLED") : "false")); + + private static final Map PATTERN_CACHE = new ConcurrentHashMap<>(); + + private static final Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[{}()\\[\\].+*?^$\\\\|]"); + + private DocumentedSpanAssertions() { + throw new IllegalStateException("Can't instantiate utility class"); + } + + static void assertThatKeyIsValid(String key, DocumentedSpan documentedSpan) { + if (SLEUTH_SPAN_ASSERTIONS_ON) { + TagKey[] allowedKeys = documentedSpan.getTagKeys(); + if (allowedKeys.length == 0) { + return; + } + boolean validTagKey = Arrays.stream(allowedKeys) + .anyMatch(tagKey -> patternOrValueMatches(key, tagKey.getKey()) + && hasRequiredPrefix(key, documentedSpan.prefix())); + if (!validTagKey) { + throw new AssertionError("The key [" + key + "] is invalid. You can use only one matching " + + Arrays.stream(allowedKeys).map(TagKey::getKey).collect(Collectors.toList()) + + prefixWarningIfPresent(documentedSpan)); + } + } + } + + private static String prefixWarningIfPresent(DocumentedSpan documentedSpan) { + return StringUtils.hasText(documentedSpan.prefix()) + ? ". Also it has start with [" + documentedSpan.prefix() + "] prefix" : ""; + } + + static void assertThatKeyIsValid(TagKey key, DocumentedSpan documentedSpan) { + if (SLEUTH_SPAN_ASSERTIONS_ON) { + TagKey[] allowedKeys = documentedSpan.getTagKeys(); + if (allowedKeys.length == 0) { + return; + } + if (Arrays.stream(allowedKeys).noneMatch(tagKey -> patternOrValueMatches(key.getKey(), tagKey.getKey()) + && hasRequiredPrefix(key.getKey(), documentedSpan.prefix()))) { + throw new AssertionError("The key [" + key.getKey() + "] is invalid. You can use only one matching " + + Arrays.stream(allowedKeys).map(TagKey::getKey).collect(Collectors.toList()) + + prefixWarningIfPresent(documentedSpan)); + } + } + } + + static void assertThatNameIsValid(String name, DocumentedSpan documentedSpan) { + String allowedName = documentedSpan.getName(); + if (SLEUTH_SPAN_ASSERTIONS_ON && !patternOrValueMatches(name, allowedName)) { + throw new AssertionError( + "The name [" + name + "] is invalid. You can use only one matching [" + allowedName + "]"); + } + } + + static void assertThatEventIsValid(String eventValue, DocumentedSpan documentedSpan) { + if (SLEUTH_SPAN_ASSERTIONS_ON) { + EventValue[] allowed = documentedSpan.getEvents(); + if (allowed.length == 0) { + return; + } + boolean valid = Arrays.stream(allowed).anyMatch(value -> patternOrValueMatches(eventValue, value.getValue()) + && hasRequiredPrefix(eventValue, documentedSpan.prefix())); + if (!valid) { + throw new AssertionError("The event [" + eventValue + "] is invalid. You can use only one matching " + + Arrays.stream(allowed).map(EventValue::getValue).collect(Collectors.toList()) + + prefixWarningIfPresent(documentedSpan)); + } + } + } + + static void assertThatEventIsValid(EventValue eventValue, DocumentedSpan documentedSpan) { + if (SLEUTH_SPAN_ASSERTIONS_ON) { + EventValue[] allowed = documentedSpan.getEvents(); + if (allowed.length == 0) { + return; + } + if (Arrays.stream(allowed).noneMatch(value -> patternOrValueMatches(eventValue.getValue(), value.getValue()) + && hasRequiredPrefix(eventValue.getValue(), documentedSpan.prefix()))) { + throw new AssertionError( + "The event [" + eventValue.getValue() + "] is invalid. You can use only one matching " + + Arrays.stream(allowed).map(EventValue::getValue).collect(Collectors.toList()) + + prefixWarningIfPresent(documentedSpan)); + } + } + } + + static void assertThatSpanStartedBeforeEnd(AssertingSpan span) { + if (SLEUTH_SPAN_ASSERTIONS_ON && !span.isStarted()) { + throw new AssertionError("The span was not started, however you're trying to end it"); + } + } + + private static boolean patternOrValueMatches(String pickedValue, String allowedValue) { + if (allowedValue.contains("%s")) { + String stringPattern = escapeSpecialRegexWithSingleEscape(allowedValue).replaceAll("%s", ".*?"); + Pattern pattern = PATTERN_CACHE.computeIfAbsent(stringPattern, Pattern::compile); + return pattern.matcher(pickedValue).matches(); + } + return allowedValue.equals(pickedValue); + } + + private static boolean hasRequiredPrefix(String value, String prefix) { + if (StringUtils.hasText(prefix)) { + return value.startsWith(prefix); + } + return true; + } + + private static String escapeSpecialRegexWithSingleEscape(String str) { + return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0"); + } + +} diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/EventValue.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/EventValue.java new file mode 100644 index 000000000..fd5ea542a --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/EventValue.java @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.docs; + +/** + * Event value representing a notable event in time. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface EventValue { + + /** + * @return event value + */ + String getValue(); + +} diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/ImmutableAssertingSpan.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/ImmutableAssertingSpan.java new file mode 100644 index 000000000..2de29248e --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/ImmutableAssertingSpan.java @@ -0,0 +1,83 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.docs; + +import java.util.Objects; + +import org.springframework.cloud.sleuth.Span; + +import static java.util.Objects.requireNonNull; + +class ImmutableAssertingSpan implements AssertingSpan { + + private final DocumentedSpan documentedSpan; + + private final Span delegate; + + boolean isStarted; + + ImmutableAssertingSpan(DocumentedSpan documentedSpan, Span delegate) { + requireNonNull(documentedSpan); + requireNonNull(delegate); + this.documentedSpan = documentedSpan; + this.delegate = delegate; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ImmutableAssertingSpan that = (ImmutableAssertingSpan) o; + return Objects.equals(documentedSpan, that.documentedSpan) && Objects.equals(delegate, that.delegate); + } + + @Override + public String toString() { + return this.delegate.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(documentedSpan, delegate); + } + + @Override + public DocumentedSpan getDocumentedSpan() { + return this.documentedSpan; + } + + @Override + public Span getDelegate() { + return this.delegate; + } + + @Override + public AssertingSpan start() { + this.isStarted = true; + return AssertingSpan.super.start(); + } + + @Override + public boolean isStarted() { + return this.isStarted; + } + +} diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/ImmutableAssertingSpanBuilder.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/ImmutableAssertingSpanBuilder.java new file mode 100644 index 000000000..61a6eb6bb --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/ImmutableAssertingSpanBuilder.java @@ -0,0 +1,70 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.docs; + +import java.util.Objects; + +import org.springframework.cloud.sleuth.Span; + +import static java.util.Objects.requireNonNull; + +class ImmutableAssertingSpanBuilder implements AssertingSpanBuilder { + + private final DocumentedSpan documentedSpan; + + private final Span.Builder delegate; + + ImmutableAssertingSpanBuilder(DocumentedSpan documentedSpan, Span.Builder delegate) { + requireNonNull(documentedSpan); + requireNonNull(delegate); + this.documentedSpan = documentedSpan; + this.delegate = delegate; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ImmutableAssertingSpanBuilder that = (ImmutableAssertingSpanBuilder) o; + return Objects.equals(documentedSpan, that.documentedSpan) && Objects.equals(delegate, that.delegate); + } + + @Override + public String toString() { + return this.delegate.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(documentedSpan, delegate); + } + + @Override + public DocumentedSpan getDocumentedSpan() { + return this.documentedSpan; + } + + @Override + public Span.Builder getDelegate() { + return this.delegate; + } + +} diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/ImmutableAssertingSpanCustomizer.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/ImmutableAssertingSpanCustomizer.java new file mode 100644 index 000000000..257cf0407 --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/ImmutableAssertingSpanCustomizer.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.docs; + +import java.util.Objects; + +import org.springframework.cloud.sleuth.SpanCustomizer; + +import static java.util.Objects.requireNonNull; + +class ImmutableAssertingSpanCustomizer implements AssertingSpanCustomizer { + + private final DocumentedSpan documentedSpan; + + private final SpanCustomizer delegate; + + ImmutableAssertingSpanCustomizer(DocumentedSpan documentedSpan, SpanCustomizer delegate) { + requireNonNull(documentedSpan); + requireNonNull(delegate); + this.documentedSpan = documentedSpan; + this.delegate = delegate; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ImmutableAssertingSpanCustomizer that = (ImmutableAssertingSpanCustomizer) o; + return Objects.equals(documentedSpan, that.documentedSpan) && Objects.equals(delegate, that.delegate); + } + + @Override + public int hashCode() { + return Objects.hash(documentedSpan, delegate); + } + + @Override + public DocumentedSpan getDocumentedSpan() { + return this.documentedSpan; + } + + @Override + public SpanCustomizer getDelegate() { + return this.delegate; + } + +} diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/TagKey.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/TagKey.java new file mode 100644 index 000000000..b3916f384 --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/TagKey.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.docs; + +import java.util.Arrays; + +/** + * Represents a tag key. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface TagKey { + + /** + * @return tag key + */ + String getKey(); + + /** + * Merges arrays of tags. + * @param tags array of tags + * @return a merged array of tags + */ + static TagKey[] merge(TagKey[]... tags) { + return Arrays.stream(tags).flatMap(Arrays::stream).toArray(TagKey[]::new); + } + +} diff --git a/spring-cloud-sleuth-api/src/test/java/org/springframework/cloud/sleuth/docs/DocumentedSpanAssertionsTests.java b/spring-cloud-sleuth-api/src/test/java/org/springframework/cloud/sleuth/docs/DocumentedSpanAssertionsTests.java new file mode 100644 index 000000000..2b8d16dcf --- /dev/null +++ b/spring-cloud-sleuth-api/src/test/java/org/springframework/cloud/sleuth/docs/DocumentedSpanAssertionsTests.java @@ -0,0 +1,307 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.docs; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; + +import org.springframework.cloud.sleuth.Span; + +import static org.assertj.core.api.BDDAssertions.thenThrownBy; +import static org.springframework.cloud.sleuth.docs.DocumentedSpanAssertions.assertThatEventIsValid; +import static org.springframework.cloud.sleuth.docs.DocumentedSpanAssertions.assertThatKeyIsValid; +import static org.springframework.cloud.sleuth.docs.DocumentedSpanAssertions.assertThatNameIsValid; +import static org.springframework.cloud.sleuth.docs.DocumentedSpanAssertions.assertThatSpanStartedBeforeEnd; +import static org.springframework.cloud.sleuth.docs.DocumentedSpanAssertionsTests.MyEventsWithNotMatchingPrefix.A_BAR_EVENT; +import static org.springframework.cloud.sleuth.docs.DocumentedSpanAssertionsTests.MySpan.SPAN_WITH_DYNAMIC_ENTRIES; +import static org.springframework.cloud.sleuth.docs.DocumentedSpanAssertionsTests.MySpan.SPAN_WITH_EMPTY_TAGS_AND_EVENTS; +import static org.springframework.cloud.sleuth.docs.DocumentedSpanAssertionsTests.MySpan.SPAN_WITH_NOT_MATCHING_PREFIX; +import static org.springframework.cloud.sleuth.docs.DocumentedSpanAssertionsTests.MySpan.SPAN_WITH_PREFIX; +import static org.springframework.cloud.sleuth.docs.DocumentedSpanAssertionsTests.MyTags.A_FOO_TAG; + +class DocumentedSpanAssertionsTests { + + @BeforeEach + void setup() { + DocumentedSpanAssertions.SLEUTH_SPAN_ASSERTIONS_ON = true; + } + + @Test + void should_do_nothing_when_system_property_not_turned_on() { + DocumentedSpanAssertions.SLEUTH_SPAN_ASSERTIONS_ON = false; + + assertThatKeyIsValid("unknown_key", SPAN_WITH_NOT_MATCHING_PREFIX); + assertThatKeyIsValid(A_FOO_TAG, SPAN_WITH_PREFIX); + assertThatEventIsValid("unknown_event", SPAN_WITH_PREFIX); + assertThatEventIsValid(A_BAR_EVENT, SPAN_WITH_PREFIX); + assertThatNameIsValid("unknown_name", SPAN_WITH_NOT_MATCHING_PREFIX); + assertThatSpanStartedBeforeEnd( + new ImmutableAssertingSpan(SPAN_WITH_NOT_MATCHING_PREFIX, BDDMockito.mock(Span.class))); + } + + @Test + void should_do_nothing_when_tags_or_events_are_empty() { + assertThatKeyIsValid("unknown_key", SPAN_WITH_EMPTY_TAGS_AND_EVENTS); + assertThatKeyIsValid(A_FOO_TAG, SPAN_WITH_EMPTY_TAGS_AND_EVENTS); + assertThatEventIsValid("unknown_event", SPAN_WITH_EMPTY_TAGS_AND_EVENTS); + assertThatEventIsValid(A_BAR_EVENT, SPAN_WITH_EMPTY_TAGS_AND_EVENTS); + } + + @Test + void should_not_fail_when_keys_and_values_are_properly_prefixed() { + assertThatKeyIsValid("some.key", SPAN_WITH_DYNAMIC_ENTRIES); + assertThatKeyIsValid(String.format(MyDynamicTags.A_DYNAMIC_TAG.getKey(), "some"), SPAN_WITH_DYNAMIC_ENTRIES); + assertThatEventIsValid("some.value", SPAN_WITH_DYNAMIC_ENTRIES); + assertThatEventIsValid(String.format(MyDynamicEvents.A_DYNAMIC_EVENT.getValue(), "some"), + SPAN_WITH_DYNAMIC_ENTRIES); + } + + @Test + void should_not_fail_when_span_was_started_and_then_ended() { + assertThatSpanStartedBeforeEnd( + new ImmutableAssertingSpan(SPAN_WITH_NOT_MATCHING_PREFIX, BDDMockito.mock(Span.class)).start()); + } + + @Test + void should_fail_when_assertion_is_on_and_a_key_is_unknown() { + thenThrownBy(() -> assertThatKeyIsValid("unknown_key", SPAN_WITH_NOT_MATCHING_PREFIX)) + .hasMessageContaining("The key [unknown_key] is invalid"); + thenThrownBy(() -> assertThatKeyIsValid(A_FOO_TAG, SPAN_WITH_NOT_MATCHING_PREFIX)) + .hasMessageContaining("The key [foo.key] is invalid"); + } + + @Test + void should_fail_when_assertion_is_on_and_an_event_is_unknown() { + thenThrownBy(() -> assertThatEventIsValid("unknown_event", SPAN_WITH_PREFIX)) + .hasMessageContaining("The event [unknown_event] is invalid"); + thenThrownBy(() -> assertThatEventIsValid(A_BAR_EVENT, SPAN_WITH_PREFIX)) + .hasMessageContaining("The event [bar.value] is invalid"); + } + + @Test + void should_fail_when_assertion_is_on_and_a_key_is_known_but_wrongly_prefixed() { + thenThrownBy(() -> assertThatKeyIsValid("bar.key", SPAN_WITH_NOT_MATCHING_PREFIX)) + .hasMessageContaining("Also it has start with [foo.] prefix"); + thenThrownBy(() -> assertThatKeyIsValid(MyTagsWithNotMatchingPrefix.A_BAR_TAG, SPAN_WITH_NOT_MATCHING_PREFIX)) + .hasMessageContaining("Also it has start with [foo.] prefix"); + } + + @Test + void should_fail_when_assertion_is_on_and_an_event_is_known_but_wrongly_prefixed() { + thenThrownBy(() -> assertThatEventIsValid("bar.value", SPAN_WITH_NOT_MATCHING_PREFIX)) + .hasMessageContaining("Also it has start with [foo.] prefix"); + thenThrownBy(() -> assertThatEventIsValid(A_BAR_EVENT, SPAN_WITH_NOT_MATCHING_PREFIX)) + .hasMessageContaining("Also it has start with [foo.] prefix"); + } + + @Test + void should_fail_when_assertion_is_on_and_a_key_is_known_but_dynamic_key_is_not_matched() { + thenThrownBy(() -> assertThatKeyIsValid("notmatching", SPAN_WITH_DYNAMIC_ENTRIES)) + .hasMessageContaining("The key [notmatching] is invalid. You can use only one matching [%s.key]"); + thenThrownBy(() -> assertThatKeyIsValid(MySimpleTag.A_SIMPLE_TAG, SPAN_WITH_DYNAMIC_ENTRIES)) + .hasMessageContaining("The key [simple] is invalid. You can use only one matching [%s.key]"); + } + + @Test + void should_fail_when_assertion_is_on_and_an_event_is_known_but_dynamic_value_is_not_matched() { + thenThrownBy(() -> assertThatEventIsValid("notmatching", SPAN_WITH_DYNAMIC_ENTRIES)) + .hasMessageContaining("The event [notmatching] is invalid. You can use only one matching [%s.value]"); + thenThrownBy(() -> assertThatEventIsValid(MySimpleEvent.A_SIMPLE_EVENT, SPAN_WITH_DYNAMIC_ENTRIES)) + .hasMessageContaining("The event [simple] is invalid. You can use only one matching [%s.value]"); + } + + @Test + void should_fail_when_assertion_is_on_and_name_is_invalid() { + thenThrownBy(() -> assertThatNameIsValid("unknown_name", SPAN_WITH_NOT_MATCHING_PREFIX)) + .hasMessageContaining("The name [unknown_name] is invalid"); + } + + @Test + void should_fail_when_assertion_is_on_and_name_is_not_matching() { + thenThrownBy(() -> assertThatNameIsValid("unknown_name", SPAN_WITH_DYNAMIC_ENTRIES)).hasMessageContaining( + "The name [unknown_name] is invalid. You can use only one matching [%s somename]"); + } + + @Test + void should_fail_when_span_was_ended_but_not_started() { + thenThrownBy(() -> assertThatSpanStartedBeforeEnd( + new ImmutableAssertingSpan(SPAN_WITH_NOT_MATCHING_PREFIX, BDDMockito.mock(Span.class)))) + .hasMessageContaining("The span was not started"); + } + + enum MySpan implements DocumentedSpan { + + SPAN_WITH_PREFIX { + @Override + public String getName() { + return "foo"; + } + + @Override + public TagKey[] getTagKeys() { + return MyTags.values(); + } + + @Override + public EventValue[] getEvents() { + return MyEvents.values(); + } + + @Override + public String prefix() { + return "foo."; + } + }, + + SPAN_WITH_NOT_MATCHING_PREFIX { + @Override + public String getName() { + return "bar"; + } + + @Override + public TagKey[] getTagKeys() { + return MyTagsWithNotMatchingPrefix.values(); + } + + @Override + public EventValue[] getEvents() { + return MyEventsWithNotMatchingPrefix.values(); + } + + @Override + public String prefix() { + return "foo."; + } + }, + + SPAN_WITH_EMPTY_TAGS_AND_EVENTS { + @Override + public String getName() { + return "baz"; + } + }, + + SPAN_WITH_DYNAMIC_ENTRIES { + @Override + public String getName() { + return "%s somename"; + } + + @Override + public TagKey[] getTagKeys() { + return MyDynamicTags.values(); + } + + @Override + public EventValue[] getEvents() { + return MyDynamicEvents.values(); + } + } + + } + + enum MyTags implements TagKey { + + A_FOO_TAG { + @Override + public String getKey() { + return "foo.key"; + } + } + + } + + enum MyEvents implements EventValue { + + A_FOO_EVENT { + @Override + public String getValue() { + return "foo.value"; + } + } + + } + + enum MyTagsWithNotMatchingPrefix implements TagKey { + + A_BAR_TAG { + @Override + public String getKey() { + return "bar.key"; + } + } + + } + + enum MyEventsWithNotMatchingPrefix implements EventValue { + + A_BAR_EVENT { + @Override + public String getValue() { + return "bar.value"; + } + } + + } + + enum MySimpleTag implements TagKey { + + A_SIMPLE_TAG { + @Override + public String getKey() { + return "simple"; + } + } + + } + + enum MySimpleEvent implements EventValue { + + A_SIMPLE_EVENT { + @Override + public String getValue() { + return "simple"; + } + } + + } + + enum MyDynamicTags implements TagKey { + + A_DYNAMIC_TAG { + @Override + public String getKey() { + return "%s.key"; + } + } + + } + + enum MyDynamicEvents implements EventValue { + + A_DYNAMIC_EVENT { + @Override + public String getValue() { + return "%s.value"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveScopedSpan.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveScopedSpan.java index e760c4239..0168c6e04 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveScopedSpan.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveScopedSpan.java @@ -47,22 +47,26 @@ class BraveScopedSpan implements ScopedSpan { @Override public ScopedSpan name(String name) { - return new BraveScopedSpan(this.span.name(name)); + this.span.name(name); + return this; } @Override public ScopedSpan tag(String key, String value) { - return new BraveScopedSpan(this.span.tag(key, value)); + this.span.tag(key, value); + return this; } @Override public ScopedSpan event(String value) { - return new BraveScopedSpan(this.span.annotate(value)); + this.span.annotate(value); + return this; } @Override public ScopedSpan error(Throwable throwable) { - return new BraveScopedSpan(this.span.error(throwable)); + this.span.error(throwable); + return this; } @Override diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpan.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpan.java index abf330535..9643d31c6 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpan.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpan.java @@ -20,6 +20,7 @@ import java.util.Objects; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.docs.AssertingSpan; /** * Brave implementation of a {@link Span}. @@ -50,22 +51,26 @@ public class BraveSpan implements Span { @Override public Span start() { - return new BraveSpan(this.delegate.start()); + this.delegate.start(); + return this; } @Override public Span name(String name) { - return new BraveSpan(this.delegate.name(name)); + this.delegate.name(name); + return this; } @Override public Span event(String value) { - return new BraveSpan(this.delegate.annotate(value)); + this.delegate.annotate(value); + return this; } @Override public Span tag(String key, String value) { - return new BraveSpan(this.delegate.tag(key, value)); + this.delegate.tag(key, value); + return this; } @Override @@ -73,7 +78,7 @@ public class BraveSpan implements Span { String message = throwable.getMessage() == null ? throwable.getClass().getSimpleName() : throwable.getMessage(); this.delegate.tag("error", message); this.delegate.error(throwable); - return new BraveSpan(this.delegate); + return this; } @Override @@ -98,7 +103,7 @@ public class BraveSpan implements Span { } public static brave.Span toBrave(Span span) { - return ((BraveSpan) span).delegate; + return ((BraveSpan) AssertingSpan.unwrap(span)).delegate; } public static Span fromBrave(brave.Span span) { diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanBuilder.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanBuilder.java index e07c0fbdd..5246cd266 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanBuilder.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanBuilder.java @@ -1,144 +1,145 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.brave.bridge; - -import java.net.URI; - -import brave.Tracer; -import brave.propagation.TraceContextOrSamplingFlags; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.TraceContext; - -/** - * Brave implementation of a {@link Span.Builder}. - * - * @author Marcin Grzejszczak - * @since 3.0.0 - */ -class BraveSpanBuilder implements Span.Builder { - - private static final Log log = LogFactory.getLog(BraveSpanBuilder.class); - - brave.Span delegate; - - TraceContextOrSamplingFlags parentContext; - - private final Tracer tracer; - - private long startTimestamp; - - BraveSpanBuilder(Tracer tracer) { - this.tracer = tracer; - } - - BraveSpanBuilder(Tracer tracer, TraceContextOrSamplingFlags parentContext) { - this.tracer = tracer; - this.parentContext = parentContext; - } - - brave.Span span() { - if (this.delegate != null) { - return this.delegate; - } - else if (this.parentContext != null) { - this.delegate = this.tracer.nextSpan(this.parentContext); - } - else { - this.delegate = this.tracer.nextSpan(); - } - return this.delegate; - } - - @Override - public Span.Builder setParent(TraceContext context) { - this.parentContext = TraceContextOrSamplingFlags.create(BraveTraceContext.toBrave(context)); - return this; - } - - @Override - public Span.Builder setNoParent() { - return this; - } - - @Override - public Span.Builder name(String name) { - span().name(name); - return this; - } - - @Override - public Span.Builder event(String value) { - span().annotate(value); - return this; - } - - @Override - public Span.Builder tag(String key, String value) { - span().tag(key, value); - return this; - } - - @Override - public Span.Builder error(Throwable throwable) { - span().error(throwable); - return this; - } - - @Override - public Span.Builder kind(Span.Kind kind) { - span().kind(kind != null ? brave.Span.Kind.valueOf(kind.toString()) : null); - return this; - } - - @Override - public Span.Builder remoteServiceName(String remoteServiceName) { - span().remoteServiceName(remoteServiceName); - return this; - } - - @Override - public Span.Builder remoteUrl(String remoteUrl) { - try { - URI uri = URI.create(remoteUrl); - span().remoteIpAndPort(uri.getHost(), uri.getPort()); - } catch (Exception e) { - if (log.isDebugEnabled()) { - log.debug("Failed to parse url [" + remoteUrl + "]. Will not set the value", e); - } - } - return this; - } - - @Override - public Span start() { - if (this.startTimestamp > 0) { - span().start(this.startTimestamp); - } - else { - span().start(); - } - return BraveSpan.fromBrave(this.delegate); - } - - static Span.Builder toBuilder(Tracer tracer, TraceContextOrSamplingFlags context) { - return new BraveSpanBuilder(tracer, context); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.bridge; + +import java.net.URI; + +import brave.Tracer; +import brave.propagation.TraceContextOrSamplingFlags; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; + +/** + * Brave implementation of a {@link Span.Builder}. + * + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +class BraveSpanBuilder implements Span.Builder { + + private static final Log log = LogFactory.getLog(BraveSpanBuilder.class); + + brave.Span delegate; + + TraceContextOrSamplingFlags parentContext; + + private final Tracer tracer; + + private long startTimestamp; + + BraveSpanBuilder(Tracer tracer) { + this.tracer = tracer; + } + + BraveSpanBuilder(Tracer tracer, TraceContextOrSamplingFlags parentContext) { + this.tracer = tracer; + this.parentContext = parentContext; + } + + brave.Span span() { + if (this.delegate != null) { + return this.delegate; + } + else if (this.parentContext != null) { + this.delegate = this.tracer.nextSpan(this.parentContext); + } + else { + this.delegate = this.tracer.nextSpan(); + } + return this.delegate; + } + + @Override + public Span.Builder setParent(TraceContext context) { + this.parentContext = TraceContextOrSamplingFlags.create(BraveTraceContext.toBrave(context)); + return this; + } + + @Override + public Span.Builder setNoParent() { + return this; + } + + @Override + public Span.Builder name(String name) { + span().name(name); + return this; + } + + @Override + public Span.Builder event(String value) { + span().annotate(value); + return this; + } + + @Override + public Span.Builder tag(String key, String value) { + span().tag(key, value); + return this; + } + + @Override + public Span.Builder error(Throwable throwable) { + span().error(throwable); + return this; + } + + @Override + public Span.Builder kind(Span.Kind kind) { + span().kind(kind != null ? brave.Span.Kind.valueOf(kind.toString()) : null); + return this; + } + + @Override + public Span.Builder remoteServiceName(String remoteServiceName) { + span().remoteServiceName(remoteServiceName); + return this; + } + + @Override + public Span.Builder remoteUrl(String remoteUrl) { + try { + URI uri = URI.create(remoteUrl); + span().remoteIpAndPort(uri.getHost(), uri.getPort()); + } + catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug("Failed to parse url [" + remoteUrl + "]. Will not set the value", e); + } + } + return this; + } + + @Override + public Span start() { + if (this.startTimestamp > 0) { + span().start(this.startTimestamp); + } + else { + span().start(); + } + return BraveSpan.fromBrave(this.delegate); + } + + static Span.Builder toBuilder(Tracer tracer, TraceContextOrSamplingFlags context) { + return new BraveSpanBuilder(tracer, context); + } + +} diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanCustomizer.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanCustomizer.java index 59058cfe6..9b81c1460 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanCustomizer.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanCustomizer.java @@ -17,6 +17,7 @@ package org.springframework.cloud.sleuth.brave.bridge; import org.springframework.cloud.sleuth.SpanCustomizer; +import org.springframework.cloud.sleuth.docs.AssertingSpanCustomizer; /** * Brave implementation of a {@link SpanCustomizer}. @@ -48,7 +49,7 @@ public class BraveSpanCustomizer implements SpanCustomizer { } static brave.SpanCustomizer toBrave(SpanCustomizer spanCustomizer) { - return ((BraveSpanCustomizer) spanCustomizer).spanCustomizer; + return ((BraveSpanCustomizer) AssertingSpanCustomizer.unwrap(spanCustomizer)).spanCustomizer; } static SpanCustomizer fromBrave(brave.SpanCustomizer spanCustomizer) { diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTracer.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTracer.java index 258a5c53b..8be126ca9 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTracer.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveTracer.java @@ -26,6 +26,7 @@ import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.SpanCustomizer; import org.springframework.cloud.sleuth.TraceContext; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; /** * Brave implementation of a {@link Tracer}. @@ -58,7 +59,8 @@ public class BraveTracer implements Tracer { @Override public SpanInScope withSpan(Span span) { - return new BraveSpanInScope(tracer.withSpanInScope(span == null ? null : ((BraveSpan) span).delegate)); + return new BraveSpanInScope( + tracer.withSpanInScope(span == null ? null : ((BraveSpan) AssertingSpan.unwrap(span)).delegate)); } @Override diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/AbstractSleuthMethodInvocationProcessor.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/AbstractSleuthMethodInvocationProcessor.java index f845aafea..ea9ba24ce 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/AbstractSleuthMethodInvocationProcessor.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/AbstractSleuthMethodInvocationProcessor.java @@ -39,10 +39,6 @@ abstract class AbstractSleuthMethodInvocationProcessor implements SleuthMethodIn private static final Log logger = LogFactory.getLog(AbstractSleuthMethodInvocationProcessor.class); - private static final String CLASS_KEY = "class"; - - private static final String METHOD_KEY = "method"; - BeanFactory beanFactory; private NewSpanParser newSpanParser; @@ -81,8 +77,9 @@ abstract class AbstractSleuthMethodInvocationProcessor implements SleuthMethodIn } void addTags(MethodInvocation invocation, Span span) { - span.tag(CLASS_KEY, invocation.getThis().getClass().getSimpleName()); - span.tag(METHOD_KEY, invocation.getMethod().getName()); + SleuthAnnotationSpan.ANNOTATION_NEW_OR_CONTINUE_SPAN.wrap(span) + .tag(SleuthAnnotationSpan.Tags.CLASS, invocation.getThis().getClass().getSimpleName()) + .tag(SleuthAnnotationSpan.Tags.METHOD, invocation.getMethod().getName()); } void logEvent(Span span, String name) { @@ -92,7 +89,7 @@ abstract class AbstractSleuthMethodInvocationProcessor implements SleuthMethodIn + "the same class then the aspect will not be properly resolved"); return; } - span.event(name); + SleuthAnnotationSpan.ANNOTATION_NEW_OR_CONTINUE_SPAN.wrap(span).event(name); } String log(ContinueSpan continueSpan) { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/NonReactorSleuthMethodInvocationProcessor.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/NonReactorSleuthMethodInvocationProcessor.java index f46a0ca56..8829981d5 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/NonReactorSleuthMethodInvocationProcessor.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/NonReactorSleuthMethodInvocationProcessor.java @@ -44,7 +44,7 @@ public class NonReactorSleuthMethodInvocationProcessor extends AbstractSleuthMet // close it on completion boolean startNewSpan = newSpan != null || span == null; if (startNewSpan) { - span = tracer().nextSpan(); + span = SleuthAnnotationSpan.ANNOTATION_NEW_OR_CONTINUE_SPAN.wrap(tracer().nextSpan()); newSpanParser().parse(invocation, newSpan, span); span.start(); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/ReactorSleuthMethodInvocationProcessor.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/ReactorSleuthMethodInvocationProcessor.java index 23949b7df..5191b6a04 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/ReactorSleuthMethodInvocationProcessor.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/ReactorSleuthMethodInvocationProcessor.java @@ -132,7 +132,7 @@ public class ReactorSleuthMethodInvocationProcessor extends AbstractSleuthMethod if (this.span == null) { // If we aren't continuing a trace from this flow, use nextSpan so that it // can consider the "current span" (typically, backed by a thread-local) - span = tracer.nextSpan(); + span = SleuthAnnotationSpan.ANNOTATION_NEW_OR_CONTINUE_SPAN.wrap(tracer.nextSpan()); this.processor.newSpanParser().parse(this.invocation, this.newSpan, span); span.start(); } @@ -177,7 +177,7 @@ public class ReactorSleuthMethodInvocationProcessor extends AbstractSleuthMethod Span span; Tracer tracer = this.processor.tracer(); if (this.span == null) { - span = tracer.nextSpan(); + span = SleuthAnnotationSpan.ANNOTATION_NEW_OR_CONTINUE_SPAN.wrap(tracer.nextSpan()); this.processor.newSpanParser().parse(this.invocation, this.newSpan, span); span.start(); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/SleuthAnnotationSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/SleuthAnnotationSpan.java new file mode 100644 index 000000000..e795e70f0 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/annotation/SleuthAnnotationSpan.java @@ -0,0 +1,111 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.annotation; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.EventValue; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthAnnotationSpan implements DocumentedSpan { + + /** + * Span that wraps a @NewSpan or @ContinueSpan annotations. + */ + ANNOTATION_NEW_OR_CONTINUE_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + @Override + public EventValue[] getEvents() { + return Events.values(); + } + + }; + + /** + * Tags related to Sleuth annotations. + * + * @author Marcin Grzejszczak + * @since 3.0.3 + */ + enum Tags implements TagKey { + + /** + * Class name where a method got annotated with a Sleuth annotation. + */ + CLASS { + @Override + public String getKey() { + return "class"; + } + }, + + /** + * Method name that got annotated with Sleuth annotation. + */ + METHOD { + @Override + public String getKey() { + return "method"; + } + } + + } + + enum Events implements EventValue { + + /** + * Annotated before executing a method annotated with @ContinueSpan or @NewSpan. + */ + BEFORE { + @Override + public String getValue() { + return "%s.before"; + } + }, + + /** + * Annotated after executing a method annotated with @ContinueSpan or @NewSpan. + */ + AFTER { + @Override + public String getValue() { + return "%s.after"; + } + }, + + /** + * Annotated after throwing an exception from a method annotated + * with @ContinueSpan or @NewSpan. + */ + AFTER_FAILURE { + @Override + public String getValue() { + return "%s.afterFailure"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/SleuthAsyncSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/SleuthAsyncSpan.java new file mode 100644 index 000000000..d4499c49b --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/SleuthAsyncSpan.java @@ -0,0 +1,87 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.async; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthAsyncSpan implements DocumentedSpan { + + /** + * Span that wraps a @Async annotation. Either continues an existing one or creates a + * new one if there was no present one. + */ + ASYNC_ANNOTATION_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + }, + + /** + * Span created whenever a Runnable needs to be instrumented. + */ + ASYNC_RUNNABLE_SPAN { + @Override + public String getName() { + return "%s"; + } + + }, + + /** + * Span created whenever a Callable needs to be instrumented. + */ + ASYNC_CALLABLE_SPAN { + @Override + public String getName() { + return "%s"; + } + + }; + + enum Tags implements TagKey { + + /** + * Class name where a method got annotated with @Async. + */ + CLASS { + @Override + public String getKey() { + return "class"; + } + }, + + /** + * Method name that got annotated with @Async. + */ + METHOD { + @Override + public String getKey() { + return "method"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java index 6be996c8d..a00057d04 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceAsyncAspect.java @@ -26,6 +26,7 @@ import org.aspectj.lang.reflect.MethodSignature; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.SpanNamer; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; import org.springframework.cloud.sleuth.internal.SpanNameUtil; import org.springframework.util.ReflectionUtils; @@ -40,10 +41,6 @@ import org.springframework.util.ReflectionUtils; @Aspect public class TraceAsyncAspect { - private static final String CLASS_KEY = "class"; - - private static final String METHOD_KEY = "method"; - private final Tracer tracer; private final SpanNamer spanNamer; @@ -60,15 +57,14 @@ public class TraceAsyncAspect { if (span == null) { span = this.tracer.nextSpan(); } - span = span.name(spanName); - try (Tracer.SpanInScope ws = this.tracer.withSpan(span.start())) { - // TODO: Make this less generic - span.tag(CLASS_KEY, pjp.getTarget().getClass().getSimpleName()); - span.tag(METHOD_KEY, pjp.getSignature().getName()); + AssertingSpan assertingSpan = SleuthAsyncSpan.ASYNC_ANNOTATION_SPAN.wrap(span).name(spanName); + try (Tracer.SpanInScope ws = this.tracer.withSpan(assertingSpan.start())) { + assertingSpan.tag(SleuthAsyncSpan.Tags.CLASS, pjp.getTarget().getClass().getSimpleName()) + .tag(SleuthAsyncSpan.Tags.METHOD, pjp.getSignature().getName()); return pjp.proceed(); } finally { - span.end(); + assertingSpan.end(); } } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceCallable.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceCallable.java index 856968ed6..2aeb420f3 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceCallable.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceCallable.java @@ -61,7 +61,8 @@ public class TraceCallable implements Callable { @Override public V call() throws Exception { - Span childSpan = this.tracer.nextSpan(this.parent).name(this.spanName); + Span childSpan = SleuthAsyncSpan.ASYNC_CALLABLE_SPAN.wrap(this.tracer.nextSpan(this.parent)) + .name(this.spanName); try (Tracer.SpanInScope ws = this.tracer.withSpan(childSpan.start())) { return this.delegate.call(); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceRunnable.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceRunnable.java index bc093f6de..de95a19e2 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceRunnable.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/async/TraceRunnable.java @@ -58,7 +58,8 @@ public class TraceRunnable implements Runnable { @Override public void run() { - Span childSpan = this.tracer.nextSpan(this.parent).name(this.spanName); + Span childSpan = SleuthAsyncSpan.ASYNC_RUNNABLE_SPAN.wrap(this.tracer.nextSpan(this.parent)) + .name(this.spanName); try (Tracer.SpanInScope ws = this.tracer.withSpan(childSpan.start())) { this.delegate.run(); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/SleuthBatchSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/SleuthBatchSpan.java new file mode 100644 index 000000000..8bd0512e6 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/SleuthBatchSpan.java @@ -0,0 +1,134 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.batch; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthBatchSpan implements DocumentedSpan { + + /** + * Span created around a Job execution. + */ + BATCH_JOB_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return JobTags.values(); + } + + }, + + /** + * Span created around a Job execution. + */ + BATCH_STEP_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return StepTags.values(); + } + + }; + + enum JobTags implements TagKey { + + /** + * Name of the Spring Batch job. + */ + JOB_NAME { + @Override + public String getKey() { + return "batch.job.name"; + } + }, + + /** + * ID of the Spring Batch job instance. + */ + JOB_INSTANCE_ID { + @Override + public String getKey() { + return "batch.job.instanceId"; + } + }, + + /** + * ID of the Spring Batch execution. + */ + JOB_EXECUTION_ID { + @Override + public String getKey() { + return "batch.job.executionId"; + } + }, + + } + + enum StepTags implements TagKey { + + /** + * Name of the Spring Batch job. + */ + STEP_NAME { + @Override + public String getKey() { + return "batch.step.name"; + } + }, + + /** + * ID of the Spring Batch execution. + */ + STEP_EXECUTION_ID { + @Override + public String getKey() { + return "batch.step.executionId"; + } + }, + + /** + * Type of the Spring Batch job. + */ + STEP_TYPE { + @Override + public String getKey() { + return "batch.step.type"; + } + }, + + /** + * ID of the Spring Batch execution. + */ + JOB_EXECUTION_ID { + @Override + public String getKey() { + return "batch.job.executionId"; + } + }, + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceJobExecutionListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceJobExecutionListener.java index c53edc5a9..b461403cb 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceJobExecutionListener.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceJobExecutionListener.java @@ -26,12 +26,13 @@ import org.springframework.batch.core.JobExecutionListener; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.SpanAndScope; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; class TraceJobExecutionListener implements JobExecutionListener { private final Tracer tracer; - private static Map SPANS = new ConcurrentHashMap<>(); + private static final Map SPANS = new ConcurrentHashMap<>(); TraceJobExecutionListener(Tracer tracer) { this.tracer = tracer; @@ -39,7 +40,8 @@ class TraceJobExecutionListener implements JobExecutionListener { @Override public void beforeJob(JobExecution jobExecution) { - Span span = this.tracer.nextSpan().name(jobExecution.getJobInstance().getJobName()); + Span span = SleuthBatchSpan.BATCH_JOB_SPAN.wrap(this.tracer.nextSpan()) + .name(jobExecution.getJobInstance().getJobName()); Tracer.SpanInScope spanInScope = this.tracer.withSpan(span.start()); SPANS.put(jobExecution, new SpanAndScope(span, spanInScope)); } @@ -48,21 +50,24 @@ class TraceJobExecutionListener implements JobExecutionListener { public void afterJob(JobExecution jobExecution) { SpanAndScope spanAndScope = SPANS.remove(jobExecution); List throwables = jobExecution.getFailureExceptions(); - Span span = spanAndScope.getSpan(); - span.tag("batch.job.name", jobExecution.getJobInstance().getJobName()); - span.tag("batch.job.instanceId", String.valueOf(jobExecution.getJobInstance().getInstanceId())); - span.tag("batch.job.executionId", String.valueOf(jobExecution.getId())); - Tracer.SpanInScope scope = spanAndScope.getScope(); - if (!throwables.isEmpty()) { - span.error(mergedThrowables(throwables)); - } - span.end(); - scope.close(); - } - - private IllegalStateException mergedThrowables(List throwables) { - return new IllegalStateException( - throwables.stream().map(Throwable::toString).collect(Collectors.joining("\n"))); - } - + // @formatter:off + AssertingSpan span = SleuthBatchSpan.BATCH_JOB_SPAN.wrap(spanAndScope.getSpan()) + .tag(SleuthBatchSpan.JobTags.JOB_NAME, jobExecution.getJobInstance().getJobName()) + .tag(SleuthBatchSpan.JobTags.JOB_INSTANCE_ID, + String.valueOf(jobExecution.getJobInstance().getInstanceId())) + .tag(SleuthBatchSpan.JobTags.JOB_EXECUTION_ID, String.valueOf(jobExecution.getId())); + // formatter:on + Tracer.SpanInScope scope = spanAndScope.getScope(); + if (!throwables.isEmpty()) { + span.error(mergedThrowables(throwables)); + } + span.end(); + scope.close(); + } + + private IllegalStateException mergedThrowables(List throwables) { + return new IllegalStateException( + throwables.stream().map(Throwable::toString).collect(Collectors.joining("\n"))); + } + } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceStepExecutionListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceStepExecutionListener.java index 185e7c553..9bbdce678 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceStepExecutionListener.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceStepExecutionListener.java @@ -28,12 +28,13 @@ import org.springframework.batch.core.StepExecutionListener; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.SpanAndScope; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; class TraceStepExecutionListener implements StepExecutionListener { private final Tracer tracer; - private static Map SPANS = new ConcurrentHashMap<>(); + private static final Map SPANS = new ConcurrentHashMap<>(); TraceStepExecutionListener(Tracer tracer) { this.tracer = tracer; @@ -41,7 +42,7 @@ class TraceStepExecutionListener implements StepExecutionListener { @Override public void beforeStep(StepExecution stepExecution) { - Span span = this.tracer.nextSpan().name(stepExecution.getStepName()); + Span span = SleuthBatchSpan.BATCH_STEP_SPAN.wrap(this.tracer.nextSpan()).name(stepExecution.getStepName()); Tracer.SpanInScope spanInScope = this.tracer.withSpan(span.start()); SPANS.put(stepExecution, new SpanAndScope(span, spanInScope)); } @@ -50,11 +51,13 @@ class TraceStepExecutionListener implements StepExecutionListener { public ExitStatus afterStep(StepExecution stepExecution) { SpanAndScope spanAndScope = SPANS.remove(stepExecution); List throwables = stepExecution.getFailureExceptions(); - Span span = spanAndScope.getSpan(); - span.tag("batch.step.name", stepExecution.getStepName()); - span.tag("batch.job.executionId", String.valueOf(stepExecution.getJobExecutionId())); - span.tag("batch.step.executionId", String.valueOf(stepExecution.getId())); - span.tag("batch.step.type", stepExecution.getExecutionContext().getString(Step.STEP_TYPE_KEY)); + // @formatter:off + AssertingSpan span = SleuthBatchSpan.BATCH_STEP_SPAN.wrap(spanAndScope.getSpan()) + .tag(SleuthBatchSpan.StepTags.STEP_NAME, stepExecution.getStepName()) + .tag(SleuthBatchSpan.StepTags.JOB_EXECUTION_ID, String.valueOf(stepExecution.getJobExecutionId())) + .tag(SleuthBatchSpan.StepTags.STEP_EXECUTION_ID, String.valueOf(stepExecution.getId())) + .tag(SleuthBatchSpan.StepTags.STEP_TYPE, stepExecution.getExecutionContext().getString(Step.STEP_TYPE_KEY)); + // @formatter:on Tracer.SpanInScope scope = spanAndScope.getScope(); if (!throwables.isEmpty()) { span.error(mergedThrowables(throwables)); diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/SleuthCircuitBreakerSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/SleuthCircuitBreakerSpan.java new file mode 100644 index 000000000..380dc9758 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/SleuthCircuitBreakerSpan.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.circuitbreaker; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; + +enum SleuthCircuitBreakerSpan implements DocumentedSpan { + + /** + * Span created when we wrap a Supplier passed to the CircuitBreaker. + */ + CIRCUIT_BREAKER_SUPPLIER_SPAN { + @Override + public String getName() { + return "%s"; + } + }, + + /** + * Span created when we wrap a Function passed to the CircuitBreaker. as fallback. + */ + CIRCUIT_BREAKER_FUNCTION_SPAN { + @Override + public String getName() { + return "%s"; + } + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java index d5f8f6df8..ffc66ffa6 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceFunction.java @@ -19,8 +19,8 @@ package org.springframework.cloud.sleuth.instrument.circuitbreaker; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; -import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; /** * Trace representation of a {@link Function}. @@ -34,19 +34,20 @@ class TraceFunction implements Function { private final Function delegate; - private final AtomicReference span; + private final AtomicReference span; TraceFunction(Tracer tracer, Function delegate) { this.tracer = tracer; this.delegate = delegate; - this.span = new AtomicReference<>(this.tracer.nextSpan()); + this.span = new AtomicReference<>( + SleuthCircuitBreakerSpan.CIRCUIT_BREAKER_FUNCTION_SPAN.wrap(this.tracer.nextSpan())); } @Override public T apply(Throwable throwable) { // TODO: This name needs to be better String name = this.delegate.getClass().getSimpleName(); - Span span = this.span.get().name(name); + AssertingSpan span = this.span.get().name(name); Throwable tr = null; try (Tracer.SpanInScope ws = this.tracer.withSpan(span.start())) { return this.delegate.apply(throwable); diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/config/SleuthConfigSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/config/SleuthConfigSpan.java new file mode 100644 index 000000000..80147c3db --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/config/SleuthConfigSpan.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.config; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthConfigSpan implements DocumentedSpan { + + /** + * Span created around an EnvironmentRepository. + */ + CONFIG_SPAN { + @Override + public String getName() { + return "find"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + }; + + enum Tags implements TagKey { + + /** + * Implementation of the EnvironmentRepository. + */ + ENVIRONMENT_CLASS { + @Override + public String getKey() { + return "config.environment.class"; + } + }, + + /** + * Method executed on the EnvironmentRepository. + */ + ENVIRONMENT_METHOD { + @Override + public String getKey() { + return "config.environment.method"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/config/TraceEnvironmentRepositoryAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/config/TraceEnvironmentRepositoryAspect.java index 93b420f00..f18d88f00 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/config/TraceEnvironmentRepositoryAspect.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/config/TraceEnvironmentRepositoryAspect.java @@ -20,8 +20,8 @@ import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; -import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; /** * Aspect wrapping resolution of properties. @@ -40,9 +40,12 @@ public class TraceEnvironmentRepositoryAspect { @Around("execution (* org.springframework.cloud.config.server.environment.EnvironmentRepository.*(..))") public Object traceFindEnvironment(final ProceedingJoinPoint pjp) throws Throwable { - Span findOneSpan = this.tracer.nextSpan().name("find"); - findOneSpan.tag("config.environment.class", pjp.getTarget().getClass().getName()); - findOneSpan.tag("config.environment.method", pjp.getSignature().getName()); + // @formatter:off + AssertingSpan findOneSpan = SleuthConfigSpan.CONFIG_SPAN.wrap(this.tracer.nextSpan()) + .name(SleuthConfigSpan.CONFIG_SPAN.getName()) + .tag(SleuthConfigSpan.Tags.ENVIRONMENT_CLASS, pjp.getTarget().getClass().getName()) + .tag(SleuthConfigSpan.Tags.ENVIRONMENT_METHOD, pjp.getSignature().getName()); + // @formatter:on try (Tracer.SpanInScope ws = this.tracer.withSpan(findOneSpan.start())) { return pjp.proceed(); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/SleuthDeployerSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/SleuthDeployerSpan.java new file mode 100644 index 000000000..ea7703d47 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/SleuthDeployerSpan.java @@ -0,0 +1,287 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.deployer; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.EventValue; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthDeployerSpan implements DocumentedSpan { + + /** + * Span created upon deploying of an application. + */ + DEPLOYER_DEPLOY_SPAN { + @Override + public String getName() { + return "deploy"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + @Override + public EventValue[] getEvents() { + return Events.values(); + } + }, + + /** + * Span created upon undeploying of an application. + */ + DEPLOYER_UNDEPLOY_SPAN { + @Override + public String getName() { + return "undeploy"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + @Override + public EventValue[] getEvents() { + return Events.values(); + } + }, + + /** + * Span created upon asking for a status of a deployed application. + */ + DEPLOYER_STATUS_SPAN { + @Override + public String getName() { + return "status"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + @Override + public EventValue[] getEvents() { + return Events.values(); + } + }, + + /** + * Span created upon asking for statuses of deployed applications. + */ + DEPLOYER_STATUSES_SPAN { + @Override + public String getName() { + return "statuses"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + @Override + public EventValue[] getEvents() { + return Events.values(); + } + }, + + /** + * Span created upon asking for logs of deployed applications. + */ + DEPLOYER_GET_LOG_SPAN { + @Override + public String getName() { + return "getLog"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + @Override + public EventValue[] getEvents() { + return Events.values(); + } + }, + + /** + * Span created upon asking for logs of deployed applications. + */ + DEPLOYER_SCALE_SPAN { + @Override + public String getName() { + return "scale"; + } + + @Override + public TagKey[] getTagKeys() { + return TagKey.merge(Tags.values(), ScaleTags.values()); + } + + @Override + public EventValue[] getEvents() { + return Events.values(); + } + }; + + enum Tags implements TagKey { + + /** + * Name of the platform to which apps are being deployed. + */ + PLATFORM_NAME { + @Override + public String getKey() { + return "deployer.platform.name"; + } + }, + + /** + * ID of the deployed application. + */ + APP_ID { + @Override + public String getKey() { + return "deployer.app.id"; + } + }, + + /** + * Name of the deployed application. + */ + APP_NAME { + @Override + public String getKey() { + return "deployer.app.name"; + } + }, + + /** + * Group of the deployed application. + */ + APP_GROUP { + @Override + public String getKey() { + return "deployer.app.group"; + } + }, + + /** + * CloudFoundry API URL. + */ + CF_URL { + @Override + public String getKey() { + return "deployer.platform.cf.url"; + } + }, + + /** + * CloudFoundry org. + */ + CF_ORG { + @Override + public String getKey() { + return "deployer.platform.cf.org"; + } + }, + + /** + * CloudFoundry space. + */ + CF_SPACE { + @Override + public String getKey() { + return "deployer.platform.cf.space"; + } + }, + + /** + * Kubernetes API URL. + */ + K8S_URL { + @Override + public String getKey() { + return "deployer.platform.k8s.url"; + } + }, + + /** + * Kubernetes namespace. + */ + K8S_NAMESPACE { + @Override + public String getKey() { + return "deployer.platform.k8s.namespace"; + } + }, + + } + + enum ScaleTags implements TagKey { + + /** + * Scale command deployment id. + */ + DEPLOYER_SCALE_DEPLOYMENT_ID { + @Override + public String getKey() { + return "deployer.scale.deploymentId"; + } + }, + + /** + * Scale count. + */ + DEPLOYER_SCALE_COUNT { + @Override + public String getKey() { + return "deployer.scale.count"; + } + } + + } + + enum Events implements EventValue { + + /** + * When deployer started deploying the application. + */ + DEPLOYER_START { + @Override + public String getValue() { + return "deployer.start"; + } + }, + + /** + * When deployer changes the state of the deployed application. + */ + DEPLOYER_STATUS_CHANGE { + @Override + public String getValue() { + return "%s"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java index d03463e19..f0c60f0d1 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java @@ -1,155 +1,160 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.deployer; - -import java.time.Duration; -import java.util.Arrays; -import java.util.Map; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.cloud.deployer.spi.app.AppDeployer; -import org.springframework.cloud.deployer.spi.app.AppScaleRequest; -import org.springframework.cloud.deployer.spi.app.AppStatus; -import org.springframework.cloud.deployer.spi.app.DeploymentState; -import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; -import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; -import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; -import org.springframework.core.env.Environment; -import org.springframework.lang.Nullable; -import org.springframework.util.StringUtils; - -/** - * Trace representation of an {@link AppDeployer}. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -public class TraceAppDeployer implements AppDeployer { - - private static final Log log = LogFactory.getLog(TraceAppDeployer.class); - - private final AppDeployer delegate; - - private final BeanFactory beanFactory; - - private final Environment environment; - - private Tracer tracer; - - private CurrentTraceContext currentTraceContext; - - private Long pollDelay; - - public TraceAppDeployer(AppDeployer delegate, BeanFactory beanFactory, Environment environment) { - this.delegate = delegate; - this.beanFactory = beanFactory; - this.environment = environment; - } - - @Override - public String deploy(AppDeploymentRequest request) { - Span.Builder spanBuilder = clientSpan("deploy", request); - Span span = spanBuilder.start(); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { - span.event("deployer.start"); - String id = this.delegate.deploy(request); - span.tag("deployer.app.id", id); - registerListener(span, id); - return id; - } - } - - private Span.Builder clientSpan(String name) { - return clientSpan(name, null, null); - } - - private Span.Builder clientSpan(String name, @Nullable AppDeploymentRequest request) { - return clientSpan(name, null, request); - } - - private Span.Builder clientSpanKind(String name, Span.Builder spanBuilder) { - return spanBuilder.kind(Span.Kind.CLIENT).name(name).remoteServiceName(remoteServiceName()); - } - - private Span.Builder clientSpan(String name, Span parentSpan) { - return clientSpan(name, parentSpan, null); - } - - private Span.Builder clientSpan(String name, @Nullable Span parentSpan, @Nullable AppDeploymentRequest request) { - Span.Builder spanBuilder = tracer().spanBuilder(); - Span currentSpan = parentSpan != null ? parentSpan : tracer().currentSpan(); - if (currentSpan != null) { - spanBuilder.setParent(currentSpan.context()); - } - Map platformSpecificInfo = environmentInfo().getPlatformSpecificInfo(); - if (request != null) { - String platformName = request.getDeploymentProperties().get("spring.cloud.deployer.platformName"); - if (StringUtils.hasText(platformName)) { - spanBuilder.tag("deployer.platform.name", platformName); - } - String appName = request.getDeploymentProperties().get("spring.cloud.deployer.appName"); - if (StringUtils.hasText(appName)) { - spanBuilder.tag("deployer.app.name", appName); - } - String group = request.getDeploymentProperties().get("spring.cloud.deployer.group"); - if (StringUtils.hasText(group)) { - spanBuilder.tag("deployer.app.name", group); - } - } - addCfTags(spanBuilder, platformSpecificInfo); - addK8sTags(spanBuilder, platformSpecificInfo); - return clientSpanKind(name, spanBuilder); - } - - private void addCfTags(Span.Builder spanBuilder, Map platformSpecificInfo) { - if (platformSpecificInfo.containsKey("API Endpoint")) { - spanBuilder.tag("deployer.platform.cf.url", platformSpecificInfo.get("API Endpoint")); - } - if (platformSpecificInfo.containsKey("Organization")) { - spanBuilder.tag("deployer.platform.cf.org", platformSpecificInfo.get("Organization")); - } - if (platformSpecificInfo.containsKey("Space")) { - spanBuilder.tag("deployer.platform.cf.space", platformSpecificInfo.get("Space")); - } - } - - private void addK8sTags(Span.Builder spanBuilder, Map platformSpecificInfo) { - if (platformSpecificInfo.containsKey("master-url")) { - spanBuilder.tag("deployer.platform.k8s.url", platformSpecificInfo.get("master-url")); - } - if (platformSpecificInfo.containsKey("namespace")) { - spanBuilder.tag("deployer.platform.k8s.namespace", platformSpecificInfo.get("namespace")); - } - } - - private String remoteServiceName() { - return environmentInfo().getPlatformType(); - } - - private void registerListener(Span span, String id) { - PreviousAndCurrentStatus previousAndCurrentStatus = new PreviousAndCurrentStatus(span); +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.deployer; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.deployer.spi.app.AppDeployer; +import org.springframework.cloud.deployer.spi.app.AppScaleRequest; +import org.springframework.cloud.deployer.spi.app.AppStatus; +import org.springframework.cloud.deployer.spi.app.DeploymentState; +import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; +import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; +import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; +import org.springframework.core.env.Environment; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Trace representation of an {@link AppDeployer}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceAppDeployer implements AppDeployer { + + private static final Log log = LogFactory.getLog(TraceAppDeployer.class); + + private final AppDeployer delegate; + + private final BeanFactory beanFactory; + + private final Environment environment; + + private Tracer tracer; + + private CurrentTraceContext currentTraceContext; + + private Long pollDelay; + + public TraceAppDeployer(AppDeployer delegate, BeanFactory beanFactory, Environment environment) { + this.delegate = delegate; + this.beanFactory = beanFactory; + this.environment = environment; + } + + @Override + public String deploy(AppDeploymentRequest request) { + AssertingSpanBuilder spanBuilder = clientSpan(SleuthDeployerSpan.DEPLOYER_DEPLOY_SPAN, request); + AssertingSpan span = spanBuilder.start(); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { + span.event(SleuthDeployerSpan.Events.DEPLOYER_START); + String id = this.delegate.deploy(request); + span.tag(SleuthDeployerSpan.Tags.APP_ID, id); + registerListener(span, id); + return id; + } + } + + private AssertingSpanBuilder clientSpan(DocumentedSpan documentedSpan) { + return clientSpan(documentedSpan, null, null); + } + + private AssertingSpanBuilder clientSpan(DocumentedSpan documentedSpan, @Nullable AppDeploymentRequest request) { + return clientSpan(documentedSpan, null, request); + } + + private AssertingSpanBuilder clientSpanKind(String name, AssertingSpanBuilder spanBuilder) { + return spanBuilder.kind(Span.Kind.CLIENT).name(name).remoteServiceName(remoteServiceName()); + } + + private AssertingSpanBuilder clientSpan(DocumentedSpan documentedSpan, Span parentSpan) { + return clientSpan(documentedSpan, parentSpan, null); + } + + private AssertingSpanBuilder clientSpan(DocumentedSpan documentedSpan, @Nullable Span parentSpan, + @Nullable AppDeploymentRequest request) { + String name = documentedSpan.getName(); + AssertingSpanBuilder spanBuilder = AssertingSpanBuilder.of(documentedSpan, tracer().spanBuilder()); + Span currentSpan = parentSpan != null ? parentSpan : tracer().currentSpan(); + if (currentSpan != null) { + spanBuilder.setParent(currentSpan.context()); + } + Map platformSpecificInfo = environmentInfo().getPlatformSpecificInfo(); + if (request != null) { + String platformName = request.getDeploymentProperties().get("spring.cloud.deployer.platformName"); + if (StringUtils.hasText(platformName)) { + spanBuilder.tag(SleuthDeployerSpan.Tags.PLATFORM_NAME, platformName); + } + String appName = request.getDeploymentProperties().get("spring.cloud.deployer.appName"); + if (StringUtils.hasText(appName)) { + spanBuilder.tag(SleuthDeployerSpan.Tags.APP_NAME, appName); + } + String group = request.getDeploymentProperties().get("spring.cloud.deployer.group"); + if (StringUtils.hasText(group)) { + spanBuilder.tag(SleuthDeployerSpan.Tags.APP_GROUP, group); + } + } + addCfTags(spanBuilder, platformSpecificInfo); + addK8sTags(spanBuilder, platformSpecificInfo); + return clientSpanKind(name, spanBuilder); + } + + private void addCfTags(AssertingSpanBuilder spanBuilder, Map platformSpecificInfo) { + if (platformSpecificInfo.containsKey("API Endpoint")) { + spanBuilder.tag(SleuthDeployerSpan.Tags.CF_URL, platformSpecificInfo.get("API Endpoint")); + } + if (platformSpecificInfo.containsKey("Organization")) { + spanBuilder.tag(SleuthDeployerSpan.Tags.CF_ORG, platformSpecificInfo.get("Organization")); + } + if (platformSpecificInfo.containsKey("Space")) { + spanBuilder.tag(SleuthDeployerSpan.Tags.CF_SPACE, platformSpecificInfo.get("Space")); + } + } + + private void addK8sTags(AssertingSpanBuilder spanBuilder, Map platformSpecificInfo) { + if (platformSpecificInfo.containsKey("master-url")) { + spanBuilder.tag(SleuthDeployerSpan.Tags.K8S_URL, platformSpecificInfo.get("master-url")); + } + if (platformSpecificInfo.containsKey("namespace")) { + spanBuilder.tag(SleuthDeployerSpan.Tags.K8S_NAMESPACE, platformSpecificInfo.get("namespace")); + } + } + + private String remoteServiceName() { + return environmentInfo().getPlatformType(); + } + + private void registerListener(Span span, String id) { + PreviousAndCurrentStatus previousAndCurrentStatus = new PreviousAndCurrentStatus(span); // @formatter:off this.delegate.statusReactive(id) .map(previousAndCurrentStatus::updateCurrent) @@ -160,173 +165,174 @@ public class TraceAppDeployer implements AppDeployer { .doOnError(span::error) // we will close the span in the reactive part .doFinally(signalType -> span.end()).subscribe(); - // @formatter:on - } - - @Override - public void undeploy(String id) { - Span.Builder spanBuilder = clientSpan("undeploy"); - Span span = spanBuilder.start(); - span.tag("deployer.app.id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { - span.event("deployer.start"); - this.delegate.undeploy(id); - registerListener(span, id); - } - finally { - span.end(); - } - } - - @Override - public AppStatus status(String id) { - Span.Builder spanBuilder = clientSpan("status"); - Span span = spanBuilder.start(); - span.tag("deployer.app.id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - return this.delegate.status(id); - } - finally { - span.end(); - } - } - - @Override - public Mono statusReactive(String id) { - return ReactorSleuth.tracedMono(tracer(), currentTraceContext(), "status", - () -> this.delegate.statusReactive(id), (o, span) -> span.tag("deployer.app.id", id), - span -> clientSpan("status", span).start()); - } - - @Override - public Flux statusesReactive(String... ids) { - return ReactorSleuth.tracedFlux(tracer(), currentTraceContext(), "statuses", - () -> this.delegate.statusesReactive(ids), - (o, span) -> span.tag("deployer.app.ids", Arrays.toString(ids)), - span -> clientSpan("statuses", span).start()); - } - - @Override - public RuntimeEnvironmentInfo environmentInfo() { - return this.delegate.environmentInfo(); - } - - @Override - public String getLog(String id) { - Span.Builder spanBuilder = clientSpan("getLog"); - Span span = spanBuilder.start(); - span.tag("deployer.app.id", id); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { - return this.delegate.getLog(id); - } - finally { - span.end(); - } - } - - @Override - public void scale(AppScaleRequest appScaleRequest) { - Span.Builder spanBuilder = clientSpan("scale"); - Span span = spanBuilder.start(); - span.tag("deployer.scale.deploymentId", appScaleRequest.getDeploymentId()); - span.tag("deployer.scale.count", String.valueOf(appScaleRequest.getCount())); - try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { - this.delegate.scale(appScaleRequest); - } - finally { - span.end(); - } - } - - private Tracer tracer() { - if (this.tracer == null) { - this.tracer = this.beanFactory.getBean(Tracer.class); - } - return this.tracer; - } - - private CurrentTraceContext currentTraceContext() { - if (this.currentTraceContext == null) { - this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); - } - return this.currentTraceContext; - } - - private long pollDelay() { - if (this.pollDelay == null) { - this.pollDelay = this.environment.getProperty("spring.sleuth.deployer.status-poll-delay", Long.class, 500L); - } - return this.pollDelay; - } - - private static final class PreviousAndCurrentStatus { - - private final Span span; - - private AppStatus current; - - private AppStatus previous; - - private PreviousAndCurrentStatus(Span span) { - this.span = span; - if (log.isDebugEnabled()) { - log.debug("Current span is [" + span + "]"); - } - } - - private PreviousAndCurrentStatus updateCurrent(AppStatus current) { - if (log.isTraceEnabled()) { - log.trace("State before change: current [" + this.current + "], previous [" + this.previous + "]"); - } - this.previous = this.current; - this.current = current; - if (log.isTraceEnabled()) { - log.trace("State after change: current [" + this.current + "], previous [" + this.previous + "]"); - } - if (statusChanged()) { - annotateSpan(); - } - else if (log.isTraceEnabled()) { - log.trace("State has not changed, will not annotate the span"); - } - return this; - } - - private void annotateSpan() { - String name = this.current.getState().name(); - if (log.isDebugEnabled()) { - log.debug("Will annotate its state with [" + name + "]"); - } - this.span.event(name); - } - - private boolean statusChanged() { - if (this.previous == null && this.current != null) { - if (log.isDebugEnabled()) { - log.debug("Previous is null, current is not null"); - } - return true; - } - else if (this.current == null) { - throw new IllegalStateException("Current state can't be null"); - } - DeploymentState currentState = this.current.getState(); - DeploymentState previousState = this.previous.getState(); - return currentState != previousState; - } - - private boolean isFinished() { - boolean finished = this.current.getState() == DeploymentState.deployed - || this.current.getState() == DeploymentState.undeployed - || this.current.getState() == DeploymentState.failed - || this.current.getState() == DeploymentState.error - || this.current.getState() == DeploymentState.unknown; - if (log.isTraceEnabled()) { - log.trace("Status is finished [" + finished + "]"); - } - return finished; - } - - } - -} + // @formatter:on + } + + @Override + public void undeploy(String id) { + AssertingSpanBuilder spanBuilder = clientSpan(SleuthDeployerSpan.DEPLOYER_UNDEPLOY_SPAN); + AssertingSpan span = spanBuilder.start(); + span.tag(SleuthDeployerSpan.Tags.APP_ID, id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { + span.event(SleuthDeployerSpan.Events.DEPLOYER_START); + this.delegate.undeploy(id); + registerListener(span, id); + } + finally { + span.end(); + } + } + + @Override + public AppStatus status(String id) { + AssertingSpanBuilder spanBuilder = clientSpan(SleuthDeployerSpan.DEPLOYER_STATUS_SPAN); + AssertingSpan span = spanBuilder.start(); + span.tag(SleuthDeployerSpan.Tags.APP_ID, id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + return this.delegate.status(id); + } + finally { + span.end(); + } + } + + @Override + public Mono statusReactive(String id) { + return ReactorSleuth.tracedMono(tracer(), currentTraceContext(), + SleuthDeployerSpan.DEPLOYER_STATUS_SPAN.getName(), () -> this.delegate.statusReactive(id), + (o, span) -> span.tag(SleuthDeployerSpan.Tags.APP_ID.getKey(), id), + span -> clientSpan(SleuthDeployerSpan.DEPLOYER_STATUS_SPAN, span).start()); + } + + @Override + public Flux statusesReactive(String... ids) { + return ReactorSleuth.tracedFlux(tracer(), currentTraceContext(), + SleuthDeployerSpan.DEPLOYER_STATUSES_SPAN.getName(), () -> this.delegate.statusesReactive(ids), + (o, span) -> span.tag(SleuthDeployerSpan.Tags.APP_ID.getKey(), Arrays.toString(ids)), + span -> clientSpan(SleuthDeployerSpan.DEPLOYER_STATUSES_SPAN, span).start()); + } + + @Override + public RuntimeEnvironmentInfo environmentInfo() { + return this.delegate.environmentInfo(); + } + + @Override + public String getLog(String id) { + AssertingSpanBuilder spanBuilder = clientSpan(SleuthDeployerSpan.DEPLOYER_GET_LOG_SPAN); + AssertingSpan span = spanBuilder.start(); + span.tag(SleuthDeployerSpan.Tags.APP_ID, id); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { + return this.delegate.getLog(id); + } + finally { + span.end(); + } + } + + @Override + public void scale(AppScaleRequest appScaleRequest) { + AssertingSpanBuilder spanBuilder = clientSpan(SleuthDeployerSpan.DEPLOYER_SCALE_SPAN); + AssertingSpan span = spanBuilder.start(); + span.tag(SleuthDeployerSpan.ScaleTags.DEPLOYER_SCALE_DEPLOYMENT_ID, appScaleRequest.getDeploymentId()); + span.tag(SleuthDeployerSpan.ScaleTags.DEPLOYER_SCALE_COUNT, String.valueOf(appScaleRequest.getCount())); + try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { + this.delegate.scale(appScaleRequest); + } + finally { + span.end(); + } + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + + private CurrentTraceContext currentTraceContext() { + if (this.currentTraceContext == null) { + this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); + } + return this.currentTraceContext; + } + + private long pollDelay() { + if (this.pollDelay == null) { + this.pollDelay = this.environment.getProperty("spring.sleuth.deployer.status-poll-delay", Long.class, 500L); + } + return this.pollDelay; + } + + private static final class PreviousAndCurrentStatus { + + private final Span span; + + private AppStatus current; + + private AppStatus previous; + + private PreviousAndCurrentStatus(Span span) { + this.span = span; + if (log.isDebugEnabled()) { + log.debug("Current span is [" + span + "]"); + } + } + + private PreviousAndCurrentStatus updateCurrent(AppStatus current) { + if (log.isTraceEnabled()) { + log.trace("State before change: current [" + this.current + "], previous [" + this.previous + "]"); + } + this.previous = this.current; + this.current = current; + if (log.isTraceEnabled()) { + log.trace("State after change: current [" + this.current + "], previous [" + this.previous + "]"); + } + if (statusChanged()) { + annotateSpan(); + } + else if (log.isTraceEnabled()) { + log.trace("State has not changed, will not annotate the span"); + } + return this; + } + + private void annotateSpan() { + String name = this.current.getState().name(); + if (log.isDebugEnabled()) { + log.debug("Will annotate its state with [" + name + "]"); + } + this.span.event(String.format(SleuthDeployerSpan.Events.DEPLOYER_STATUS_CHANGE.getValue(), name)); + } + + private boolean statusChanged() { + if (this.previous == null && this.current != null) { + if (log.isDebugEnabled()) { + log.debug("Previous is null, current is not null"); + } + return true; + } + else if (this.current == null) { + throw new IllegalStateException("Current state can't be null"); + } + DeploymentState currentState = this.current.getState(); + DeploymentState previousState = this.previous.getState(); + return currentState != previousState; + } + + private boolean isFinished() { + boolean finished = this.current.getState() == DeploymentState.deployed + || this.current.getState() == DeploymentState.undeployed + || this.current.getState() == DeploymentState.failed + || this.current.getState() == DeploymentState.error + || this.current.getState() == DeploymentState.unknown; + if (log.isTraceEnabled()) { + log.trace("Status is finished [" + finished + "]"); + } + return finished; + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingUtils.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingUtils.java index 3c3b23461..55e06ae75 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingUtils.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingUtils.java @@ -21,6 +21,7 @@ import org.apache.commons.logging.LogFactory; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; import org.springframework.cloud.sleuth.propagation.Propagator; final class KafkaTracingUtils { @@ -32,10 +33,13 @@ final class KafkaTracingUtils { static void buildAndFinishSpan(ConsumerRecord consumerRecord, Propagator propagator, Propagator.Getter> extractor) { - Span.Builder spanBuilder = propagator.extract(consumerRecord, extractor).kind(Span.Kind.CONSUMER) - .name("kafka.consume").tag("kafka.topic", consumerRecord.topic()) - .tag("kafka.offset", Long.toString(consumerRecord.offset())) - .tag("kafka.partition", Integer.toString(consumerRecord.partition())); + // @formatter:off + Span.Builder spanBuilder = AssertingSpanBuilder.of(SleuthKafkaSpan.KAFKA_CONSUMER_SPAN, propagator.extract(consumerRecord, extractor).kind(Span.Kind.CONSUMER)) + .name(SleuthKafkaSpan.KAFKA_CONSUMER_SPAN.getName()) + .tag(SleuthKafkaSpan.ConsumerTags.TOPIC, consumerRecord.topic()) + .tag(SleuthKafkaSpan.ConsumerTags.OFFSET, Long.toString(consumerRecord.offset())) + .tag(SleuthKafkaSpan.ConsumerTags.PARTITION, Integer.toString(consumerRecord.partition())); + // @formatter:on Span span = spanBuilder.start(); if (log.isDebugEnabled()) { log.debug("Extracted span from event headers " + span); diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/SleuthKafkaSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/SleuthKafkaSpan.java new file mode 100644 index 000000000..e19cf3c54 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/SleuthKafkaSpan.java @@ -0,0 +1,112 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kafka; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthKafkaSpan implements DocumentedSpan { + + /** + * Span created on the Kafka consumer side. + */ + KAFKA_CONSUMER_SPAN { + @Override + public String getName() { + return "kafka.consume"; + } + + @Override + public TagKey[] getTagKeys() { + return ConsumerTags.values(); + } + + @Override + public String prefix() { + return "kafka."; + } + }, + + /** + * Span created on the Kafka consumer side. + */ + KAFKA_PRODUCER_SPAN { + @Override + public String getName() { + return "kafka.produce"; + } + + @Override + public TagKey[] getTagKeys() { + return ProducerTags.values(); + } + + @Override + public String prefix() { + return "kafka."; + } + }; + + enum ConsumerTags implements TagKey { + + /** + * Name of the Kafka topic. + */ + TOPIC { + @Override + public String getKey() { + return "kafka.topic"; + } + }, + + /** + * Kafka offset number. + */ + OFFSET { + @Override + public String getKey() { + return "kafka.offset"; + } + }, + + /** + * Kafka partition number. + */ + PARTITION { + @Override + public String getKey() { + return "kafka.partition"; + } + }, + + } + + enum ProducerTags implements TagKey { + + /** + * Name of the Kafka topic. + */ + TOPIC { + @Override + public String getKey() { + return "kafka.topic"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducer.java index 9a75e6748..b10804a54 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducer.java @@ -38,6 +38,7 @@ import org.apache.kafka.common.errors.ProducerFencedException; import org.springframework.beans.factory.BeanFactory; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; import org.springframework.cloud.sleuth.propagation.Propagator; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; @@ -134,8 +135,10 @@ public class TracingKafkaProducer implements Producer { @Override public Future send(ProducerRecord producerRecord, Callback callback) { - Span.Builder spanBuilder = tracer().spanBuilder().kind(Span.Kind.PRODUCER).name("kafka.produce") - .tag("kafka.topic", producerRecord.topic()); + Span.Builder spanBuilder = AssertingSpanBuilder + .of(SleuthKafkaSpan.KAFKA_PRODUCER_SPAN, tracer().spanBuilder().kind(Span.Kind.PRODUCER)) + .name(SleuthKafkaSpan.KAFKA_PRODUCER_SPAN.getName()) + .tag(SleuthKafkaSpan.ProducerTags.TOPIC, producerRecord.topic()); Span span = spanBuilder.start(); propagator().inject(span.context(), producerRecord, injector()); try (Tracer.SpanInScope spanInScope = tracer().withSpan(span)) { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/DefaultMessageSpanCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/DefaultMessageSpanCustomizer.java index 29be5c7e6..6ef9ecaa2 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/DefaultMessageSpanCustomizer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/DefaultMessageSpanCustomizer.java @@ -100,13 +100,15 @@ public class DefaultMessageSpanCustomizer implements MessageSpanCustomizer { private void addTags(SpanCustomizer result, MessageChannel channel) { if (channel != null) { - result.tag("channel", messageChannelName(channel)); + SleuthMessagingSpan.MESSAGING_SPAN.wrap(result).tag(SleuthMessagingSpan.Tags.CHANNEL, + messageChannelName(channel)); } } private void addTags(Span.Builder result, MessageChannel channel) { if (channel != null) { - result.tag("channel", messageChannelName(channel)); + SleuthMessagingSpan.MESSAGING_SPAN.wrap(result).tag(SleuthMessagingSpan.Tags.CHANNEL, + messageChannelName(channel)); } } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/SleuthMessagingSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/SleuthMessagingSpan.java new file mode 100644 index 000000000..1a8cfccba --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/SleuthMessagingSpan.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.messaging; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthMessagingSpan implements DocumentedSpan { + + /** + * Span created when message is sent or received. + */ + MESSAGING_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + }; + + enum Tags implements TagKey { + + /** + * Name of the Spring Integration channel. + */ + CHANNEL { + @Override + public String getKey() { + return "channel"; + } + }, + + /** + * User provided keys via customization options. + */ + CUSTOM { + @Override + public String getKey() { + return "%s"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessageHandler.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessageHandler.java index 52d02ffb3..8628fa810 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessageHandler.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessageHandler.java @@ -97,7 +97,8 @@ class TraceMessageHandler { static TraceMessageHandler forNonSpringIntegration(Tracer tracer, Propagator propagator, Propagator.Setter injector, Propagator.Getter extractor) { - Function preSendFunction = span -> tracer.nextSpan(span).name("handle").start(); + Function preSendFunction = span -> SleuthMessagingSpan.MESSAGING_SPAN.wrap(tracer.nextSpan(span)) + .name("handle").start(); TriConsumer preSendMessageManipulator = (headers, parentSpan, childSpan) -> { headers.setHeader("traceHandlerParentSpan", parentSpan); headers.setHeader(Span.class.getName(), childSpan); @@ -136,7 +137,8 @@ class TraceMessageHandler { MessageHeaderAccessor headers = mutableHeaderAccessor(message); Span extracted = this.propagator.extract(headers, this.extractor).start(); // Start and finish a consumer span as we will immediately process it. - Span.Builder consumerSpanBuilder = this.tracer.spanBuilder().setParent(extracted.context()); + Span.Builder consumerSpanBuilder = SleuthMessagingSpan.MESSAGING_SPAN + .wrap(this.tracer.spanBuilder().setParent(extracted.context())); Span consumerSpan = consumerSpan(destinationName, extracted, consumerSpanBuilder); // create and scope a span for the message processor Span span = this.preSendFunction.apply(consumerSpan); @@ -187,7 +189,8 @@ class TraceMessageHandler { private void addTags(Span.Builder result, String destinationName) { if (StringUtils.hasText(destinationName)) { - result.tag("channel", SpanNameUtil.shorten(destinationName)); + SleuthMessagingSpan.MESSAGING_SPAN.wrap(result).tag(SleuthMessagingSpan.Tags.CHANNEL, + SpanNameUtil.shorten(destinationName)); } } @@ -318,6 +321,7 @@ class TraceMessageHandler { if (message == null) { message = error.getClass().getSimpleName(); } + // TODO: Go with span.error(...) span.tag("error", message); } span.end(); diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java index 9ee309081..786e7d0d0 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TracingChannelInterceptor.java @@ -305,7 +305,7 @@ public final class TracingChannelInterceptor extends ChannelInterceptorAdapter } Span consumerSpan = consumerSpan(message, channel, headers); // create and scope a span for the message processor - Span handle = this.tracer.nextSpan(consumerSpan); + Span handle = SleuthMessagingSpan.MESSAGING_SPAN.wrap(this.tracer.nextSpan(consumerSpan)); handle = this.messageSpanCustomizer.customizeHandle(handle, message, channel).start(); if (log.isDebugEnabled()) { log.debug("Created consumer span " + handle); diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/quartz/SleuthQuartzSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/quartz/SleuthQuartzSpan.java new file mode 100644 index 000000000..271d30afe --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/quartz/SleuthQuartzSpan.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.quartz; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthQuartzSpan implements DocumentedSpan { + + /** + * Span created when trigger is fired and then completed. + */ + QUARTZ_TRIGGER_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + }; + + enum Tags implements TagKey { + + /** + * Name of the trigger. + */ + TRIGGER { + @Override + public String getKey() { + return "quartz.trigger"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/quartz/TracingJobListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/quartz/TracingJobListener.java index 81b020901..524b75c8d 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/quartz/TracingJobListener.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/quartz/TracingJobListener.java @@ -26,6 +26,7 @@ import org.quartz.TriggerListener; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; import org.springframework.cloud.sleuth.propagation.Propagator; /** @@ -37,8 +38,6 @@ import org.springframework.cloud.sleuth.propagation.Propagator; */ public class TracingJobListener implements JobListener, TriggerListener { - static final String TRIGGER_TAG_KEY = "quartz.trigger"; - static final String CONTEXT_SPAN_KEY = Span.class.getName(); static final String CONTEXT_SPAN_IN_SCOPE_KEY = Tracer.SpanInScope.class.getName(); @@ -68,8 +67,9 @@ public class TracingJobListener implements JobListener, TriggerListener { @Override public void triggerFired(Trigger trigger, JobExecutionContext context) { Span nextSpan = propagator.extract(context.getMergedJobDataMap(), GETTER).start(); - Span span = nextSpan.name(context.getTrigger().getJobKey().toString()).tag(TRIGGER_TAG_KEY, - context.getTrigger().getKey().toString()); + AssertingSpan span = SleuthQuartzSpan.QUARTZ_TRIGGER_SPAN.wrap(nextSpan) + .tag(SleuthQuartzSpan.Tags.TRIGGER, context.getTrigger().getKey().toString()) + .name(context.getTrigger().getJobKey().toString()); context.put(CONTEXT_SPAN_KEY, span); context.put(CONTEXT_SPAN_IN_SCOPE_KEY, tracer.withSpan(span.start())); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/SleuthR2dbcSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/SleuthR2dbcSpan.java new file mode 100644 index 000000000..53d2759e6 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/SleuthR2dbcSpan.java @@ -0,0 +1,93 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.r2dbc; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.EventValue; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthR2dbcSpan implements DocumentedSpan { + + /** + * Span created on the Kafka consumer side. + */ + R2DBC_QUERY_SPAN { + @Override + public String getName() { + return "query"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + @Override + public String prefix() { + return "r2dbc."; + } + }; + + enum Tags implements TagKey { + + /** + * Name of the R2DBC connection. + */ + CONNECTION { + @Override + public String getKey() { + return "r2dbc.connection"; + } + }, + + /** + * Name of the R2DBC thread. + */ + THREAD { + @Override + public String getKey() { + return "r2dbc.thread"; + } + }, + + /** + * Name of the R2DBC query. + */ + QUERY { + @Override + public String getKey() { + return "r2dbc.query[%s]"; + } + }, + + } + + enum Events implements EventValue { + + /** + * Annotated before executing a method annotated with @ContinueSpan or @NewSpan. + */ + QUERY_RESULT { + @Override + public String getValue() { + return "r2dbc.query_result"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListener.java index 67d23c797..35a4f3a1b 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListener.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListener.java @@ -1,120 +1,149 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.r2dbc; - -import io.r2dbc.proxy.core.QueryExecutionInfo; -import io.r2dbc.proxy.core.QueryInfo; -import io.r2dbc.proxy.listener.ProxyExecutionListener; -import io.r2dbc.spi.ConnectionFactory; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.boot.autoconfigure.r2dbc.R2dbcProperties; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.util.StringUtils; - -/** - * Trace representation of a {@link ProxyExecutionListener}. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -public class TraceProxyExecutionListener implements ProxyExecutionListener { - - private static final Log log = LogFactory.getLog(TraceProxyExecutionListener.class); - - private final BeanFactory beanFactory; - - private final ConnectionFactory connectionFactory; - - private Tracer tracer; - - public TraceProxyExecutionListener(BeanFactory beanFactory, ConnectionFactory connectionFactory) { - this.beanFactory = beanFactory; - this.connectionFactory = connectionFactory; - } - - @Override - public void beforeQuery(QueryExecutionInfo executionInfo) { - if (tracer().currentSpan() == null) { - return; - } - String name = this.connectionFactory.getMetadata().getName(); - Span span = clientSpan(executionInfo, name); - if (log.isDebugEnabled()) { - log.debug("Created a new child span before query [" + span + "]"); - } - tagQueries(executionInfo, span); - executionInfo.getValueStore().put(Span.class, span); - } - - Span clientSpan(QueryExecutionInfo executionInfo, String name) { - R2dbcProperties r2dbcProperties = this.beanFactory.getBean(R2dbcProperties.class); - String url = r2dbcProperties.getUrl(); - Span.Builder builder = tracer().spanBuilder().kind(Span.Kind.CLIENT).name("query") - .remoteServiceName(name) - .tag("rd2bc.connection", name).tag("rd2bc.thread", executionInfo.getThreadName()); - if (StringUtils.hasText(url)) { - builder.remoteUrl(url); - } - return builder.start(); - } - - private void tagQueries(QueryExecutionInfo executionInfo, Span span) { - int i = 0; - for (QueryInfo queryInfo : executionInfo.getQueries()) { - span.tag("r2dbc.query[" + i + "]", queryInfo.getQuery()); - i = i + 1; - } - } - - @Override - public void afterQuery(QueryExecutionInfo executionInfo) { - Span span = executionInfo.getValueStore().get(Span.class, Span.class); - if (span != null) { - if (log.isDebugEnabled()) { - log.debug("Continued the child span in after query [" + span + "]"); - } - final Throwable throwable = executionInfo.getThrowable(); - if (throwable != null) { - span.error(throwable); - } - span.end(); - } - } - - @Override - public void eachQueryResult(QueryExecutionInfo executionInfo) { - Span span = executionInfo.getValueStore().get(Span.class, Span.class); - if (span != null) { - if (log.isDebugEnabled()) { - log.debug("Marking after query result for span [" + span + "]"); - } - span.event("r2dbc.query_result"); - } - } - - private Tracer tracer() { - if (this.tracer == null) { - this.tracer = this.beanFactory.getBean(Tracer.class); - } - return this.tracer; - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.r2dbc; + +import io.r2dbc.proxy.core.QueryExecutionInfo; +import io.r2dbc.proxy.core.QueryInfo; +import io.r2dbc.proxy.listener.ProxyExecutionListener; +import io.r2dbc.spi.ConnectionFactory; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcProperties; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; +import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; +import org.springframework.cloud.sleuth.internal.ContextUtil; +import org.springframework.util.StringUtils; + +/** + * Trace representation of a {@link ProxyExecutionListener}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceProxyExecutionListener implements ProxyExecutionListener { + + private static final Log log = LogFactory.getLog(TraceProxyExecutionListener.class); + + private final BeanFactory beanFactory; + + private final ConnectionFactory connectionFactory; + + private Tracer tracer; + + public TraceProxyExecutionListener(BeanFactory beanFactory, ConnectionFactory connectionFactory) { + this.beanFactory = beanFactory; + this.connectionFactory = connectionFactory; + } + + @Override + public void beforeQuery(QueryExecutionInfo executionInfo) { + if (isContextUnusable()) { + if (log.isDebugEnabled()) { + log.debug("Context is not ready - won't do anything"); + } + return; + } + else if (tracer().currentSpan() == null) { + return; + } + String name = this.connectionFactory.getMetadata().getName(); + AssertingSpan span = clientSpan(executionInfo, name); + if (log.isDebugEnabled()) { + log.debug("Created a new child span before query [" + span + "]"); + } + tagQueries(executionInfo, span); + executionInfo.getValueStore().put(Span.class, span); + } + + AssertingSpan clientSpan(QueryExecutionInfo executionInfo, String name) { + R2dbcProperties r2dbcProperties = this.beanFactory.getBean(R2dbcProperties.class); + String url = r2dbcProperties.getUrl(); + // @formatter:off + AssertingSpanBuilder builder = AssertingSpanBuilder.of(SleuthR2dbcSpan.R2DBC_QUERY_SPAN, tracer.spanBuilder()) + .kind(Span.Kind.CLIENT) + .name(SleuthR2dbcSpan.R2DBC_QUERY_SPAN.getName()).remoteServiceName(name) + .tag(SleuthR2dbcSpan.Tags.CONNECTION, name) + .tag(SleuthR2dbcSpan.Tags.THREAD, executionInfo.getThreadName()); + // @formatter:on + if (StringUtils.hasText(url)) { + builder.remoteUrl(url); + } + return builder.start(); + } + + private void tagQueries(QueryExecutionInfo executionInfo, AssertingSpan span) { + int i = 0; + for (QueryInfo queryInfo : executionInfo.getQueries()) { + span.tag(String.format(SleuthR2dbcSpan.Tags.QUERY.getKey(), i), queryInfo.getQuery()); + i = i + 1; + } + } + + @Override + public void afterQuery(QueryExecutionInfo executionInfo) { + if (isContextUnusable()) { + if (log.isDebugEnabled()) { + log.debug("Context is not ready - won't do anything"); + } + return; + } + Span span = executionInfo.getValueStore().get(Span.class, Span.class); + if (span != null) { + if (log.isDebugEnabled()) { + log.debug("Continued the child span in after query [" + span + "]"); + } + final Throwable throwable = executionInfo.getThrowable(); + if (throwable != null) { + span.error(throwable); + } + span.end(); + } + } + + @Override + public void eachQueryResult(QueryExecutionInfo executionInfo) { + if (isContextUnusable()) { + if (log.isDebugEnabled()) { + log.debug("Context is not ready - won't do anything"); + } + return; + } + Span span = executionInfo.getValueStore().get(Span.class, Span.class); + if (span != null) { + if (log.isDebugEnabled()) { + log.debug("Marking after query result for span [" + span + "]"); + } + SleuthR2dbcSpan.R2DBC_QUERY_SPAN.wrap(span).event(SleuthR2dbcSpan.Events.QUERY_RESULT); + } + } + + boolean isContextUnusable() { + return ContextUtil.isContextUnusable(this.beanFactory); + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/SleuthRSocketSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/SleuthRSocketSpan.java new file mode 100644 index 000000000..311c80407 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/SleuthRSocketSpan.java @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthRSocketSpan implements DocumentedSpan { + + /** + * Span created on the RSocket responder side. + */ + RSOCKET_RESPONDER_SPAN { + @Override + public String getName() { + return "%s"; + } + }, + + /** + * Span created on the RSocket responder side. + */ + RSOCKET_REQUESTER_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + @Override + public String prefix() { + return "rsocket."; + } + }; + + enum Tags implements TagKey { + + /** + * Name of the RSocket route. + */ + ROUTE { + @Override + public String getKey() { + return "rsocket.route"; + } + }, + + /** + * Name of the R2DBC thread. + */ + REQUEST_TYPE { + @Override + public String getKey() { + return "rsocket.request-type"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRequesterRSocketProxy.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRequesterRSocketProxy.java index 31d0b9a96..90e3d6ec1 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRequesterRSocketProxy.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRequesterRSocketProxy.java @@ -39,6 +39,7 @@ import reactor.util.context.ContextView; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.TraceContext; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; import org.springframework.cloud.sleuth.internal.EncodingUtils; import org.springframework.cloud.sleuth.propagation.Propagator; @@ -89,9 +90,10 @@ public class TracingRequesterRSocketProxy extends RSocketProxy { final RoutingMetadata routingMetadata = new RoutingMetadata(extracted); final Iterator iterator = routingMetadata.iterator(); String route = iterator.next(); - Span span = spanBuilder.kind(Span.Kind.PRODUCER).name(frameType.name() + " " + route).start(); - span.tag("rsocket.route", route); - span.tag("rsocket.request-type", frameType.name()); + Span span = AssertingSpanBuilder + .of(SleuthRSocketSpan.RSOCKET_REQUESTER_SPAN, spanBuilder.kind(Span.Kind.PRODUCER)) + .name(frameType.name() + " " + route).tag(SleuthRSocketSpan.Tags.ROUTE, route) + .tag(SleuthRSocketSpan.Tags.REQUEST_TYPE, frameType.name()).start(); if (log.isDebugEnabled()) { log.debug("Extracted result from context or thread local " + span); } @@ -156,7 +158,9 @@ public class TracingRequesterRSocketProxy extends RSocketProxy { final RoutingMetadata routingMetadata = new RoutingMetadata(CompositeMetadataUtils .extract(payload.sliceMetadata(), WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString())); final Iterator iterator = routingMetadata.iterator(); - Span span = spanBuilder.kind(Span.Kind.PRODUCER).name(iterator.next()).start(); + Span span = AssertingSpanBuilder + .of(SleuthRSocketSpan.RSOCKET_REQUESTER_SPAN, spanBuilder.kind(Span.Kind.PRODUCER)) + .name(iterator.next()).start(); if (log.isDebugEnabled()) { log.debug("Extracted result from context or thread local " + span); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java index b75a4aff9..25e7ace0b 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java @@ -39,6 +39,7 @@ import org.springframework.cloud.sleuth.ThreadLocalSpan; import org.springframework.cloud.sleuth.TraceContext; import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.WithThreadLocalSpan; +import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; import org.springframework.cloud.sleuth.internal.EncodingUtils; import org.springframework.cloud.sleuth.propagation.Propagator; @@ -140,7 +141,9 @@ public class TracingResponderRSocketProxy extends RSocketProxy implements WithTh final Iterator iterator = routingMetadata.iterator(); name = requestType.name() + " " + iterator.next(); } - return consumerSpanBuilder.kind(Span.Kind.CONSUMER).name(name).start(); + return AssertingSpanBuilder + .of(SleuthRSocketSpan.RSOCKET_RESPONDER_SPAN, consumerSpanBuilder.kind(Span.Kind.CONSUMER)).name(name) + .start(); } private Span.Builder consumerSpanBuilder(ByteBuf headers) { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rxjava/SleuthRxJavaSchedulersHook.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rxjava/SleuthRxJavaSchedulersHook.java index a2f226270..b4ff09d23 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rxjava/SleuthRxJavaSchedulersHook.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rxjava/SleuthRxJavaSchedulersHook.java @@ -113,8 +113,6 @@ public class SleuthRxJavaSchedulersHook extends RxJavaSchedulersHook { */ static class TraceAction implements Action0 { - private static final String THREAD_NAME_KEY = "thread"; - private final Action0 actual; private final Tracer tracer; @@ -149,8 +147,9 @@ public class SleuthRxJavaSchedulersHook extends RxJavaSchedulersHook { Span span = this.parent; boolean created = false; if (span == null) { - span = this.tracer.nextSpan().name(RXJAVA_COMPONENT).start(); - span.tag(THREAD_NAME_KEY, Thread.currentThread().getName()); + span = SleuthRxJavaSpan.RX_JAVA_TRACE_ACTION_SPAN.wrap(this.tracer.nextSpan()) + .name(SleuthRxJavaSpan.RX_JAVA_TRACE_ACTION_SPAN.getName()) + .tag(SleuthRxJavaSpan.Tags.THREAD, Thread.currentThread().getName()).start(); created = true; } try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rxjava/SleuthRxJavaSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rxjava/SleuthRxJavaSpan.java new file mode 100644 index 000000000..66d8b6b84 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rxjava/SleuthRxJavaSpan.java @@ -0,0 +1,62 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rxjava; + +import rx.functions.Action; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthRxJavaSpan implements DocumentedSpan { + + /** + * Span that wraps a Rx Java {@link Action}. + */ + RX_JAVA_TRACE_ACTION_SPAN { + @Override + public String getName() { + return "rxjava"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + }; + + /** + * Tags related to RX Java. + * + * @author Marcin Grzejszczak + * @since 3.0.3 + */ + enum Tags implements TagKey { + + /** + * Name of the thread. + */ + THREAD { + @Override + public String getKey() { + return "thread"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/scheduling/SleuthSchedulingSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/scheduling/SleuthSchedulingSpan.java new file mode 100644 index 000000000..983caa81d --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/scheduling/SleuthSchedulingSpan.java @@ -0,0 +1,66 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.scheduling; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.TagKey; +import org.springframework.scheduling.annotation.Scheduled; + +enum SleuthSchedulingSpan implements DocumentedSpan { + + /** + * Span that wraps a {@link Scheduled} annotated method. Either creates a new span or + * continues an existing one. + */ + SCHEDULED_ANNOTATION_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + }; + + enum Tags implements TagKey { + + /** + * Class name where a method got annotated with @Scheduled. + */ + CLASS { + @Override + public String getKey() { + return "class"; + } + }, + + /** + * Method name that got annotated with @Scheduled. + */ + METHOD { + @Override + public String getKey() { + return "method"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/scheduling/TraceSchedulingAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/scheduling/TraceSchedulingAspect.java index 16dcaff11..e7a261e01 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/scheduling/TraceSchedulingAspect.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/scheduling/TraceSchedulingAspect.java @@ -24,6 +24,7 @@ import org.aspectj.lang.annotation.Aspect; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; import org.springframework.cloud.sleuth.internal.SpanNameUtil; import org.springframework.lang.Nullable; @@ -43,10 +44,6 @@ import org.springframework.lang.Nullable; @Aspect public class TraceSchedulingAspect { - private static final String CLASS_KEY = "class"; - - private static final String METHOD_KEY = "method"; - private final Tracer tracer; @Nullable @@ -66,10 +63,10 @@ public class TraceSchedulingAspect { return pjp.proceed(); } String spanName = SpanNameUtil.toLowerHyphen(pjp.getSignature().getName()); - Span span = startOrContinueRenamedSpan(spanName); + AssertingSpan span = SleuthSchedulingSpan.SCHEDULED_ANNOTATION_SPAN.wrap(startOrContinueSpan()).name(spanName); try (Tracer.SpanInScope ws = this.tracer.withSpan(span.start())) { - span.tag(CLASS_KEY, pjp.getTarget().getClass().getSimpleName()); - span.tag(METHOD_KEY, pjp.getSignature().getName()); + span.tag(SleuthSchedulingSpan.Tags.CLASS, pjp.getTarget().getClass().getSimpleName()) + .tag(SleuthSchedulingSpan.Tags.METHOD, pjp.getSignature().getName()); return pjp.proceed(); } catch (Throwable ex) { @@ -81,12 +78,12 @@ public class TraceSchedulingAspect { } } - private Span startOrContinueRenamedSpan(String spanName) { + private Span startOrContinueSpan() { Span currentSpan = this.tracer.currentSpan(); if (currentSpan != null) { - return currentSpan.name(spanName); + return currentSpan; } - return this.tracer.nextSpan().name(spanName); + return SleuthSchedulingSpan.SCHEDULED_ANNOTATION_SPAN.wrap(this.tracer.nextSpan()); } } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/SleuthTaskSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/SleuthTaskSpan.java new file mode 100644 index 000000000..aa801a66c --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/SleuthTaskSpan.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.task; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; + +enum SleuthTaskSpan implements DocumentedSpan { + + /** + * Span created when a task runner is executed. + */ + TASK_RUNNER_SPAN { + @Override + public String getName() { + return "%s"; + } + }, + + /** + * Span created within the lifecycle of a task. + */ + TASK_EXECUTION_LISTENER_SPAN { + @Override + public String getName() { + return "%s"; + } + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceApplicationRunner.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceApplicationRunner.java index 4e3f6c77d..3b7b173c2 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceApplicationRunner.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceApplicationRunner.java @@ -46,7 +46,7 @@ public class TraceApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { - Span span = tracer().nextSpan().name(this.beanName); + Span span = SleuthTaskSpan.TASK_RUNNER_SPAN.wrap(tracer().nextSpan()).name(this.beanName); try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { this.delegate.run(args); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceCommandLineRunner.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceCommandLineRunner.java index 70b9ee320..741d25ab8 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceCommandLineRunner.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceCommandLineRunner.java @@ -45,7 +45,7 @@ public class TraceCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { - Span span = tracer().nextSpan().name(this.beanName); + Span span = SleuthTaskSpan.TASK_RUNNER_SPAN.wrap(tracer().nextSpan()).name(this.beanName); try (Tracer.SpanInScope spanInScope = tracer().withSpan(span.start())) { this.delegate.run(args); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceTaskExecutionListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceTaskExecutionListener.java index d50d7ab00..c6b82724c 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceTaskExecutionListener.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceTaskExecutionListener.java @@ -51,7 +51,8 @@ public class TraceTaskExecutionListener implements TaskExecutionListener, Ordere @Override public void onTaskStartup(TaskExecution taskExecution) { - Span span = this.tracer.nextSpan().name(this.projectName).start(); + Span span = SleuthTaskSpan.TASK_EXECUTION_LISTENER_SPAN.wrap(this.tracer.nextSpan()).name(this.projectName) + .start(); this.threadLocalSpan.set(span); if (log.isDebugEnabled()) { log.debug("Put the span [" + span + "] to thread local"); diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/SleuthTxSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/SleuthTxSpan.java new file mode 100644 index 000000000..8a0c35e46 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/SleuthTxSpan.java @@ -0,0 +1,134 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.tx; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.EventValue; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthTxSpan implements DocumentedSpan { + + /** + * Span created when there was no previous transaction. If there was one, we will + * continue it unless propagation is required. + */ + TX_SPAN { + @Override + public String getName() { + return "tx"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + @Override + public String prefix() { + return "tx."; + } + }; + + enum Tags implements TagKey { + + /** + * Name of the TransactionManager. + */ + TRANSACTION_MANAGER { + @Override + public String getKey() { + return "tx.transaction-manager"; + } + }, + + /** + * Whether the transaction is read-only. + */ + READ_ONLY { + @Override + public String getKey() { + return "tx.read-only"; + } + }, + + /** + * Transaction propagation level. + */ + PROPAGATION_LEVEL { + @Override + public String getKey() { + return "tx.propagation-level"; + } + }, + + /** + * Transaction isolation level. + */ + ISOLATION_LEVEL { + @Override + public String getKey() { + return "tx.isolation-level"; + } + }, + + /** + * Transaction timeout. + */ + TIMEOUT { + @Override + public String getKey() { + return "tx.timeout"; + } + }, + + /** + * Transaction name. + */ + NAME { + @Override + public String getKey() { + return "tx.name"; + } + }, + + } + + enum Events implements EventValue { + + /** + * Annotated after transaction commit. + */ + COMMIT { + @Override + public String getValue() { + return "tx.commit"; + } + }, + + /** + * Annotated after transaction rollback. + */ + ROLLBACK { + @Override + public String getValue() { + return "tx.rollback"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManager.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManager.java index 28a43cfe1..c12f605c8 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManager.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManager.java @@ -1,175 +1,175 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.tx; - -import javax.annotation.PostConstruct; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.SpanAndScope; -import org.springframework.cloud.sleuth.ThreadLocalSpan; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionException; -import org.springframework.transaction.TransactionStatus; - -/** - * A trace representation of a {@link PlatformTransactionManager}. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -public class TracePlatformTransactionManager implements PlatformTransactionManager { - - private static final Log log = LogFactory.getLog(TracePlatformTransactionManager.class); - - private final PlatformTransactionManager delegate; - - private final BeanFactory beanFactory; - - private Tracer tracer; - - volatile ThreadLocalSpan threadLocalSpan; - - public TracePlatformTransactionManager(PlatformTransactionManager delegate, BeanFactory beanFactory) { - this.delegate = delegate; - this.beanFactory = beanFactory; - } - - @PostConstruct - void initialize() { - if (this.threadLocalSpan == null) { - this.threadLocalSpan = new ThreadLocalSpan(tracer()); - } - } - - @Override - public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { - initialize(); - SpanAndScope spanAndScope = this.threadLocalSpan.get(); - Span currentSpan = spanAndScope != null ? spanAndScope.getSpan() : tracer().currentSpan(); - Span span = fallbackSpan(); - try { - TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults()); - TransactionStatus status = this.delegate.getTransaction(definition); - span = taggedSpan(currentSpan, span, def, status); - return status; - } - catch (Exception e) { - if (log.isDebugEnabled()) { - log.debug( - "Exception occurred while trying to get a transaction, will mark the span with error and report it"); - } - span.error(e); - span.end(); - throw e; - } - } - - Span fallbackSpan() { - return tracer().nextSpan().name("tx").start(); - } - - private Span taggedSpan(Span currentSpan, Span span, TransactionDefinition def, TransactionStatus status) { - if (status.isNewTransaction() || currentSpan == null) { - if (log.isDebugEnabled()) { - log.debug("Creating new span cause a new transaction is started"); - } - TracePlatformTransactionManagerTags.tag(span, def, this.delegate.getClass()); - } - else { - span = currentSpan; - } - this.threadLocalSpan.set(span); - return span; - } - - @Override - public void commit(TransactionStatus status) throws TransactionException { - SpanAndScope spanAndScope = this.threadLocalSpan.get(); - if (spanAndScope == null) { - if (log.isDebugEnabled()) { - log.debug("No span and scope found - this shouldn't happen, sth is wrong"); - } - this.delegate.commit(status); - return; - } - Exception ex = null; - Span span = spanAndScope.getSpan(); - try { - if (log.isDebugEnabled()) { - log.debug("Wrapping commit"); - } - this.delegate.commit(status); - } - catch (Exception e) { - ex = e; - span.error(e); - throw e; - } - finally { - span.event("tx.commit"); - span.end(); - if (ex == null) { - if (log.isDebugEnabled()) { - log.debug("No exception was found - will clear thread local span"); - } - this.threadLocalSpan.remove(); - } - } - } - - @Override - public void rollback(TransactionStatus status) throws TransactionException { - SpanAndScope spanAndScope = this.threadLocalSpan.get(); - if (spanAndScope == null) { - if (log.isDebugEnabled()) { - log.debug("No span and scope found - this shouldn't happen, sth is wrong"); - } - this.delegate.rollback(status); - return; - } - Span span = spanAndScope.getSpan(); - try { - if (log.isDebugEnabled()) { - log.debug("Wrapping rollback"); - } - this.delegate.rollback(status); - } - catch (Exception e) { - span.error(e); - throw e; - } - finally { - span.event("tx.rollback"); - span.end(); - this.threadLocalSpan.remove(); - } - } - - private Tracer tracer() { - if (this.tracer == null) { - this.tracer = this.beanFactory.getBean(Tracer.class); - } - return this.tracer; - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.tx; + +import javax.annotation.PostConstruct; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.ThreadLocalSpan; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionStatus; + +/** + * A trace representation of a {@link PlatformTransactionManager}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TracePlatformTransactionManager implements PlatformTransactionManager { + + private static final Log log = LogFactory.getLog(TracePlatformTransactionManager.class); + + private final PlatformTransactionManager delegate; + + private final BeanFactory beanFactory; + + private Tracer tracer; + + volatile ThreadLocalSpan threadLocalSpan; + + public TracePlatformTransactionManager(PlatformTransactionManager delegate, BeanFactory beanFactory) { + this.delegate = delegate; + this.beanFactory = beanFactory; + } + + @PostConstruct + void initialize() { + if (this.threadLocalSpan == null) { + this.threadLocalSpan = new ThreadLocalSpan(tracer()); + } + } + + @Override + public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { + initialize(); + SpanAndScope spanAndScope = this.threadLocalSpan.get(); + Span currentSpan = spanAndScope != null ? spanAndScope.getSpan() : tracer().currentSpan(); + Span span = fallbackSpan(); + try { + TransactionDefinition def = (definition != null ? definition : TransactionDefinition.withDefaults()); + TransactionStatus status = this.delegate.getTransaction(definition); + span = taggedSpan(currentSpan, span, def, status); + return status; + } + catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug( + "Exception occurred while trying to get a transaction, will mark the span with error and report it"); + } + span.error(e); + span.end(); + throw e; + } + } + + Span fallbackSpan() { + return SleuthTxSpan.TX_SPAN.wrap(tracer().nextSpan()).name(SleuthTxSpan.TX_SPAN.getName()).start(); + } + + private Span taggedSpan(Span currentSpan, Span span, TransactionDefinition def, TransactionStatus status) { + if (status.isNewTransaction() || currentSpan == null) { + if (log.isDebugEnabled()) { + log.debug("Creating new span cause a new transaction is started"); + } + TracePlatformTransactionManagerTags.tag(span, def, this.delegate.getClass()); + } + else { + span = currentSpan; + } + this.threadLocalSpan.set(span); + return span; + } + + @Override + public void commit(TransactionStatus status) throws TransactionException { + SpanAndScope spanAndScope = this.threadLocalSpan.get(); + if (spanAndScope == null) { + if (log.isDebugEnabled()) { + log.debug("No span and scope found - this shouldn't happen, sth is wrong"); + } + this.delegate.commit(status); + return; + } + Exception ex = null; + Span span = spanAndScope.getSpan(); + try { + if (log.isDebugEnabled()) { + log.debug("Wrapping commit"); + } + this.delegate.commit(status); + } + catch (Exception e) { + ex = e; + span.error(e); + throw e; + } + finally { + SleuthTxSpan.TX_SPAN.wrap(span).event(SleuthTxSpan.Events.COMMIT); + span.end(); + if (ex == null) { + if (log.isDebugEnabled()) { + log.debug("No exception was found - will clear thread local span"); + } + this.threadLocalSpan.remove(); + } + } + } + + @Override + public void rollback(TransactionStatus status) throws TransactionException { + SpanAndScope spanAndScope = this.threadLocalSpan.get(); + if (spanAndScope == null) { + if (log.isDebugEnabled()) { + log.debug("No span and scope found - this shouldn't happen, sth is wrong"); + } + this.delegate.rollback(status); + return; + } + Span span = spanAndScope.getSpan(); + try { + if (log.isDebugEnabled()) { + log.debug("Wrapping rollback"); + } + this.delegate.rollback(status); + } + catch (Exception e) { + span.error(e); + throw e; + } + finally { + SleuthTxSpan.TX_SPAN.wrap(span).event(SleuthTxSpan.Events.ROLLBACK); + span.end(); + this.threadLocalSpan.remove(); + } + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTags.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTags.java index 664c64b86..2629e1660 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTags.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTags.java @@ -17,6 +17,7 @@ package org.springframework.cloud.sleuth.instrument.tx; import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.docs.AssertingSpan; import org.springframework.transaction.TransactionDefinition; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -28,16 +29,19 @@ final class TracePlatformTransactionManagerTags { } static void tag(Span span, TransactionDefinition def, Class transactionManagerClass) { - span.tag("tx.transaction-manager", ClassUtils.getQualifiedName(transactionManagerClass)); - span.tag("tx.read-only", String.valueOf(def.isReadOnly())); - span.tag("tx.propagation-level", propagationLevel(def)); - span.tag("tx.isolation-level", isolationLevel(def)); - if (def.getTimeout() > 0) { - span.tag("tx.timeout", String.valueOf(def.getTimeout())); - } - if (StringUtils.hasText(def.getName())) { - span.tag("tx.name", def.getName()); - } + // @formatter:off + AssertingSpan assertingSpan = SleuthTxSpan.TX_SPAN.wrap(span) + .tag(SleuthTxSpan.Tags.TRANSACTION_MANAGER, ClassUtils.getQualifiedName(transactionManagerClass)) + .tag(SleuthTxSpan.Tags.READ_ONLY, String.valueOf(def.isReadOnly())) + .tag(SleuthTxSpan.Tags.PROPAGATION_LEVEL, propagationLevel(def)) + .tag(SleuthTxSpan.Tags.ISOLATION_LEVEL, isolationLevel(def)); + if (def.getTimeout() > 0) { + assertingSpan.tag(SleuthTxSpan.Tags.TIMEOUT, String.valueOf(def.getTimeout())); + } + if (StringUtils.hasText(def.getName())) { + assertingSpan.tag(SleuthTxSpan.Tags.NAME, def.getName()); + } + // @formatter:on } private static String propagationLevel(TransactionDefinition def) { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java index a9f79a6f5..b90e24dee 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java @@ -1,187 +1,188 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.tx; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Mono; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.SpanAndScope; -import org.springframework.cloud.sleuth.TraceContext; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.transaction.ReactiveTransaction; -import org.springframework.transaction.ReactiveTransactionManager; -import org.springframework.transaction.TransactionDefinition; -import org.springframework.transaction.TransactionException; - -/** - * A trace representation of a {@link ReactiveTransactionManager}. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -public class TraceReactiveTransactionManager implements ReactiveTransactionManager { - - private static final Log log = LogFactory.getLog(TraceReactiveTransactionManager.class); - - private final ReactiveTransactionManager delegate; - - private final BeanFactory beanFactory; - - private Tracer tracer; - - private CurrentTraceContext currentTraceContext; - - public TraceReactiveTransactionManager(ReactiveTransactionManager delegate, BeanFactory beanFactory) { - this.delegate = delegate; - this.beanFactory = beanFactory; - } - - private Tracer tracer() { - if (this.tracer == null) { - this.tracer = this.beanFactory.getBean(Tracer.class); - } - return this.tracer; - } - - private CurrentTraceContext currentTraceContext() { - if (this.currentTraceContext == null) { - this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); - } - return this.currentTraceContext; - } - - @Override - public Mono getReactiveTransaction(TransactionDefinition definition) - throws TransactionException { - return Mono.deferContextual(contextView -> this.delegate.getReactiveTransaction(definition).map(tx -> { - Span span = span(contextView); - if (tx.isNewTransaction() || span == null) { - if (span == null) { - span = tracer().nextSpan().name("tx").start(); - } - else { - span = tracer().nextSpan(span).name("tx").start(); - } - TracePlatformTransactionManagerTags.tag(span, definition, this.delegate.getClass()); - } - Tracer.SpanInScope withSpan = tracer().withSpan(span); - SpanAndScope spanAndScope = new SpanAndScope(span, withSpan); - return new TraceReactiveTransaction(tx, spanAndScope); - })); - } - - private Span span(reactor.util.context.ContextView contextView) { - Span span = contextView.getOrDefault(Span.class, null); - if (span == null) { - TraceContext traceContext = contextView.getOrDefault(TraceContext.class, null); - if (traceContext == null) { - Span currentSpan = tracer().currentSpan(); - if (log.isDebugEnabled()) { - log.debug("There's no Span or TraceContext in the reactor context. Current span is [" + currentSpan - + "]"); - } - span = currentSpan; - } - else { - span = spanFromContext(traceContext); - } - } - return span; - } - - private Span spanFromContext(TraceContext traceContext) { - try (CurrentTraceContext.Scope scope = currentTraceContext().maybeScope(traceContext)) { - return tracer().currentSpan(); - } - } - - @Override - public Mono commit(ReactiveTransaction transaction) throws TransactionException { - if (!(transaction instanceof TraceReactiveTransaction)) { - return this.delegate.commit(transaction); - } - TraceReactiveTransaction reactiveTransaction = (TraceReactiveTransaction) transaction; - SpanAndScope spanAndScope = reactiveTransaction.spanAndScope; - Span span = spanAndScope.getSpan(); - Tracer.SpanInScope scope = spanAndScope.getScope(); - return this.delegate.commit(reactiveTransaction.delegate) - // TODO: Fix me when this is resolved in Reactor - // .doOnSubscribe(__ -> scope.close()) - .doOnError(span::error).doOnSuccess(signalType -> { - span.end(); - scope.close(); - }); - } - - @Override - public Mono rollback(ReactiveTransaction transaction) throws TransactionException { - if (!(transaction instanceof TraceReactiveTransaction)) { - return this.delegate.rollback(transaction); - } - TraceReactiveTransaction reactiveTransaction = (TraceReactiveTransaction) transaction; - SpanAndScope spanAndScope = reactiveTransaction.spanAndScope; - Span span = spanAndScope.getSpan(); - Tracer.SpanInScope scope = spanAndScope.getScope(); - return this.delegate.rollback(reactiveTransaction.delegate) - // TODO: Fix me when this is resolved in Reactor - // .doOnSubscribe(__ -> scope.close()) - .doOnError(span::error).doFinally(signalType -> { - span.end(); - if (scope != null) { - scope.close(); - } - }); - } - - static class TraceReactiveTransaction implements ReactiveTransaction { - - final ReactiveTransaction delegate; - - final SpanAndScope spanAndScope; - - TraceReactiveTransaction(ReactiveTransaction delegate, SpanAndScope spanAndScope) { - this.delegate = delegate; - this.spanAndScope = spanAndScope; - } - - @Override - public boolean isNewTransaction() { - return this.delegate.isNewTransaction(); - } - - @Override - public void setRollbackOnly() { - this.delegate.setRollbackOnly(); - } - - @Override - public boolean isRollbackOnly() { - return this.delegate.isRollbackOnly(); - } - - @Override - public boolean isCompleted() { - return this.delegate.isCompleted(); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.tx; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.transaction.ReactiveTransaction; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; + +/** + * A trace representation of a {@link ReactiveTransactionManager}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceReactiveTransactionManager implements ReactiveTransactionManager { + + private static final Log log = LogFactory.getLog(TraceReactiveTransactionManager.class); + + private final ReactiveTransactionManager delegate; + + private final BeanFactory beanFactory; + + private Tracer tracer; + + private CurrentTraceContext currentTraceContext; + + public TraceReactiveTransactionManager(ReactiveTransactionManager delegate, BeanFactory beanFactory) { + this.delegate = delegate; + this.beanFactory = beanFactory; + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + + private CurrentTraceContext currentTraceContext() { + if (this.currentTraceContext == null) { + this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); + } + return this.currentTraceContext; + } + + @Override + public Mono getReactiveTransaction(TransactionDefinition definition) + throws TransactionException { + return Mono.deferContextual(contextView -> this.delegate.getReactiveTransaction(definition).map(tx -> { + Span span = SleuthTxSpan.TX_SPAN.wrap(span(contextView)); + if (tx.isNewTransaction() || span == null) { + if (span == null) { + span = SleuthTxSpan.TX_SPAN.wrap(tracer().nextSpan()).name(SleuthTxSpan.TX_SPAN.getName()).start(); + } + else { + span = SleuthTxSpan.TX_SPAN.wrap(tracer().nextSpan(span)).name(SleuthTxSpan.TX_SPAN.getName()) + .start(); + } + TracePlatformTransactionManagerTags.tag(span, definition, this.delegate.getClass()); + } + Tracer.SpanInScope withSpan = tracer().withSpan(span); + SpanAndScope spanAndScope = new SpanAndScope(span, withSpan); + return new TraceReactiveTransaction(tx, spanAndScope); + })); + } + + private Span span(reactor.util.context.ContextView contextView) { + Span span = contextView.getOrDefault(Span.class, null); + if (span == null) { + TraceContext traceContext = contextView.getOrDefault(TraceContext.class, null); + if (traceContext == null) { + Span currentSpan = tracer().currentSpan(); + if (log.isDebugEnabled()) { + log.debug("There's no Span or TraceContext in the reactor context. Current span is [" + currentSpan + + "]"); + } + span = currentSpan; + } + else { + span = spanFromContext(traceContext); + } + } + return span; + } + + private Span spanFromContext(TraceContext traceContext) { + try (CurrentTraceContext.Scope scope = currentTraceContext().maybeScope(traceContext)) { + return SleuthTxSpan.TX_SPAN.wrap(tracer().currentSpan()).start(); + } + } + + @Override + public Mono commit(ReactiveTransaction transaction) throws TransactionException { + if (!(transaction instanceof TraceReactiveTransaction)) { + return this.delegate.commit(transaction); + } + TraceReactiveTransaction reactiveTransaction = (TraceReactiveTransaction) transaction; + SpanAndScope spanAndScope = reactiveTransaction.spanAndScope; + Span span = spanAndScope.getSpan(); + Tracer.SpanInScope scope = spanAndScope.getScope(); + return this.delegate.commit(reactiveTransaction.delegate) + // TODO: Fix me when this is resolved in Reactor + // .doOnSubscribe(__ -> scope.close()) + .doOnError(span::error).doOnSuccess(signalType -> { + span.end(); + scope.close(); + }); + } + + @Override + public Mono rollback(ReactiveTransaction transaction) throws TransactionException { + if (!(transaction instanceof TraceReactiveTransaction)) { + return this.delegate.rollback(transaction); + } + TraceReactiveTransaction reactiveTransaction = (TraceReactiveTransaction) transaction; + SpanAndScope spanAndScope = reactiveTransaction.spanAndScope; + Span span = spanAndScope.getSpan(); + Tracer.SpanInScope scope = spanAndScope.getScope(); + return this.delegate.rollback(reactiveTransaction.delegate) + // TODO: Fix me when this is resolved in Reactor + // .doOnSubscribe(__ -> scope.close()) + .doOnError(span::error).doFinally(signalType -> { + span.end(); + if (scope != null) { + scope.close(); + } + }); + } + + static class TraceReactiveTransaction implements ReactiveTransaction { + + final ReactiveTransaction delegate; + + final SpanAndScope spanAndScope; + + TraceReactiveTransaction(ReactiveTransaction delegate, SpanAndScope spanAndScope) { + this.delegate = delegate; + this.spanAndScope = spanAndScope; + } + + @Override + public boolean isNewTransaction() { + return this.delegate.isNewTransaction(); + } + + @Override + public void setRollbackOnly() { + this.delegate.setRollbackOnly(); + } + + @Override + public boolean isRollbackOnly() { + return this.delegate.isRollbackOnly(); + } + + @Override + public boolean isCompleted() { + return this.delegate.isCompleted(); + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/SleuthWebSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/SleuthWebSpan.java new file mode 100644 index 000000000..a46a8f9e7 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/SleuthWebSpan.java @@ -0,0 +1,81 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.web; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthWebSpan implements DocumentedSpan { + + /** + * Span around a WebFilter. Will continue the current span or create a new one and tag + * it + */ + WEB_FILTER_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + }; + + /** + * Tags related to web. + * + * @author Marcin Grzejszczak + * @since 3.0.3 + */ + enum Tags implements TagKey { + + /** + * Name of the class that is processing the request. + */ + CLASS { + @Override + public String getKey() { + return "mvc.controller.class"; + } + }, + + /** + * Name of the method that is processing the request. + */ + METHOD { + @Override + public String getKey() { + return "mvc.controller.method"; + } + }, + + /** + * Response status code. + */ + RESPONSE_STATUS_CODE { + @Override + public String getKey() { + return "http.status_code"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceWebFilter.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceWebFilter.java index c2ed27cc8..df64b85b7 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceWebFilter.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/TraceWebFilter.java @@ -33,6 +33,7 @@ import org.springframework.cloud.sleuth.CurrentTraceContext; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.TraceContext; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; import org.springframework.cloud.sleuth.http.HttpServerHandler; import org.springframework.cloud.sleuth.http.HttpServerRequest; import org.springframework.cloud.sleuth.http.HttpServerResponse; @@ -62,13 +63,8 @@ public class TraceWebFilter implements WebFilter, Ordered, ApplicationContextAwa // Remember that this can be used in other packages protected static final String TRACE_REQUEST_ATTR = Span.class.getName(); - static final String MVC_CONTROLLER_CLASS_KEY = "mvc.controller.class"; - static final String MVC_CONTROLLER_METHOD_KEY = "mvc.controller.method"; - private static final Log log = LogFactory.getLog(TraceWebFilter.class); - private static final String STATUS_CODE_KEY = "http.status_code"; - private static final String TRACE_SPAN_WITHOUT_PARENT = TraceWebFilter.class.getName() + ".SPAN_WITH_NO_PARENT"; private final Tracer tracer; @@ -203,6 +199,7 @@ public class TraceWebFilter implements WebFilter, Ordered, ApplicationContextAwa private Span findOrCreateSpan(Context c) { Span span; + AssertingSpan assertingSpan = null; if (c.hasKey(Span.class)) { Span parent = c.get(Span.class); try (Tracer.SpanInScope spanInScope = this.tracer.withSpan(parent)) { @@ -231,9 +228,13 @@ public class TraceWebFilter implements WebFilter, Ordered, ApplicationContextAwa else if (log.isDebugEnabled()) { log.debug("Found tracer specific span in reactor context [" + span + "]"); } - this.exchange.getAttributes().put(TRACE_REQUEST_ATTR, span); + assertingSpan = SleuthWebSpan.WEB_FILTER_SPAN.wrap(span); + this.exchange.getAttributes().put(TRACE_REQUEST_ATTR, assertingSpan); } - return span; + if (assertingSpan == null) { + assertingSpan = SleuthWebSpan.WEB_FILTER_SPAN.wrap(span); + } + return assertingSpan; } static final class WebFilterTraceSubscriber implements CoreSubscriber { @@ -302,7 +303,7 @@ public class TraceWebFilter implements WebFilter, Ordered, ApplicationContextAwa private void addClassMethodTag(Object handler, Span span) { if (handler instanceof HandlerMethod) { String methodName = ((HandlerMethod) handler).getMethod().getName(); - span.tag(MVC_CONTROLLER_METHOD_KEY, methodName); + SleuthWebSpan.WEB_FILTER_SPAN.wrap(span).tag(SleuthWebSpan.Tags.METHOD, methodName); if (log.isDebugEnabled()) { log.debug("Adding a method tag with value [" + methodName + "] to a span " + span); } @@ -323,13 +324,14 @@ public class TraceWebFilter implements WebFilter, Ordered, ApplicationContextAwa if (log.isDebugEnabled()) { log.debug("Adding a class tag with value [" + className + "] to a span " + span); } - span.tag(MVC_CONTROLLER_CLASS_KEY, className); + SleuthWebSpan.WEB_FILTER_SPAN.wrap(span).tag(SleuthWebSpan.Tags.CLASS, className); } private void addResponseTagsForSpanWithoutParent(ServerWebExchange exchange, ServerHttpResponse response, Span span) { if (spanWithoutParent(exchange) && response.getStatusCode() != null && span != null) { - span.tag(STATUS_CODE_KEY, String.valueOf(response.getStatusCode().value())); + SleuthWebSpan.WEB_FILTER_SPAN.wrap(span).tag(SleuthWebSpan.Tags.CLASS, + String.valueOf(response.getStatusCode().value())); } } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/mvc/HandlerParser.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/mvc/HandlerParser.java index 2ee9cb929..59e3cdbd3 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/mvc/HandlerParser.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/mvc/HandlerParser.java @@ -20,6 +20,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.cloud.sleuth.SpanCustomizer; +import org.springframework.cloud.sleuth.docs.AssertingSpanCustomizer; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; @@ -43,10 +44,12 @@ public class HandlerParser { }; /** Simple class name that processed the request. ex BookController */ - public static final String CONTROLLER_CLASS = "mvc.controller.class"; + // TODO: Remove me + public static final String CONTROLLER_CLASS = SleuthMvcSpan.Tags.CLASS.getKey(); /** Method name that processed the request. ex listOfBooks */ - public static final String CONTROLLER_METHOD = "mvc.controller.method"; + // TODO: Remove me + public static final String CONTROLLER_METHOD = SleuthMvcSpan.Tags.METHOD.getKey(); /** * Invoked prior to request invocation during @@ -60,13 +63,14 @@ public class HandlerParser { * @param customizer span customizer */ protected void preHandle(HttpServletRequest request, Object handler, SpanCustomizer customizer) { + AssertingSpanCustomizer span = SleuthMvcSpan.MVC_HANDLER_INTERCEPTOR_SPAN.wrap(customizer); if (WebMvcRuntime.get().isHandlerMethod(handler)) { HandlerMethod handlerMethod = ((HandlerMethod) handler); - customizer.tag(CONTROLLER_CLASS, handlerMethod.getBeanType().getSimpleName()); - customizer.tag(CONTROLLER_METHOD, handlerMethod.getMethod().getName()); + span.tag(SleuthMvcSpan.Tags.CLASS, handlerMethod.getBeanType().getSimpleName()); + span.tag(SleuthMvcSpan.Tags.METHOD, handlerMethod.getMethod().getName()); } else { - customizer.tag(CONTROLLER_CLASS, handler.getClass().getSimpleName()); + span.tag(SleuthMvcSpan.Tags.CLASS, handler.getClass().getSimpleName()); } } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/mvc/SleuthMvcSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/mvc/SleuthMvcSpan.java new file mode 100644 index 000000000..8097536a3 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/mvc/SleuthMvcSpan.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.web.mvc; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthMvcSpan implements DocumentedSpan { + + /** + * Span around a HandlerInterceptor. Will continue the current span and tag it + */ + MVC_HANDLER_INTERCEPTOR_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + }; + + enum Tags implements TagKey { + + /** + * Class name where a method got annotated with @Scheduled. + */ + CLASS { + @Override + public String getKey() { + return "mvc.controller.class"; + } + }, + + /** + * Method name that got annotated with @Scheduled. + */ + METHOD { + @Override + public String getKey() { + return "mvc.controller.method"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListenerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListenerTests.java index 1abf353e9..80100381d 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListenerTests.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListenerTests.java @@ -1,143 +1,149 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.r2dbc; - -import java.util.concurrent.atomic.AtomicReference; - -import io.r2dbc.proxy.core.QueryExecutionInfo; -import io.r2dbc.proxy.core.ValueStore; -import io.r2dbc.proxy.test.MockQueryExecutionInfo; -import io.r2dbc.spi.Connection; -import io.r2dbc.spi.ConnectionFactory; -import io.r2dbc.spi.ConnectionFactoryMetadata; -import org.junit.jupiter.api.Test; -import org.reactivestreams.Publisher; - -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.support.StaticListableBeanFactory; -import org.springframework.boot.autoconfigure.r2dbc.R2dbcProperties; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.tracer.SimpleSpan; -import org.springframework.cloud.sleuth.tracer.SimpleTracer; - -import static org.assertj.core.api.BDDAssertions.then; - -class TraceProxyExecutionListenerTests { - - SimpleTracer simpleTracer = new SimpleTracer(); - - ConnectionFactory connectionFactory = connectionFactory(); - - TraceProxyExecutionListener listener = new TraceProxyExecutionListener(beanFactory(), connectionFactory); - - @Test - void should_do_nothing_on_before_query_when_there_was_no_previous_span() { - MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); - - listener.beforeQuery(queryExecutionInfo); - - then(this.simpleTracer.spans).isEmpty(); - then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); - } - - @Test - void should_do_nothing_on_after_query_when_there_was_no_previous_span() { - MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); - - listener.afterQuery(queryExecutionInfo); - - then(this.simpleTracer.spans).isEmpty(); - then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); - } - - @Test - void should_do_nothing_on_each_query_when_there_was_no_previous_span() { - MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); - - listener.eachQueryResult(queryExecutionInfo); - - then(this.simpleTracer.spans).isEmpty(); - then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); - } - - @Test - void should_put_a_child_span_in_value_store_when_a_span_was_already_in_context() { - MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); - this.simpleTracer.nextSpan().start(); - AtomicReference clientSpan = new AtomicReference<>(); - listener = new TraceProxyExecutionListener(beanFactory(), connectionFactory) { - @Override - Span clientSpan(QueryExecutionInfo executionInfo, String name) { - Span span = super.clientSpan(executionInfo, name); - clientSpan.set(span); - return span; - } - }; - - listener.beforeQuery(queryExecutionInfo); - - then(queryExecutionInfo.getValueStore().get(Span.class)).isSameAs(clientSpan.get()); - } - - @Test - void should_annotate_a_span_on_query_result() { - SimpleSpan span = new SimpleSpan(); - ValueStore valueStore = ValueStore.create(); - valueStore.put(Span.class, span); - MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.builder().valueStore(valueStore).build(); - - listener.eachQueryResult(queryExecutionInfo); - - then(span.events).isNotEmpty(); - } - - @Test - void should_end_span_on_after_query() { - SimpleSpan span = new SimpleSpan().start(); - ValueStore valueStore = ValueStore.create(); - valueStore.put(Span.class, span); - MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.builder().throwable(new RuntimeException()) - .valueStore(valueStore).build(); - - listener.afterQuery(queryExecutionInfo); - - then(span.throwable).isNotNull(); - then(span.ended).isTrue(); - } - - private ConnectionFactory connectionFactory() { - return new ConnectionFactory() { - @Override - public Publisher create() { - return null; - } - - @Override - public ConnectionFactoryMetadata getMetadata() { - return () -> "my-name"; - } - }; - } - - private BeanFactory beanFactory() { - StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); - beanFactory.addBean("tracer", this.simpleTracer); - beanFactory.addBean("r2dbcProperties", new R2dbcProperties()); - return beanFactory; - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.r2dbc; + +import java.util.concurrent.atomic.AtomicReference; + +import io.r2dbc.proxy.core.QueryExecutionInfo; +import io.r2dbc.proxy.core.ValueStore; +import io.r2dbc.proxy.test.MockQueryExecutionInfo; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryMetadata; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcProperties; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.docs.AssertingSpan; +import org.springframework.cloud.sleuth.tracer.SimpleSpan; +import org.springframework.cloud.sleuth.tracer.SimpleTracer; + +import static org.assertj.core.api.BDDAssertions.then; + +class TraceProxyExecutionListenerTests { + + SimpleTracer simpleTracer = new SimpleTracer(); + + ConnectionFactory connectionFactory = connectionFactory(); + + TraceProxyExecutionListener listener = new TraceProxyExecutionListener(beanFactory(), connectionFactory) { + @Override + boolean isContextUnusable() { + return false; + } + }; + + @Test + void should_do_nothing_on_before_query_when_there_was_no_previous_span() { + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); + + listener.beforeQuery(queryExecutionInfo); + + then(this.simpleTracer.spans).isEmpty(); + then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); + } + + @Test + void should_do_nothing_on_after_query_when_there_was_no_previous_span() { + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); + + listener.afterQuery(queryExecutionInfo); + + then(this.simpleTracer.spans).isEmpty(); + then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); + } + + @Test + void should_do_nothing_on_each_query_when_there_was_no_previous_span() { + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); + + listener.eachQueryResult(queryExecutionInfo); + + then(this.simpleTracer.spans).isEmpty(); + then(queryExecutionInfo.getValueStore().get(Span.class)).isNull(); + } + + @Test + void should_put_a_child_span_in_value_store_when_a_span_was_already_in_context() { + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.empty(); + this.simpleTracer.nextSpan().start(); + AtomicReference clientSpan = new AtomicReference<>(); + listener = new TraceProxyExecutionListener(beanFactory(), connectionFactory) { + @Override + AssertingSpan clientSpan(QueryExecutionInfo executionInfo, String name) { + AssertingSpan span = super.clientSpan(executionInfo, name); + clientSpan.set(span); + return span; + } + }; + + listener.beforeQuery(queryExecutionInfo); + + then(queryExecutionInfo.getValueStore().get(Span.class)).isSameAs(clientSpan.get()); + } + + @Test + void should_annotate_a_span_on_query_result() { + SimpleSpan span = new SimpleSpan(); + ValueStore valueStore = ValueStore.create(); + valueStore.put(Span.class, span); + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.builder().valueStore(valueStore).build(); + + listener.eachQueryResult(queryExecutionInfo); + + then(span.events).isNotEmpty(); + } + + @Test + void should_end_span_on_after_query() { + SimpleSpan span = new SimpleSpan().start(); + ValueStore valueStore = ValueStore.create(); + valueStore.put(Span.class, span); + MockQueryExecutionInfo queryExecutionInfo = MockQueryExecutionInfo.builder().throwable(new RuntimeException()) + .valueStore(valueStore).build(); + + listener.afterQuery(queryExecutionInfo); + + then(span.throwable).isNotNull(); + then(span.ended).isTrue(); + } + + private ConnectionFactory connectionFactory() { + return new ConnectionFactory() { + @Override + public Publisher create() { + return null; + } + + @Override + public ConnectionFactoryMetadata getMetadata() { + return () -> "my-name"; + } + }; + } + + private BeanFactory beanFactory() { + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); + beanFactory.addBean("tracer", this.simpleTracer); + beanFactory.addBean("r2dbcProperties", new R2dbcProperties()); + return beanFactory; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTests.java index b9c623841..c7b47457b 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTests.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTests.java @@ -24,6 +24,7 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.support.StaticListableBeanFactory; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.docs.AssertingSpan; import org.springframework.cloud.sleuth.tracer.SimpleSpan; import org.springframework.cloud.sleuth.tracer.SimpleTracer; import org.springframework.transaction.PlatformTransactionManager; @@ -186,7 +187,7 @@ class TracePlatformTransactionManagerTests { private SimpleSpan thenATaggedSpanWasCreated(TracePlatformTransactionManager manager) { SpanAndScope spanAndScope = manager.threadLocalSpan.get(); then(spanAndScope).isNotNull(); - SimpleSpan span = (SimpleSpan) spanAndScope.getSpan(); + SimpleSpan span = AssertingSpan.unwrap(spanAndScope.getSpan()); then(span.started).isTrue(); then(span.tags).isNotEmpty(); return span; diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java index bec2d510b..320c0278a 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpanBuilder.java @@ -1,113 +1,113 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.tracer; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.TraceContext; - -/** - * A noop implementation. Does nothing. - * - * @author Marcin Grzejszczak - * @since 3.0.0 - */ -public class SimpleSpanBuilder implements Span.Builder { - - public List events = new ArrayList<>(); - - public Map tags = new HashMap<>(); - - public Throwable error; - - public Span.Kind spanKind; - - public String remoteServiceName; - - public String name; - - public SimpleTracer simpleTracer; - - public SimpleSpanBuilder(SimpleTracer simpleTracer) { - this.simpleTracer = simpleTracer; - } - - @Override - public Span.Builder setParent(TraceContext context) { - return this; - } - - @Override - public Span.Builder setNoParent() { - return this; - } - - @Override - public Span.Builder name(String name) { - this.name = name; - return this; - } - - @Override - public Span.Builder event(String value) { - this.events.add(value); - return this; - } - - @Override - public Span.Builder tag(String key, String value) { - this.tags.put(key, value); - return this; - } - - @Override - public Span.Builder error(Throwable throwable) { - this.error = throwable; - return this; - } - - @Override - public Span.Builder kind(Span.Kind spanKind) { - this.spanKind = spanKind; - return this; - } - - @Override - public Span.Builder remoteServiceName(String remoteServiceName) { - this.remoteServiceName = remoteServiceName; - return this; - } - - @Override - public Span start() { - SimpleSpan span = new SimpleSpan(); - this.tags.forEach(span::tag); - this.events.forEach(span::event); - span.remoteServiceName(this.remoteServiceName); - span.error(this.error); - span.spanKind = this.spanKind; - span.name(this.name); - span.start(); - simpleTracer.spans.add(span); - return span; - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.tracer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; + +/** + * A noop implementation. Does nothing. + * + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +public class SimpleSpanBuilder implements Span.Builder { + + public List events = new ArrayList<>(); + + public Map tags = new HashMap<>(); + + public Throwable error; + + public Span.Kind spanKind; + + public String remoteServiceName; + + public String name; + + public SimpleTracer simpleTracer; + + public SimpleSpanBuilder(SimpleTracer simpleTracer) { + this.simpleTracer = simpleTracer; + } + + @Override + public Span.Builder setParent(TraceContext context) { + return this; + } + + @Override + public Span.Builder setNoParent() { + return this; + } + + @Override + public Span.Builder name(String name) { + this.name = name; + return this; + } + + @Override + public Span.Builder event(String value) { + this.events.add(value); + return this; + } + + @Override + public Span.Builder tag(String key, String value) { + this.tags.put(key, value); + return this; + } + + @Override + public Span.Builder error(Throwable throwable) { + this.error = throwable; + return this; + } + + @Override + public Span.Builder kind(Span.Kind spanKind) { + this.spanKind = spanKind; + return this; + } + + @Override + public Span.Builder remoteServiceName(String remoteServiceName) { + this.remoteServiceName = remoteServiceName; + return this; + } + + @Override + public Span start() { + SimpleSpan span = new SimpleSpan(); + this.tags.forEach(span::tag); + this.events.forEach(span::event); + span.remoteServiceName(this.remoteServiceName); + span.error(this.error); + span.spanKind = this.spanKind; + span.name(this.name); + span.start(); + simpleTracer.spans.add(span); + return span; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java index 6735cdbcc..51c93020b 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleTracer.java @@ -1,128 +1,128 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.tracer; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.assertj.core.api.BDDAssertions; - -import org.springframework.cloud.sleuth.BaggageInScope; -import org.springframework.cloud.sleuth.ScopedSpan; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.SpanCustomizer; -import org.springframework.cloud.sleuth.TraceContext; -import org.springframework.cloud.sleuth.Tracer; - -/** - * A noop implementation. Does nothing. - * - * @author Marcin Grzejszczak - * @since 3.0.0 - */ -public class SimpleTracer implements Tracer { - - public List spans = new ArrayList<>(); - - @Override - public Span nextSpan(Span parent) { - return new SimpleSpan(); - } - - public SimpleSpan getOnlySpan() { - BDDAssertions.then(this.spans).hasSize(1); - SimpleSpan span = this.spans.get(0); - BDDAssertions.then(span.started).as("Span must be started").isTrue(); - BDDAssertions.then(span.ended).as("Span must be finished").isTrue(); - return span; - } - - public SimpleSpan getLastSpan() { - BDDAssertions.then(this.spans).isNotEmpty(); - SimpleSpan span = this.spans.get(this.spans.size() - 1); - BDDAssertions.then(span.started).as("Span must be started").isTrue(); - return span; - } - - @Override - public SpanInScope withSpan(Span span) { - return new NoOpSpanInScope(); - } - - @Override - public SpanCustomizer currentSpanCustomizer() { - return null; - } - - @Override - public Span currentSpan() { - if (this.spans.isEmpty()) { - return null; - } - return this.spans.get(spans.size() - 1); - } - - @Override - public SimpleSpan nextSpan() { - final SimpleSpan span = new SimpleSpan(); - this.spans.add(span); - return span; - } - - @Override - public ScopedSpan startScopedSpan(String name) { - return null; - } - - @Override - public Span.Builder spanBuilder() { - return new SimpleSpanBuilder(this); - } - - @Override - public TraceContext.Builder traceContextBuilder() { - return null; - } - - @Override - public Map getAllBaggage() { - return new HashMap<>(); - } - - @Override - public BaggageInScope getBaggage(String name) { - return null; - } - - @Override - public BaggageInScope getBaggage(TraceContext traceContext, String name) { - return null; - } - - @Override - public BaggageInScope createBaggage(String name) { - return null; - } - - @Override - public BaggageInScope createBaggage(String name, String value) { - return null; - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.tracer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.BDDAssertions; + +import org.springframework.cloud.sleuth.BaggageInScope; +import org.springframework.cloud.sleuth.ScopedSpan; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanCustomizer; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; + +/** + * A noop implementation. Does nothing. + * + * @author Marcin Grzejszczak + * @since 3.0.0 + */ +public class SimpleTracer implements Tracer { + + public List spans = new ArrayList<>(); + + @Override + public Span nextSpan(Span parent) { + return new SimpleSpan(); + } + + public SimpleSpan getOnlySpan() { + BDDAssertions.then(this.spans).hasSize(1); + SimpleSpan span = this.spans.get(0); + BDDAssertions.then(span.started).as("Span must be started").isTrue(); + BDDAssertions.then(span.ended).as("Span must be finished").isTrue(); + return span; + } + + public SimpleSpan getLastSpan() { + BDDAssertions.then(this.spans).isNotEmpty(); + SimpleSpan span = this.spans.get(this.spans.size() - 1); + BDDAssertions.then(span.started).as("Span must be started").isTrue(); + return span; + } + + @Override + public SpanInScope withSpan(Span span) { + return new NoOpSpanInScope(); + } + + @Override + public SpanCustomizer currentSpanCustomizer() { + return null; + } + + @Override + public Span currentSpan() { + if (this.spans.isEmpty()) { + return null; + } + return this.spans.get(spans.size() - 1); + } + + @Override + public SimpleSpan nextSpan() { + final SimpleSpan span = new SimpleSpan(); + this.spans.add(span); + return span; + } + + @Override + public ScopedSpan startScopedSpan(String name) { + return null; + } + + @Override + public Span.Builder spanBuilder() { + return new SimpleSpanBuilder(this); + } + + @Override + public TraceContext.Builder traceContextBuilder() { + return null; + } + + @Override + public Map getAllBaggage() { + return new HashMap<>(); + } + + @Override + public BaggageInScope getBaggage(String name) { + return null; + } + + @Override + public BaggageInScope getBaggage(TraceContext traceContext, String name) { + return null; + } + + @Override + public BaggageInScope createBaggage(String name) { + return null; + } + + @Override + public BaggageInScope createBaggage(String name, String value) { + return null; + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/src/test/resources/application.yml b/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/src/test/resources/application.yml index be7f4fe29..687a3ab51 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/src/test/resources/application.yml +++ b/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/src/test/resources/application.yml @@ -1 +1 @@ -logging.level.org.springframework.cloud: DEBUG +logging.level.org.springframework.cloud: TRACE diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/quartz/TracingJobListenerTest.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/quartz/TracingJobListenerTest.java index faf1f7102..f6016a86f 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/quartz/TracingJobListenerTest.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/quartz/TracingJobListenerTest.java @@ -50,7 +50,6 @@ import org.springframework.cloud.sleuth.test.TestTracingAwareSupplier; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.cloud.sleuth.instrument.quartz.TracingJobListener.CONTEXT_SPAN_IN_SCOPE_KEY; import static org.springframework.cloud.sleuth.instrument.quartz.TracingJobListener.CONTEXT_SPAN_KEY; -import static org.springframework.cloud.sleuth.instrument.quartz.TracingJobListener.TRIGGER_TAG_KEY; /** * @author Branden Cash @@ -125,7 +124,8 @@ public abstract class TracingJobListenerTest implements TestTracingAwareSupplier // expect FinishedSpan span = tracerTest().handler().takeLocalSpan(); assertThat(span.getName()).isEqualToIgnoringCase(SUCCESSFUL_JOB_KEY.toString()); - assertThat(span.getTags().get(TRIGGER_TAG_KEY)).isEqualToIgnoringCase(TRIGGER_KEY.toString()); + assertThat(span.getTags().get(SleuthQuartzSpan.Tags.TRIGGER.getKey())) + .isEqualToIgnoringCase(TRIGGER_KEY.toString()); } @Test From 1273fa01bf8636bb2138b0a58e4e636cf8735d8f Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 18 May 2021 19:34:46 +0200 Subject: [PATCH 32/59] Added Vault integration; fixes gh-1952 --- docs/src/main/asciidoc/_configprops.adoc | 1 + docs/src/main/asciidoc/integrations.adoc | 9 +++ spring-cloud-sleuth-autoconfigure/pom.xml | 5 ++ .../TraceWebClientAutoConfiguration.java | 49 +++++++++---- ...itional-spring-configuration-metadata.json | 6 ++ .../client/TraceVaultConfigurationTests.java | 70 +++++++++++++++++++ 6 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceVaultConfigurationTests.java diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 9b43bd350..95f1c0a35 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -61,6 +61,7 @@ |spring.sleuth.trace-id128 | `false` | When true, generate 128-bit trace IDs instead of 64-bit ones. |spring.sleuth.tracer.mode | | Set which tracer implementation should be picked. |spring.sleuth.tx.enabled | `true` | Enable Spring TX instrumentation. +|spring.sleuth.vault.enabled | `true` | Enable Spring Vault instrumentation. |spring.sleuth.web.additional-skip-pattern | | Additional pattern for URLs that should be skipped in tracing. This will be appended to the {@link SleuthWebProperties#skipPattern}. |spring.sleuth.web.client.enabled | `true` | Enable interceptor injecting into {@link org.springframework.web.client.RestTemplate}. |spring.sleuth.web.client.skip-pattern | | Pattern for URLs that should be skipped in client side tracing. diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 04f70fe95..4127620f7 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -634,3 +634,12 @@ This feature is available for all tracer implementations. If you have R2DBC Proxy on the classpath we will instrument the `ConnectionFactory`so that it contains a custom `ProxyExecutionListener`. In order to disable this instrumentation set `spring.sleuth.r2dbc.enabled` to `false`. + + +[[sleuth-vault-integration]] +== Spring Vault + +This feature is available for all tracer implementations. + +We're instrumenting the `RestTemplate` or `WebClient` instances used by Spring Vault to communicate with Vault. +In order to disable this instrumentation set `spring.sleuth.vault.enabled` to `false`. diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index 4be621272..46a491f01 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -183,6 +183,11 @@ spring-security-oauth2-autoconfigure true + + org.springframework.vault + spring-vault-core + true + diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientAutoConfiguration.java index 3e7d85c46..507d87116 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientAutoConfiguration.java @@ -20,7 +20,6 @@ import reactor.netty.http.client.HttpClient; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -29,7 +28,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoRestTemplateCustomizer; -import org.springframework.boot.web.client.RestTemplateCustomizer; import org.springframework.cloud.commons.httpclient.HttpClientConfiguration; import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter; import org.springframework.cloud.sleuth.CurrentTraceContext; @@ -50,6 +48,7 @@ import org.springframework.cloud.sleuth.propagation.Propagator; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.web.client.RestTemplate; @@ -81,22 +80,27 @@ class TraceWebClientAutoConfiguration { httpClientHandler); } + @Bean + @Order + TraceRestTemplateCustomizer traceRestTemplateCustomizer(BeanFactory beanFactory) { + return new TraceRestTemplateCustomizer(new LazyTraceClientHttpRequestInterceptor(beanFactory)); + } + + @Bean + static TraceRestTemplateBeanPostProcessor traceRestTemplateBeanPostProcessor(ListableBeanFactory beanFactory) { + return new TraceRestTemplateBeanPostProcessor(beanFactory); + } + @Configuration(proxyBeanMethods = false) - protected static class TraceInterceptorConfiguration { - - @Autowired - private BeanFactory beanFactory; + @ConditionalOnClass(org.springframework.vault.client.RestTemplateCustomizer.class) + @ConditionalOnProperty(value = "spring.sleuth.vault.enabled", matchIfMissing = true) + static class VaultRestTemplateCustomizerConfiguration { @Bean - static TraceRestTemplateBeanPostProcessor traceRestTemplateBeanPostProcessor( - ListableBeanFactory beanFactory) { - return new TraceRestTemplateBeanPostProcessor(beanFactory); - } - - @Bean - @Order - RestTemplateCustomizer traceRestTemplateCustomizer() { - return new TraceRestTemplateCustomizer(new LazyTraceClientHttpRequestInterceptor(this.beanFactory)); + @Order(Ordered.HIGHEST_PRECEDENCE) + org.springframework.vault.client.RestTemplateCustomizer traceVaultRestTemplateCustomizer( + TraceRestTemplateCustomizer restTemplateCustomizer) { + return restTemplateCustomizer::customize; } } @@ -144,6 +148,21 @@ class TraceWebClientAutoConfiguration { return new TraceWebClientBeanPostProcessor(springContext); } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(org.springframework.vault.client.WebClientCustomizer.class) + @ConditionalOnProperty(value = "spring.sleuth.vault.enabled", matchIfMissing = true) + static class VaultWebClientCustomizerConfiguration { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + org.springframework.vault.client.WebClientCustomizer traceVaultWebClientCustomizer( + ConfigurableApplicationContext springContext) { + return webClientBuilder -> new TraceWebClientBeanPostProcessor(springContext) + .postProcessAfterInitialization(webClientBuilder, ""); + } + + } + } @Configuration(proxyBeanMethods = false) diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 97669ed28..a428a7a2d 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -178,6 +178,12 @@ "type": "java.lang.Boolean", "description": "Enable R2dbc instrumentation.", "defaultValue": true + }, + { + "name": "spring.sleuth.vault.enabled", + "type": "java.lang.Boolean", + "description": "Enable Spring Vault instrumentation.", + "defaultValue": true } ] } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceVaultConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceVaultConfigurationTests.java new file mode 100644 index 000000000..07a2ed9d7 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceVaultConfigurationTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.web.client; + +import org.junit.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.vault.client.RestTemplateCustomizer; +import org.springframework.vault.client.WebClientCustomizer; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TraceVaultConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withPropertyValues("spring.sleuth.noop.enabled=true").withConfiguration( + AutoConfigurations.of(TraceNoOpAutoConfiguration.class, TraceWebClientAutoConfiguration.class)); + + @Test + public void shouldNotCreateVaultRestTemplateCustomizerWhenVaultNotOnClasspath() { + this.contextRunner.withClassLoader(new FilteredClassLoader(RestTemplateCustomizer.class)) + .run((context) -> assertThat(context).doesNotHaveBean(RestTemplateCustomizer.class)); + } + + @Test + public void shouldNotCreateVaultWebClientCustomizerWhenVaultNotOnClasspath() { + this.contextRunner.withClassLoader(new FilteredClassLoader(WebClientCustomizer.class)) + .run((context) -> assertThat(context).doesNotHaveBean(WebClientCustomizer.class)); + } + + @Test + public void shouldNotCreateVaultRestTemplateCustomizerWhenSleuthVaultDisabled() { + this.contextRunner.withPropertyValues("spring.sleuth.vault.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(RestTemplateCustomizer.class)); + } + + @Test + public void shouldNotCreateVaultWebClientCustomizerWhenSleuthVaultDisabled() { + this.contextRunner.withPropertyValues("spring.sleuth.vault.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(WebClientCustomizer.class)); + } + + @Test + public void shouldCreateVaultRestTemplateCustomizerWhenVaultNotOnClasspath() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(RestTemplateCustomizer.class)); + } + + @Test + public void shouldCreateVaultWebClientCustomizerWhenVaultNotOnClasspath() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(WebClientCustomizer.class)); + } + +} From f4830ea8b3ab5552f61e3f9142791040e4724266 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 19 May 2021 08:38:50 +0200 Subject: [PATCH 33/59] Changed equality to include asserting span as a proxy --- .../cloud/sleuth/brave/bridge/BraveSpan.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpan.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpan.java index 9643d31c6..99510fb4b 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpan.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpan.java @@ -115,10 +115,14 @@ public class BraveSpan implements Span { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + Object unwrapped = o; + if (o instanceof AssertingSpan) { + unwrapped = ((AssertingSpan) o).getDelegate(); + } + if (unwrapped == null || getClass() != unwrapped.getClass()) { return false; } - BraveSpan braveSpan = (BraveSpan) o; + BraveSpan braveSpan = (BraveSpan) unwrapped; return Objects.equals(this.delegate, braveSpan.delegate); } From e7e97a65b6c571ea305659b606ab97ae37747bac Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 20 May 2021 09:51:23 +0200 Subject: [PATCH 34/59] Removed Brave reference from configruation --- .../instrument/reactor/TraceReactorAutoConfiguration.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/reactor/TraceReactorAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/reactor/TraceReactorAutoConfiguration.java index a4bc41fa7..f50ac2a73 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/reactor/TraceReactorAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/reactor/TraceReactorAutoConfiguration.java @@ -23,8 +23,6 @@ import java.util.Iterator; import java.util.Queue; import java.util.function.Function; -import brave.propagation.CurrentTraceContext; -import brave.propagation.TraceContext; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; @@ -44,6 +42,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.context.scope.refresh.RefreshScope; import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.TraceContext; import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; @@ -294,7 +294,7 @@ class HookRegisteringBeanDefinitionRegistryPostProcessor implements BeanDefiniti @Override public boolean offer(Object o) { - TraceContext traceContext = currentTraceContext.get(); + TraceContext traceContext = currentTraceContext.context(); return envelopeQueue.offer(new Envelope(o, traceContext)); } From 2dd82624e597d1dd39104fcd428701dc6d672be6 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 20 May 2021 18:55:07 +0200 Subject: [PATCH 35/59] Added support for Tomcat's Valve; fixes gh-1329 --- docs/src/main/asciidoc/_configprops.adoc | 1 + docs/src/main/asciidoc/integrations.adoc | 9 +- .../web/TraceWebServletConfiguration.java | 131 ++++++++++-------- ...itional-spring-configuration-metadata.json | 6 + .../web/TraceTomcatConfigurationTests.java | 81 +++++++++++ .../servlet/HttpServletRequestWrapper.java | 3 +- .../servlet/HttpServletResponseWrapper.java | 3 +- .../instrument/web/tomcat/TraceValve.java | 88 ++++++++++++ .../deployer/TraceAppDeployerTests.java | 4 +- .../web/tomcat/TraceValveTests.java | 104 ++++++++++++++ ...xt.java => SimpleCurrentTraceContext.java} | 8 +- .../web/TraceFilterWebIntegrationTests.java | 6 +- 12 files changed, 375 insertions(+), 69 deletions(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/TraceTomcatConfigurationTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/tomcat/TraceValve.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/web/tomcat/TraceValveTests.java rename spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/{NoOpCurrentTraceContext.java => SimpleCurrentTraceContext.java} (88%) diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 95f1c0a35..2fa255bb1 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -70,6 +70,7 @@ |spring.sleuth.web.ignore-auto-configured-skip-patterns | `false` | If set to true, auto-configured skip patterns will be ignored. |spring.sleuth.web.servlet.enabled | `true` | Enable servlet instrumentation. |spring.sleuth.web.skip-pattern | `/api-docs.*\|/swagger.*\|.*\.png\|.*\.css\|.*\.js\|.*\.html\|/favicon.ico\|/hystrix.stream` | Pattern for URLs that should be skipped in tracing. +|spring.sleuth.web.tomcat.enabled | `true` | Enable tracing instrumentation for Tomcat. |spring.sleuth.web.webclient.enabled | `true` | Enable tracing instrumentation for WebClient. |spring.zipkin.activemq.message-max-bytes | `100000` | Maximum number of bytes for a given message with spans sent to Zipkin over ActiveMQ. |spring.zipkin.activemq.queue | `zipkin` | Name of the ActiveMQ queue where spans should be sent to Zipkin. diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 4127620f7..8c6368eee 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -635,7 +635,6 @@ This feature is available for all tracer implementations. If you have R2DBC Proxy on the classpath we will instrument the `ConnectionFactory`so that it contains a custom `ProxyExecutionListener`. In order to disable this instrumentation set `spring.sleuth.r2dbc.enabled` to `false`. - [[sleuth-vault-integration]] == Spring Vault @@ -643,3 +642,11 @@ This feature is available for all tracer implementations. We're instrumenting the `RestTemplate` or `WebClient` instances used by Spring Vault to communicate with Vault. In order to disable this instrumentation set `spring.sleuth.vault.enabled` to `false`. + +[[sleuth-tomcat-integration]] +== Spring Tomcat + +This feature is available for all tracer implementations. + +We're adding an instrumented Tomcat's `Valve` that originates the span. +In order to disable this instrumentation set `spring.sleuth.web.tomcat.enabled` to `false`. diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/TraceWebServletConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/TraceWebServletConfiguration.java index 1c0e73d5f..abb21771a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/TraceWebServletConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/TraceWebServletConfiguration.java @@ -26,10 +26,14 @@ import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import org.apache.catalina.Valve; + import org.springframework.beans.factory.BeanFactory; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.web.embedded.tomcat.ConfigurableTomcatWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.cloud.sleuth.CurrentTraceContext; import org.springframework.cloud.sleuth.SpanNamer; @@ -38,9 +42,12 @@ import org.springframework.cloud.sleuth.http.HttpServerHandler; import org.springframework.cloud.sleuth.instrument.web.TraceWebAspect; import org.springframework.cloud.sleuth.instrument.web.mvc.SpanCustomizingAsyncHandlerInterceptor; import org.springframework.cloud.sleuth.instrument.web.servlet.TracingFilter; +import org.springframework.cloud.sleuth.instrument.web.tomcat.TraceValve; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; @@ -57,74 +64,84 @@ import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @ConditionalOnClass(HandlerInterceptorAdapter.class) @Import(SpanCustomizingAsyncHandlerInterceptor.class) +@ConditionalOnProperty(value = "spring.sleuth.web.servlet.enabled", matchIfMissing = true) class TraceWebServletConfiguration { + @Bean + TraceWebAspect traceWebAspect(Tracer tracer, CurrentTraceContext currentTraceContext, SpanNamer spanNamer) { + return new TraceWebAspect(tracer, currentTraceContext, spanNamer); + } + + @Bean + FilterRegistrationBean traceWebFilter(BeanFactory beanFactory, SleuthWebProperties webProperties) { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new LazyTracingFilter(beanFactory)); + filterRegistrationBean.setDispatcherTypes(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.FORWARD, + DispatcherType.INCLUDE, DispatcherType.REQUEST); + filterRegistrationBean.setOrder(webProperties.getFilterOrder()); + return filterRegistrationBean; + } + + /** + * Nested config that configures Web MVC if it's present (without adding a runtime + * dependency to it). + */ @Configuration(proxyBeanMethods = false) - @ConditionalOnProperty(value = "spring.sleuth.web.servlet.enabled", matchIfMissing = true) - static class ServletConfiguration { + @ConditionalOnClass(WebMvcConfigurer.class) + @Import(TraceWebMvcConfigurer.class) + protected static class TraceWebMvcAutoConfiguration { - @Bean - TraceWebAspect traceWebAspect(Tracer tracer, CurrentTraceContext currentTraceContext, SpanNamer spanNamer) { - return new TraceWebAspect(tracer, currentTraceContext, spanNamer); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ Valve.class, ConfigurableTomcatWebServerFactory.class }) + @ConditionalOnProperty(value = "spring.sleuth.web.tomcat.enabled", matchIfMissing = true) + protected static class TraceTomcatConfiguration { + + static final String CUSTOMIZER_NAME = "traceTomcatWebServerFactoryCustomizer"; + + @Bean(name = CUSTOMIZER_NAME) + @Order(Ordered.HIGHEST_PRECEDENCE) + WebServerFactoryCustomizer traceTomcatWebServerFactoryCustomizer( + HttpServerHandler httpServerHandler, CurrentTraceContext currentTraceContext) { + return factory -> factory.addEngineValves(new TraceValve(httpServerHandler, currentTraceContext)); } - @Bean - FilterRegistrationBean traceWebFilter(BeanFactory beanFactory, SleuthWebProperties webProperties) { - FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean( - new LazyTracingFilter(beanFactory)); - filterRegistrationBean.setDispatcherTypes(DispatcherType.ASYNC, DispatcherType.ERROR, - DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.REQUEST); - filterRegistrationBean.setOrder(webProperties.getFilterOrder()); - return filterRegistrationBean; + } + + static final class LazyTracingFilter implements Filter { + + private final BeanFactory beanFactory; + + private Filter tracingFilter; + + LazyTracingFilter(BeanFactory beanFactory) { + this.beanFactory = beanFactory; } - /** - * Nested config that configures Web MVC if it's present (without adding a runtime - * dependency to it). - */ - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(WebMvcConfigurer.class) - @Import(TraceWebMvcConfigurer.class) - protected static class TraceWebMvcAutoConfiguration { + @Override + public void init(FilterConfig filterConfig) throws ServletException { + tracingFilter().init(filterConfig); + } + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + tracingFilter().doFilter(request, response, chain); + } + + @Override + public void destroy() { + tracingFilter().destroy(); + } + + private Filter tracingFilter() { + if (this.tracingFilter == null) { + this.tracingFilter = TracingFilter.create(this.beanFactory.getBean(CurrentTraceContext.class), + this.beanFactory.getBean(HttpServerHandler.class)); + } + return this.tracingFilter; } } } - -final class LazyTracingFilter implements Filter { - - private final BeanFactory beanFactory; - - private Filter tracingFilter; - - LazyTracingFilter(BeanFactory beanFactory) { - this.beanFactory = beanFactory; - } - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - tracingFilter().init(filterConfig); - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - tracingFilter().doFilter(request, response, chain); - } - - @Override - public void destroy() { - tracingFilter().destroy(); - } - - private Filter tracingFilter() { - if (this.tracingFilter == null) { - this.tracingFilter = TracingFilter.create(this.beanFactory.getBean(CurrentTraceContext.class), - this.beanFactory.getBean(HttpServerHandler.class)); - } - return this.tracingFilter; - } - -} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index a428a7a2d..95cc9ca24 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -131,6 +131,12 @@ "description": "Enable tracing instrumentation for WebClient.", "defaultValue": true }, + { + "name": "spring.sleuth.web.tomcat.enabled", + "type": "java.lang.Boolean", + "description": "Enable tracing instrumentation for Tomcat.", + "defaultValue": true + }, { "name": "spring.sleuth.integration.enabled", "type": "java.lang.Boolean", diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/TraceTomcatConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/TraceTomcatConfigurationTests.java new file mode 100644 index 000000000..f8b2aa4e3 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/TraceTomcatConfigurationTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.web; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.context.annotation.Configuration; + +class TraceTomcatConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withPropertyValues("spring.sleuth.noop.enabled=true").withUserConfiguration(TestConfig.class) + .withConfiguration( + AutoConfigurations.of(TraceWebServletConfiguration.class, TraceNoOpAutoConfiguration.class)); + + @Test + public void should_not_register_customizer_when_tomcat_not_present() throws Exception { + contextRunner.withClassLoader(new FilteredClassLoader("org.apache.catalina.Valve")).run(context -> BDDAssertions + .then(context).doesNotHaveBean(TraceWebServletConfiguration.TraceTomcatConfiguration.CUSTOMIZER_NAME)); + } + + @Test + public void should_not_register_customizer_when_tomcat_customizer_not_present() throws Exception { + contextRunner + .withClassLoader(new FilteredClassLoader( + "org.springframework.boot.web.embedded.tomcat.ConfigurableTomcatWebServerFactory")) + .run(context -> BDDAssertions.then(context) + .doesNotHaveBean(TraceWebServletConfiguration.TraceTomcatConfiguration.CUSTOMIZER_NAME)); + } + + @Test + public void should_not_register_customizer_when_tomcat_disabled() throws Exception { + contextRunner.withPropertyValues("spring.sleuth.web.tomcat.enabled=false").run(context -> BDDAssertions + .then(context).doesNotHaveBean(TraceWebServletConfiguration.TraceTomcatConfiguration.CUSTOMIZER_NAME)); + } + + @Test + public void should_not_register_customizer_when_servlet_disabled() throws Exception { + contextRunner.withPropertyValues("spring.sleuth.web.servlet.enabled=false").run(context -> BDDAssertions + .then(context).doesNotHaveBean(TraceWebServletConfiguration.TraceTomcatConfiguration.CUSTOMIZER_NAME)); + } + + @Test + public void should_not_register_customizer_when_web_disabled() throws Exception { + contextRunner.withPropertyValues("spring.sleuth.web.enabled=false").run(context -> BDDAssertions.then(context) + .doesNotHaveBean(TraceWebServletConfiguration.TraceTomcatConfiguration.CUSTOMIZER_NAME)); + } + + @Test + public void should_register_customizer_when_tomcat_present() throws Exception { + contextRunner.run(context -> BDDAssertions.then(context) + .hasBean(TraceWebServletConfiguration.TraceTomcatConfiguration.CUSTOMIZER_NAME)); + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(SleuthWebProperties.class) + static class TestConfig { + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletRequestWrapper.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletRequestWrapper.java index 133c195ba..8fe1bc235 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletRequestWrapper.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletRequestWrapper.java @@ -31,8 +31,7 @@ import org.springframework.lang.Nullable; * * @since 5.10 */ -// Public for use in sparkjava or other frameworks that re-use servlet types -class HttpServletRequestWrapper implements HttpServerRequest { +public class HttpServletRequestWrapper implements HttpServerRequest { /** @since 5.10 */ public static HttpServerRequest create(HttpServletRequest request) { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletResponseWrapper.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletResponseWrapper.java index 5971fc1ef..3d70cbd80 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletResponseWrapper.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletResponseWrapper.java @@ -31,8 +31,7 @@ import org.springframework.lang.Nullable; * * @since 5.10 */ -// Public for use in sparkjava or other frameworks that re-use servlet types -class HttpServletResponseWrapper implements HttpServerResponse { +public class HttpServletResponseWrapper implements HttpServerResponse { // not final for inner // subtype diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/tomcat/TraceValve.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/tomcat/TraceValve.java new file mode 100644 index 000000000..1a67d98b6 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/tomcat/TraceValve.java @@ -0,0 +1,88 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.web.tomcat; + +import java.io.IOException; + +import javax.servlet.ServletException; + +import org.apache.catalina.Valve; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ValveBase; + +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanCustomizer; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.http.HttpServerHandler; +import org.springframework.cloud.sleuth.instrument.web.servlet.HttpServletRequestWrapper; +import org.springframework.cloud.sleuth.instrument.web.servlet.HttpServletResponseWrapper; +import org.springframework.core.log.LogAccessor; + +/** + * A trace representation of a {@link Valve}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceValve extends ValveBase { + + private static final LogAccessor log = new LogAccessor(TraceValve.class); + + private final HttpServerHandler httpServerHandler; + + private final CurrentTraceContext currentTraceContext; + + public TraceValve(HttpServerHandler httpServerHandler, CurrentTraceContext currentTraceContext) { + this.httpServerHandler = httpServerHandler; + this.currentTraceContext = currentTraceContext; + } + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + Exception ex = null; + Span handleReceive = this.httpServerHandler + .handleReceive(HttpServletRequestWrapper.create(request.getRequest())); + if (log.isDebugEnabled()) { + log.debug("Created a server receive span [" + handleReceive + "]"); + } + request.setAttribute(SpanCustomizer.class.getName(), handleReceive); + request.setAttribute(TraceContext.class.getName(), handleReceive.context()); + request.setAttribute(Span.class.getName(), handleReceive); + try (CurrentTraceContext.Scope ws = this.currentTraceContext.maybeScope(handleReceive.context())) { + Valve next = getNext(); + if (null == next) { + // no next valve + return; + } + next.invoke(request, response); + } + catch (Exception exception) { + ex = exception; + throw exception; + } + finally { + this.httpServerHandler.handleSend( + HttpServletResponseWrapper.create(request.getRequest(), response.getResponse(), ex), handleReceive); + if (log.isDebugEnabled()) { + log.debug("Handled send of span [" + handleReceive + "]"); + } + } + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java index 208abe3f3..45ee9aaf6 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java @@ -36,7 +36,7 @@ import org.springframework.cloud.deployer.spi.app.AppStatus; import org.springframework.cloud.deployer.spi.core.AppDefinition; import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; import org.springframework.cloud.deployer.spi.core.RuntimeEnvironmentInfo; -import org.springframework.cloud.sleuth.tracer.NoOpCurrentTraceContext; +import org.springframework.cloud.sleuth.tracer.SimpleCurrentTraceContext; import org.springframework.cloud.sleuth.tracer.SimpleTracer; import org.springframework.core.env.Environment; import org.springframework.core.io.PathResource; @@ -145,7 +145,7 @@ class TraceAppDeployerTests { private BeanFactory beanFactory() { StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); beanFactory.addBean("tracer", this.simpleTracer); - beanFactory.addBean("currentTraceContext", new NoOpCurrentTraceContext()); + beanFactory.addBean("currentTraceContext", new SimpleCurrentTraceContext()); return beanFactory; } diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/web/tomcat/TraceValveTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/web/tomcat/TraceValveTests.java new file mode 100644 index 000000000..2a80595f9 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/web/tomcat/TraceValveTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.web.tomcat; + +import java.io.IOException; + +import javax.servlet.ServletException; + +import org.apache.catalina.Valve; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ValveBase; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.http.HttpServerHandler; +import org.springframework.cloud.sleuth.http.HttpServerRequest; +import org.springframework.cloud.sleuth.http.HttpServerResponse; +import org.springframework.cloud.sleuth.tracer.SimpleCurrentTraceContext; +import org.springframework.cloud.sleuth.tracer.SimpleSpan; + +import static org.assertj.core.api.BDDAssertions.then; + +class TraceValveTests { + + SimpleSpan simpleSpan = new SimpleSpan(); + + HttpServerHandler httpServerHandler = new HttpServerHandler() { + @Override + public SimpleSpan handleReceive(HttpServerRequest request) { + return simpleSpan.start(); + } + + @Override + public void handleSend(HttpServerResponse response, Span span) { + span.end(); + } + }; + + TraceValve traceValve = new TraceValve(this.httpServerHandler, new SimpleCurrentTraceContext()); + + @Test + void should_populate_tracecontext_attribute_for_tracing_filter_to_reuse() throws ServletException, IOException { + Request request = request(); + + this.traceValve.invoke(request, new Response()); + + then(request.getAttribute(TraceContext.class.getName())).isNotNull(); + thenSpanIsStartedAndStopped(); + } + + private void thenSpanIsStartedAndStopped() { + then(simpleSpan.started).isTrue(); + then(simpleSpan.ended).isTrue(); + } + + @Test + void should_populate_tracecontext_attribute_for_tracing_filter_to_reuse_when_there_is_another_valve_in_chain() + throws ServletException, IOException { + Request request = request(); + + new TraceValve(this.httpServerHandler, new SimpleCurrentTraceContext()) { + @Override + public Valve getNext() { + return new MyValve(); + } + }.invoke(request, new Response()); + + then(request.getAttribute(TraceContext.class.getName())).isNotNull(); + thenSpanIsStartedAndStopped(); + } + + private Request request() { + Request request = new Request(new Connector()); + request.setCoyoteRequest(new org.apache.coyote.Request()); + return request; + } + +} + +class MyValve extends ValveBase { + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpCurrentTraceContext.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleCurrentTraceContext.java similarity index 88% rename from spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpCurrentTraceContext.java rename to spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleCurrentTraceContext.java index 2634c18cf..6c171a1d2 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpCurrentTraceContext.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleCurrentTraceContext.java @@ -29,21 +29,25 @@ import org.springframework.cloud.sleuth.TraceContext; * @author Marcin Grzejszczak * @since 3.0.0 */ -public class NoOpCurrentTraceContext implements CurrentTraceContext { +public class SimpleCurrentTraceContext implements CurrentTraceContext { + + public TraceContext traceContext; @Override public TraceContext context() { - return null; + return this.traceContext; } @Override public Scope newScope(TraceContext context) { + this.traceContext = context; return () -> { }; } @Override public Scope maybeScope(TraceContext context) { + this.traceContext = context; return () -> { }; } diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceFilterWebIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceFilterWebIntegrationTests.java index 0eb5bc3cd..8ed8ad588 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceFilterWebIntegrationTests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceFilterWebIntegrationTests.java @@ -117,7 +117,7 @@ public class TraceFilterWebIntegrationTests { } @Test - public void exception_logging_span_handler_logs_synchronous_exceptions(CapturedOutput capture) { + public void should_instrument_logs_for_tomcat_entries(CapturedOutput capture) { try { new RestTemplate().getForObject("http://localhost:" + port() + "/", String.class); BDDAssertions.fail("should fail due to runtime exception"); @@ -127,7 +127,7 @@ public class TraceFilterWebIntegrationTests { then(this.currentTraceContext.get()).isNull(); MutableSpan fromFirstTraceFilterFlow = spanHandler.takeRemoteSpanWithErrorMessage(Kind.SERVER, - "Request processing failed; nested exception is java.lang.RuntimeException: Throwing exception"); + "Throwing exception"); then(fromFirstTraceFilterFlow.tags()).containsEntry("http.method", "GET").containsEntry("mvc.controller.class", "BasicErrorController"); // Trace IDs in logs: issue#714 @@ -137,7 +137,7 @@ public class TraceFilterWebIntegrationTests { private void thenLogsForExceptionLoggingFilterContainTracingInformation(CapturedOutput capture, String hex) { String[] split = capture.toString().split("\n"); - List list = Arrays.stream(split).filter(s -> s.contains("Uncaught exception thrown")) + List list = Arrays.stream(split).filter(s -> s.contains("Servlet.service() for servlet")) .filter(s -> s.contains(hex + "," + hex + "]")).collect(Collectors.toList()); then(list).isNotEmpty(); } From 190a15de571a8cbcb701ab95650141970166ea6c Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 26 May 2021 10:00:14 +0200 Subject: [PATCH 36/59] Adding jdbc instrumentation (#1959) fixes gh-1930 --- docs/src/main/asciidoc/_configprops.adoc | 27 +- docs/src/main/asciidoc/_spans.adoc | 155 ++++ docs/src/main/asciidoc/appendix.adoc | 44 +- docs/src/main/asciidoc/integrations.adoc | 52 +- .../main/asciidoc/spring-cloud-sleuth.adoc | 36 +- .../cloud/sleuth/documentation/SpanEntry.java | 8 +- .../SpanSearchingFileVisitor.java | 8 +- pom.xml | 24 + .../springframework/cloud/sleuth/Span.java | 16 +- .../cloud/sleuth/SpanAndScope.java | 10 +- spring-cloud-sleuth-autoconfigure/pom.xml | 26 +- .../autoconfig/SleuthBaggageProperties.java | 7 +- .../jdbc/DataSourceProxyConfiguration.java | 120 +++ .../instrument/jdbc/P6SpyConfiguration.java | 76 ++ .../jdbc/P6SpyPropertiesSetter.java | 189 +++++ ...eDataSourceDecoratorAutoConfiguration.java | 71 ++ ...eDataSourceDecoratorBeanPostProcessor.java | 140 ++++ .../TraceDataSourceDecoratorProperties.java | 502 ++++++++++++ .../main/resources/META-INF/spring.factories | 1 + ...SourceDecoratorAutoConfigurationTests.java | 370 +++++++++ .../jdbc/P6SpyConfigurationTests.java | 277 +++++++ .../ProxyDataSourceConfigurationTests.java | 238 ++++++ ...thP6SpyListenerAutoConfigurationTests.java | 65 ++ ...aSourceListenerAutoConfigurationTests.java | 68 ++ .../jdbc/TestSpanHandlerConfiguration.java | 38 + .../jdbc/TracingJdbcEventListenerTests.java | 85 +++ .../jdbc/TracingListenerStrategyTests.java | 720 ++++++++++++++++++ .../TracingQueryExecutionListenerTests.java | 38 + .../TraceQuartzAutoConfigurationTest.java | 25 +- .../cloud/sleuth/brave/bridge/BraveSpan.java | 6 + .../sleuth/brave/bridge/BraveSpanBuilder.java | 18 +- spring-cloud-sleuth-instrumentation/pom.xml | 11 + .../instrument/jdbc/DataSourceDecorator.java | 38 + .../jdbc/DataSourceNameResolver.java | 41 + .../DataSourceProxyBuilderCustomizer.java | 202 +++++ ...ourceProxyConnectionIdManagerProvider.java | 31 + .../DataSourceProxyDataSourceDecorator.java | 57 ++ .../jdbc/DataSourceProxyProperties.java | 263 +++++++ .../instrument/jdbc/DataSourceWrapper.java | 57 ++ .../P6SpyContextJdbcEventListenerFactory.java | 57 ++ .../jdbc/P6SpyDataSourceDecorator.java | 56 ++ .../instrument/jdbc/SleuthJdbcSpan.java | 167 ++++ ...eHikariListenerStrategySpanCustomizer.java | 54 ++ .../jdbc/TraceJdbcEventListener.java | 152 ++++ .../jdbc/TraceListenerStrategy.java | 402 ++++++++++ .../TraceListenerStrategySpanCustomizer.java | 43 ++ .../jdbc/TraceQueryExecutionListener.java | 170 +++++ .../sleuth/instrument/jdbc/TraceType.java | 39 + .../r2dbc/TraceProxyExecutionListener.java | 12 +- .../ContextJdbcEventListenerFactoryTest.java | 69 ++ .../web/TraceAsyncIntegrationTests.java | 1 + 51 files changed, 5293 insertions(+), 89 deletions(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorAutoConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorBeanPostProcessor.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorProperties.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TestSpanHandlerConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingQueryExecutionListenerTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceDecorator.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceNameResolver.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyBuilderCustomizer.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyConnectionIdManagerProvider.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyDataSourceDecorator.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyProperties.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceWrapper.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/P6SpyContextJdbcEventListenerFactory.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/P6SpyDataSourceDecorator.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/SleuthJdbcSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceHikariListenerStrategySpanCustomizer.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceJdbcEventListener.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategy.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceQueryExecutionListener.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceType.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/jdbc/ContextJdbcEventListenerFactoryTest.java diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 2fa255bb1..1399bc59a 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -5,10 +5,10 @@ |spring.sleuth.async.enabled | `true` | Enable instrumenting async related components so that the tracing information is passed between threads. |spring.sleuth.async.ignored-beans | | List of {@link java.util.concurrent.Executor} bean names that should be ignored and not wrapped in a trace representation. |spring.sleuth.baggage.correlation-enabled | `true` | Enables correlating the baggage context with logging contexts. -|spring.sleuth.baggage.correlation-fields | | -|spring.sleuth.baggage.local-fields | | +|spring.sleuth.baggage.correlation-fields | | List of fields that should be propagated over the wire. +|spring.sleuth.baggage.local-fields | | List of fields that should be accessible within the JVM process but not propagated over the wire. |spring.sleuth.baggage.remote-fields | | List of fields that are referenced the same in-process as it is on the wire. For example, the field "x-vcap-request-id" would be set as-is including the prefix. -|spring.sleuth.baggage.tag-fields | | +|spring.sleuth.baggage.tag-fields | | List of fields that should automatically become tags. |spring.sleuth.batch.enabled | `true` | Enable Spring Batch instrumentation. |spring.sleuth.circuitbreaker.enabled | `true` | Enable Spring Cloud CircuitBreaker instrumentation. |spring.sleuth.config.server.enabled | `true` | Enable Spring Cloud Config Server instrumentation. @@ -23,6 +23,27 @@ |spring.sleuth.integration.enabled | `true` | Enable Spring Integration instrumentation. |spring.sleuth.integration.patterns | `[!hystrixStreamOutput*, *, !channel*]` | An array of patterns against which channel names will be matched. @see org.springframework.integration.config.GlobalChannelInterceptor#patterns() Defaults to any channel name not matching the Hystrix Stream and functional Stream channel names. |spring.sleuth.integration.websockets.enabled | `true` | Enable tracing for WebSockets. +|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.count-query | `false` | Creates listener to count queries. @see ProxyDataSourceBuilder#countQuery() @see QueryCountHolder +|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.json-format | `false` | Use json output for logging query. @see ProxyDataSourceBuilder#asJson() +|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.logging | | Logging to use for logging queries. +|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.multiline | `true` | Use multiline output for logging query. @see ProxyDataSourceBuilder#multiline() +|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.query.enable-logging | `true` | Enable logging all queries to the log. +|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.query.log-level | `DEBUG` | Severity of query logger. +|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.query.logger-name | | Name of query logger. +|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.slow-query.log-level | `WARN` | Severity of slow query logger. +|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.slow-query.logger-name | | Name of slow query logger. +|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.slow-query.threshold | `300` | Number of seconds to consider query as slow. +|spring.sleuth.jdbc.decorator.datasource.enabled | `true` | Enables data source decorating. +|spring.sleuth.jdbc.decorator.datasource.excluded-beans | | Beans that won't be decorated. +|spring.sleuth.jdbc.decorator.datasource.includes | | Which types of tracing we would like to include. +|spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class | | Class file to use (only with logging=custom). The class must implement {@link com.p6spy.engine.spy.appender.FormattedLogger}. +|spring.sleuth.jdbc.decorator.datasource.p6spy.enable-logging | `true` | Enables logging JDBC events. +|spring.sleuth.jdbc.decorator.datasource.p6spy.log-file | `spy.log` | Name of log file to use (only with logging=file). +|spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern | | Use regex pattern to filter log messages. Only matched messages will be logged. +|spring.sleuth.jdbc.decorator.datasource.p6spy.log-format | | Custom log format. +|spring.sleuth.jdbc.decorator.datasource.p6spy.logging | | Logging to use for logging queries. +|spring.sleuth.jdbc.decorator.datasource.p6spy.multiline | `true` | Enables multiline output. +|spring.sleuth.jdbc.decorator.datasource.p6spy.tracing.include-parameter-values | `true` | Report the effective sql string (with '?' replaced with real values) to tracing systems.

NOTE this setting does not affect the logging message. |spring.sleuth.kafka.enabled | `true` | Enable instrumenting of Apache Kafka clients. |spring.sleuth.messaging.aspect.enabled | `false` | Should {@link MessageMapping} wrapping be enabled. |spring.sleuth.messaging.enabled | `false` | Should messaging be turned on. diff --git a/docs/src/main/asciidoc/_spans.adoc b/docs/src/main/asciidoc/_spans.adoc index b07649e68..4172c04e5 100644 --- a/docs/src/main/asciidoc/_spans.adoc +++ b/docs/src/main/asciidoc/_spans.adoc @@ -292,6 +292,105 @@ Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.in |deployer.start|When deployer started deploying the application. |=== +=== Jdbc Connection Span + +> Span created when a JDBC connection takes place. + +**Span name** `connection`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.jdbc.SleuthJdbcSpan` + +IMPORTANT: All tags and events must be prefixed with `jdbc.` prefix! + +.Tag Keys +|=== +|Name | Description +|jdbc.datasource.driver|Name of the JDBC datasource driver. +|jdbc.datasource.pool|Name of the JDBC datasource pool. +|=== + +=== Jdbc Query Span + +> Span created when a JDBC query gets executed. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.jdbc.SleuthJdbcSpan` + +IMPORTANT: All tags and events must be prefixed with `jdbc.` prefix! + +.Tag Keys +|=== +|Name | Description +|jdbc.query|The SQL query value. +|jdbc.row-count|Number of SQL rows. +|=== + +.Event Values +|=== +|Name | Description +|jdbc.commit|When the transaction gets commited. +|jdbc.rollback|When the transaction gets rolled back. +|=== + +=== Jdbc Result Set Span + +> Span created when working with JDBC result set. + +**Span name** `result-set`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.jdbc.SleuthJdbcSpan` + +IMPORTANT: All tags and events must be prefixed with `jdbc.` prefix! + +.Tag Keys +|=== +|Name | Description +|jdbc.query|The SQL query value. +|jdbc.row-count|Number of SQL rows. +|=== + +.Event Values +|=== +|Name | Description +|jdbc.commit|When the transaction gets commited. +|jdbc.rollback|When the transaction gets rolled back. +|=== + +=== Kafka Consumer Span + +> Span created on the Kafka consumer side. + +**Span name** `kafka.consume`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.kafka.SleuthKafkaSpan` + +IMPORTANT: All tags and events must be prefixed with `kafka.` prefix! + +.Tag Keys +|=== +|Name | Description +|kafka.offset|Kafka offset number. +|kafka.partition|Kafka partition number. +|kafka.topic|Name of the Kafka topic. +|=== + +=== Kafka Producer Span + +> Span created on the Kafka consumer side. + +**Span name** `kafka.produce`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.kafka.SleuthKafkaSpan` + +IMPORTANT: All tags and events must be prefixed with `kafka.` prefix! + +.Tag Keys +|=== +|Name | Description +|kafka.topic|Name of the Kafka topic. +|=== + === Messaging Span > Span created when message is sent or received. @@ -336,6 +435,41 @@ Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.in |quartz.trigger|Name of the trigger. |=== +=== R2dbc Query Span + +> Span created on the Kafka consumer side. + +**Span name** `query`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.r2dbc.SleuthR2dbcSpan` + +IMPORTANT: All tags and events must be prefixed with `r2dbc.` prefix! + +.Tag Keys +|=== +|Name | Description +|r2dbc.connection|Name of the R2DBC connection. +|r2dbc.query[%s]|Name of the R2DBC query. (since the name contains `%s` the final value will be resolved at runtime) +|r2dbc.thread|Name of the R2DBC thread. +|=== + +=== Rsocket Requester Span + +> Span created on the RSocket responder side. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.rsocket.SleuthRSocketSpan` + +IMPORTANT: All tags and events must be prefixed with `rsocket.` prefix! + +.Tag Keys +|=== +|Name | Description +|rsocket.request-type|Name of the R2DBC thread. +|rsocket.route|Name of the RSocket route. +|=== + === Rsocket Responder Span > Span created on the RSocket responder side. @@ -389,6 +523,27 @@ Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.in Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.task.SleuthTaskSpan` +=== Tx Span + +> Span created when there was no previous transaction. If there was one, we will continue it unless propagation is required. + +**Span name** `tx`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.tx.SleuthTxSpan` + +IMPORTANT: All tags and events must be prefixed with `tx.` prefix! + +.Tag Keys +|=== +|Name | Description +|tx.isolation-level|Transaction isolation level. +|tx.name|Transaction name. +|tx.propagation-level|Transaction propagation level. +|tx.read-only|Whether the transaction is read-only. +|tx.timeout|Transaction timeout. +|tx.transaction-manager|Name of the TransactionManager. +|=== + === Web Filter Span > Span around a WebFilter. Will continue the current span or create a new one and tag it diff --git a/docs/src/main/asciidoc/appendix.adoc b/docs/src/main/asciidoc/appendix.adoc index 216338c13..4798f62e5 100644 --- a/docs/src/main/asciidoc/appendix.adoc +++ b/docs/src/main/asciidoc/appendix.adoc @@ -1,22 +1,22 @@ -:numbered!: - -[[appendix]] -[[sleuth-spans]] -== Spring Cloud Sleuth Spans - -Below you can find a list of all the spans that are created by Spring Cloud Sleuth. - -include::_spans.adoc[] - -[[common-application-properties]] -== Common application properties - -include::_attributes.adoc[] - -Various properties can be specified inside your `application.properties` file, inside your `application.yml` file, or as command line switches. -This appendix provides a list of common {project-full-name} properties and references to the underlying classes that consume them. - -NOTE: Property contributions can come from additional jar files on your classpath, so you should not consider this an exhaustive list. -Also, you can define your own properties. - -include::_configprops.adoc[] \ No newline at end of file +:numbered!: + +[[appendix]] +[[common-application-properties]] +== Common application properties + +include::_attributes.adoc[] + +Various properties can be specified inside your `application.properties` file, inside your `application.yml` file, or as command line switches. +This appendix provides a list of common {project-full-name} properties and references to the underlying classes that consume them. + +NOTE: Property contributions can come from additional jar files on your classpath, so you should not consider this an exhaustive list. +Also, you can define your own properties. + +include::_configprops.adoc[] + +[[sleuth-spans]] +== Spring Cloud Sleuth Spans + +Below you can find a list of all the spans that are created by Spring Cloud Sleuth. + +include::_spans.adoc[] diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 8c6368eee..719002b3a 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -3,7 +3,7 @@ include::_attributes.adoc[] -In this section, we describe how to customize various parts of Spring Cloud Sleuth. +In this section, we describe how to customize various parts of Spring Cloud Sleuth. Please check the <> for the list of spans, tags and events. [[sleuth-kafka-integration]] == Apache Kafka @@ -650,3 +650,53 @@ This feature is available for all tracer implementations. We're adding an instrumented Tomcat's `Valve` that originates the span. In order to disable this instrumentation set `spring.sleuth.web.tomcat.enabled` to `false`. + + +[[sleuth-jdbc-integration]] +== Spring JDBC + +This feature is available for all tracer implementations. It has been ported from the https://github.com/gavlyukovskiy/spring-boot-data-source-decorator/[spring-boot-datasource-decorator] project. + +We're decorating `DataSource`s in a trace representation. We delegate actual proxying to either https://github.com/p6spy/p6spy[p6spy] or https://github.com/ttddyy/datasource-proxy[datasource-proxy]. In order to use this feature you need to have them on the classpath. + +==== +[source,xml,indent=0,subs="verbatim,attributes",role="primary"] +.P6Spy Maven +---- + + p6spy + p6spy + ${p6spy.version} + runtime + +---- + +[source,yml,indent=0,subs="verbatim,attributes",role="secondary"] +.P6Spy Gradle +---- +runtimeOnly "p6spy:p6spy:${p6spyVersion}" +---- + +[source,xml,indent=0,subs="verbatim,attributes",role="secondary"] +.Datasource Proxy Maven +---- + + net.ttddyy + datasource-proxy + ${datasource-proxy.version} + runtime + +---- + +[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] +.Datasource Proxy Gradle +---- +runtimeOnly "net.ttddyy:datasource-proxy:${datasourceProxyVersion}" +---- +==== + +Please check the <> page under `spring.sleuth.jdbc.decorator.datasource.p6spy` for all p6spy configuration options and `spring.sleuth.jdbc.decorator.datasource.datasource-proxy` for all datasource proxy configuration options. + +You can configure P6Spy manually using one of available configuration methods. For more information please refer to the http://p6spy.readthedocs.io/en/latest/configandusage.html[P6Spy Configuration Guide]. + +In order to disable this instrumentation set `spring.sleuth.jdbc.decorator.datasource.enabled` to `false`. diff --git a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc index d0a241950..d34261b40 100644 --- a/docs/src/main/asciidoc/spring-cloud-sleuth.adoc +++ b/docs/src/main/asciidoc/spring-cloud-sleuth.adoc @@ -1,18 +1,18 @@ -[[spring-cloud-sleuth-reference-documentation]] -= Spring Cloud Sleuth Reference Documentation -Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant - -:docinfo: shared -include::_attributes.adoc[] - -The reference documentation consists of the following sections: - -[horizontal] -<> :: Legal information. -<> :: About the Documentation, Getting Help, First Steps, and more. -<> :: Introducing {project-full-name}, Developing Your First {project-full-name}-based Application -<> :: {project-full-name} usage examples and workflows. -<> :: Span creation, context propagation, and more. -<> :: Add sampling, propagate remote tags, and more. -<> :: Instrumentation configuration, context propagation, and more. -<> :: Configuration properties. +[[spring-cloud-sleuth-reference-documentation]] += Spring Cloud Sleuth Reference Documentation +Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant + +:docinfo: shared +include::_attributes.adoc[] + +The reference documentation consists of the following sections: + +[horizontal] +<> :: Legal information. +<> :: About the Documentation, Getting Help, First Steps, and more. +<> :: Introducing {project-full-name}, Developing Your First {project-full-name}-based Application +<> :: {project-full-name} usage examples and workflows. +<> :: Span creation, context propagation, and more. +<> :: Add sampling, propagate remote tags, and more. +<> :: Instrumentation configuration, context propagation, and more. +<> :: Span definitions and configuration properties. diff --git a/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanEntry.java b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanEntry.java index 5788812ae..b11eb31eb 100644 --- a/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanEntry.java +++ b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanEntry.java @@ -35,11 +35,13 @@ class SpanEntry implements Comparable { final String description; + final String prefix; + final Collection tagKeys; final Collection events; - SpanEntry(String name, String enclosingClass, String enumName, String description, + SpanEntry(String name, String enclosingClass, String enumName, String description, String prefix, Collection tagKeys, Collection events) { Assert.isTrue(StringUtils.hasText(name), "Span name must not be empty"); Assert.isTrue(StringUtils.hasText(description), "Span description must not be empty"); @@ -47,6 +49,7 @@ class SpanEntry implements Comparable { this.enclosingClass = enclosingClass; this.enumName = enumName; this.description = description; + this.prefix = prefix; this.tagKeys = tagKeys; this.events = events; } @@ -89,6 +92,9 @@ class SpanEntry implements Comparable { text.append("."); } text.append("\n\n").append("Fully qualified name of the enclosing class `").append(this.enclosingClass).append("`"); + if (StringUtils.hasText(prefix)) { + text.append("\n\nIMPORTANT: All tags and events must be prefixed with `").append(this.prefix).append("` prefix!"); + } if (!tagKeys.isEmpty()) { text.append("\n\n.Tag Keys\n|===\n|Name | Description\n").append(this.tagKeys.stream().map(KeyValueEntry::toString).collect(Collectors.joining("\n"))) .append("\n|==="); diff --git a/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanSearchingFileVisitor.java b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanSearchingFileVisitor.java index 22c43411e..66f17122a 100644 --- a/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanSearchingFileVisitor.java +++ b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanSearchingFileVisitor.java @@ -99,6 +99,7 @@ class SpanSearchingFileVisitor extends SimpleFileVisitor { } String name = ""; String description = enumConstant.getJavaDoc().getText(); + String prefix = ""; Collection tags = new TreeSet<>(); Collection events = new TreeSet<>(); for (MemberSource member : members) { @@ -117,11 +118,12 @@ class SpanSearchingFileVisitor extends SimpleFileVisitor { else if ("getEvents".equals(methodName)) { events.addAll(keyValueEntries(myEnum, methodDeclaration, EventValue.class)); } - else { - return null; + else if ("prefix".equals(methodName)) { + prefix = readStringReturnValue(methodDeclaration); } } - return new SpanEntry(name, myEnum.getCanonicalName(), enumConstant.getName(), description, tags, events); + return new SpanEntry(name, myEnum.getCanonicalName(), enumConstant.getName(), description, prefix, tags, + events); } private Collection keyValueEntries(JavaEnumImpl myEnum, MethodDeclaration methodDeclaration, diff --git a/pom.xml b/pom.xml index 986d5be7d..00da89234 100644 --- a/pom.xml +++ b/pom.xml @@ -84,6 +84,10 @@ 3.3.0 3.0.1 2.2.0.RELEASE + 3.9.1 + 1.7 + 10.0.6 + 2.8.0 true @@ -293,6 +297,26 @@ brave-propagation-aws ${brave-propagation-aws.version} + + p6spy + p6spy + ${p6spy.version} + + + net.ttddyy + datasource-proxy + ${datasource-proxy.version} + + + org.apache.commons + commons-dbcp2 + ${commons-dbcp2.version} + + + org.apache.tomcat + tomcat-jdbc + ${tomcat-jdbc.version} + org.hamcrest hamcrest-core diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Span.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Span.java index 444b8e1eb..e0bf641d3 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Span.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/Span.java @@ -99,6 +99,17 @@ public interface Span extends SpanCustomizer { return this; } + /** + * Sets the remote url on the span. + * @param ip remote ip + * @param port remote port + * @return this span + * @since 3.1.0 + */ + default Span remoteIpAndPort(String ip, int port) { + return this; + } + /** * Type of span. Can be used to specify additional relationships between spans in * addition to a parent/child relationship. @@ -204,10 +215,11 @@ public interface Span extends SpanCustomizer { /** * Sets the remote URL for the span. - * @param remoteUrl remote service name + * @param ip remote service ip + * @param port remote service port * @return this */ - default Builder remoteUrl(String remoteUrl) { + default Builder remoteIpAndPort(String ip, int port) { return this; } diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java index 807e871c6..b61826426 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java @@ -16,13 +16,15 @@ package org.springframework.cloud.sleuth; +import java.io.Closeable; + /** * Container object for {@link Span} and its corresponding {@link Tracer.SpanInScope}. * * @author Marcin Grzejszczak * @since 3.1.0 */ -public class SpanAndScope { +public class SpanAndScope implements Closeable { private final Span span; @@ -46,4 +48,10 @@ public class SpanAndScope { return "SpanAndScope{" + "span=" + this.span + '}'; } + @Override + public void close() { + this.scope.close(); + this.span.end(); + } + } diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index 46a491f01..34c1f7bfa 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -188,7 +188,31 @@ spring-vault-core true - + + p6spy + p6spy + true + + + net.ttddyy + datasource-proxy + true + + + org.apache.commons + commons-dbcp2 + true + + + org.apache.tomcat + tomcat-jdbc + true + + + com.zaxxer + HikariCP + true + org.springframework.cloud diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/SleuthBaggageProperties.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/SleuthBaggageProperties.java index 9f629e987..64f5985df 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/SleuthBaggageProperties.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/SleuthBaggageProperties.java @@ -36,19 +36,24 @@ public class SleuthBaggageProperties { private boolean correlationEnabled = true; /** + * List of fields that should be propagated over the wire. */ private List correlationFields = new ArrayList<>(); + /** + * List of fields that should be accessible within the JVM process but not propagated + * over the wire. + */ private List localFields = new ArrayList<>(); /** * List of fields that are referenced the same in-process as it is on the wire. For * example, the field "x-vcap-request-id" would be set as-is including the prefix. - * */ private List remoteFields = new ArrayList<>(); /** + * List of fields that should automatically become tags. */ private List tagFields = new ArrayList<>(); diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java new file mode 100644 index 000000000..fb0daac9b --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java @@ -0,0 +1,120 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.ArrayList; +import java.util.List; + +import net.ttddyy.dsproxy.listener.MethodExecutionListener; +import net.ttddyy.dsproxy.listener.QueryCountStrategy; +import net.ttddyy.dsproxy.listener.QueryExecutionListener; +import net.ttddyy.dsproxy.listener.SingleQueryCountHolder; +import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; +import net.ttddyy.dsproxy.proxy.ResultSetProxyLogicFactory; +import net.ttddyy.dsproxy.proxy.SimpleResultSetProxyLogicFactory; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import net.ttddyy.dsproxy.transform.ParameterTransformer; +import net.ttddyy.dsproxy.transform.QueryTransformer; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyBuilderCustomizer; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyDataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyProperties; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; +import org.springframework.context.annotation.Bean; + +/** + * Configuration for integration with datasource-proxy, allows to use define custom + * {@link QueryExecutionListener}, {@link ParameterTransformer} and + * {@link QueryTransformer}. + * + * @author Arthur Gavlyukovskiy + */ +@ConditionalOnClass(ProxyDataSource.class) +class DataSourceProxyConfiguration { + + @Bean + @ConditionalOnMissingBean + DataSourceProxyConnectionIdManagerProvider traceConnectionIdManagerProvider() { + return GlobalConnectionIdManager::new; + } + + @Bean + DataSourceProxyBuilderCustomizer proxyDataSourceBuilderConfigurer( + ObjectProvider queryCountStrategy, + ObjectProvider> listeners, + ObjectProvider> methodExecutionListeners, + ObjectProvider parameterTransformer, + ObjectProvider queryTransformer, + ObjectProvider resultSetProxyLogicFactory, + ObjectProvider dataSourceProxyConnectionIdManagerProvider, + TraceDataSourceDecoratorProperties dataSourceDecoratorProperties) { + return new DataSourceProxyBuilderCustomizer(queryCountStrategy.getIfAvailable(() -> null), + listeners.getIfAvailable(() -> null), methodExecutionListeners.getIfAvailable(() -> null), + parameterTransformer.getIfAvailable(() -> null), queryTransformer.getIfAvailable(() -> null), + resultSetProxyLogicFactory.getIfAvailable(() -> null), + dataSourceProxyConnectionIdManagerProvider.getIfAvailable(() -> null), + props(dataSourceDecoratorProperties)); + } + + private DataSourceProxyProperties props(TraceDataSourceDecoratorProperties dataSourceDecoratorProperties) { + TraceDataSourceDecoratorProperties.DataSourceProxyProperties originalProxy = dataSourceDecoratorProperties + .getDatasourceProxy(); + DataSourceProxyProperties props = new DataSourceProxyProperties(); + BeanUtils.copyProperties(originalProxy, props); + props.setLogging(DataSourceProxyProperties.DataSourceProxyLogging.valueOf(originalProxy.getLogging().name())); + return props; + } + + @Bean + DataSourceProxyDataSourceDecorator proxyDataSourceDecorator( + DataSourceProxyBuilderCustomizer dataSourceProxyBuilderCustomizer, + DataSourceNameResolver dataSourceNameResolver) { + return new DataSourceProxyDataSourceDecorator(dataSourceProxyBuilderCustomizer, dataSourceNameResolver); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = "spring.sleuth.jdbc.decorator.datasource.datasource-proxy.count-query", + havingValue = "true") + QueryCountStrategy queryCountStrategy() { + return new SingleQueryCountHolder(); + } + + @Bean + TraceQueryExecutionListener traceQueryExecutionListener(Tracer tracer, + TraceDataSourceDecoratorProperties dataSourceDecoratorProperties, + ObjectProvider> customizers) { + return new TraceQueryExecutionListener(tracer, dataSourceDecoratorProperties.getIncludes(), + customizers.getIfAvailable(ArrayList::new)); + } + + @Bean + @ConditionalOnMissingBean + ResultSetProxyLogicFactory traceResultSetProxyLogicFactory() { + return new SimpleResultSetProxyLogicFactory(); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfiguration.java new file mode 100644 index 000000000..1597e77d4 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfiguration.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.ArrayList; +import java.util.List; + +import com.p6spy.engine.event.JdbcEventListener; +import com.p6spy.engine.spy.DefaultJdbcEventListenerFactory; +import com.p6spy.engine.spy.JdbcEventListenerFactory; +import com.p6spy.engine.spy.P6DataSource; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; +import org.springframework.cloud.sleuth.instrument.jdbc.P6SpyContextJdbcEventListenerFactory; +import org.springframework.cloud.sleuth.instrument.jdbc.P6SpyDataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; + +/** + * Configuration for integration with p6spy, allows to define custom + * {@link JdbcEventListener}. + * + * @author Arthur Gavlyukovskiy + */ +@ConditionalOnClass(P6DataSource.class) +class P6SpyConfiguration { + + @Bean + static P6SpyPropertiesSetter p6SpyPropertiesSetter(ConfigurableApplicationContext context) { + return new P6SpyPropertiesSetter(context); + } + + @Bean + @ConditionalOnMissingBean + JdbcEventListenerFactory traceJdbcEventListenerFactory(ObjectProvider> listeners) { + JdbcEventListenerFactory jdbcEventListenerFactory = new DefaultJdbcEventListenerFactory(); + List listenerList = listeners.getIfAvailable(() -> null); + return listenerList != null ? new P6SpyContextJdbcEventListenerFactory(jdbcEventListenerFactory, listenerList) + : jdbcEventListenerFactory; + } + + @Bean + P6SpyDataSourceDecorator p6SpyDataSourceDecorator(JdbcEventListenerFactory jdbcEventListenerFactory) { + return new P6SpyDataSourceDecorator(jdbcEventListenerFactory); + } + + @Bean + TraceJdbcEventListener tracingJdbcEventListener(Tracer tracer, DataSourceNameResolver dataSourceNameResolver, + TraceDataSourceDecoratorProperties dataSourceDecoratorProperties, + ObjectProvider> customizers) { + return new TraceJdbcEventListener(tracer, dataSourceNameResolver, dataSourceDecoratorProperties.getIncludes(), + dataSourceDecoratorProperties.getP6spy().getTracing().isIncludeParameterValues(), + customizers.getIfAvailable(ArrayList::new)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java new file mode 100644 index 000000000..dc97a2740 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java @@ -0,0 +1,189 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.p6spy.engine.logging.P6LogFactory; +import com.p6spy.engine.spy.P6ModuleManager; +import com.p6spy.engine.spy.P6SpyFactory; +import com.p6spy.engine.spy.option.EnvironmentVariables; +import com.p6spy.engine.spy.option.P6OptionsSource; +import com.p6spy.engine.spy.option.SpyDotProperties; +import com.p6spy.engine.spy.option.SystemProperties; +import org.slf4j.Logger; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +import static org.slf4j.LoggerFactory.getLogger; + +/** + * Sets p6spy properties to / from system properties. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +class P6SpyPropertiesSetter implements BeanDefinitionRegistryPostProcessor, Closeable { + + private static final Logger log = getLogger(P6SpyPropertiesSetter.class); + + private final ConfigurableApplicationContext context; + + private final Map initialP6SpyOptions; + + P6SpyPropertiesSetter(ConfigurableApplicationContext context) { + this.context = context; + this.initialP6SpyOptions = findDefinedOptions(); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + ConfigurableEnvironment environment = this.context.getEnvironment(); + String customModuleList = initialP6SpyOptions.get("modulelist"); + boolean isEnableLogging = environment + .getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.enable-logging", Boolean.class, true); + if (customModuleList != null) { + log.info("P6Spy modulelist is overridden, some p6spy configuration features will not be applied"); + } + else { + List moduleList = new ArrayList<>(); + // default factory, holds P6Spy configuration + moduleList.add(P6SpyFactory.class.getName()); + if (isEnableLogging) { + moduleList.add(P6LogFactory.class.getName()); + } + System.setProperty("p6spy.config.modulelist", String.join(",", moduleList)); + } + if (!initialP6SpyOptions.containsKey("logMessageFormat")) { + String logFormat = logFormat(environment); + boolean isMultiline = multiLine(environment); + if (logFormat != null) { + System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); + System.setProperty("p6spy.config.customLogMessageFormat", logFormat); + } + else if (isMultiline) { + System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.MultiLineFormat"); + } + } + if (isEnableLogging && !initialP6SpyOptions.containsKey("appender")) { + TraceDataSourceDecoratorProperties.P6SpyProperties.P6SpyLogging logging = TraceDataSourceDecoratorProperties.P6SpyProperties.P6SpyLogging + .valueOf(environment + .getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.logging", String.class, + TraceDataSourceDecoratorProperties.P6SpyProperties.P6SpyLogging.SLF4J.toString()) + .toUpperCase()); + switch (logging) { + case SYSOUT: + System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.StdoutLogger"); + break; + case SLF4J: + System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.Slf4JLogger"); + break; + case FILE: + System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.FileLogger"); + break; + case CUSTOM: + String customAppender = environment.getProperty( + "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class", String.class, ""); + System.setProperty("p6spy.config.appender", customAppender); + break; + } + } + if (!initialP6SpyOptions.containsKey("logfile")) { + String logFile = environment.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.log-file", + String.class, "spy.log"); + System.setProperty("p6spy.config.logfile", logFile); + } + String pattern = environment.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern", + String.class); + if (pattern != null) { + System.setProperty("p6spy.config.filter", "true"); + System.setProperty("p6spy.config.sqlexpression", pattern); + } + // If factories were loaded before this method is initialized changing properties + // will not be applied + // Changes done in this method could not override anything user specified, + // therefore it is safe to call reload + P6ModuleManager.getInstance().reload(); + } + + private Boolean multiLine(ConfigurableEnvironment environment) { + return environment.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.multiline", Boolean.class, true); + } + + private String logFormat(ConfigurableEnvironment environment) { + return environment.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.log-format", String.class); + } + + @Override + public void close() throws IOException { + if (!initialP6SpyOptions.containsKey("modulelist")) { + System.clearProperty("p6spy.config.modulelist"); + } + if (!initialP6SpyOptions.containsKey("logMessageFormat")) { + ConfigurableEnvironment environment = this.context.getEnvironment(); + String logFormat = logFormat(environment); + boolean isMultiline = multiLine(environment); + if (logFormat != null) { + System.clearProperty("p6spy.config.logMessageFormat"); + System.clearProperty("p6spy.config.customLogMessageFormat"); + } + else if (isMultiline) { + System.clearProperty("p6spy.config.logMessageFormat"); + } + } + if (!initialP6SpyOptions.containsKey("appender")) { + System.clearProperty("p6spy.config.appender"); + } + if (!initialP6SpyOptions.containsKey("logfile")) { + System.clearProperty("p6spy.config.logfile"); + } + P6ModuleManager.getInstance().reload(); + } + + private Map findDefinedOptions() { + SpyDotProperties spyDotProperties = null; + try { + spyDotProperties = new SpyDotProperties(); + } + catch (IOException ignored) { + } + return Stream.of(spyDotProperties, new EnvironmentVariables(), new SystemProperties()).filter(Objects::nonNull) + .map(P6OptionsSource::getOptions).filter(Objects::nonNull) + .flatMap(options -> options.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, + // always using value from the first P6OptionsSource + (value1, value2) -> value1)); + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorAutoConfiguration.java new file mode 100644 index 000000000..2066149c7 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorAutoConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import javax.sql.DataSource; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceHikariListenerStrategySpanCustomizer; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for proxying DataSource. + * + * @author Arthur Gavlyukovskiy + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(TraceDataSourceDecoratorProperties.class) +@ConditionalOnProperty(name = "spring.sleuth.jdbc.decorator.datasource.enabled", havingValue = "true", + matchIfMissing = true) +@ConditionalOnBean({ DataSource.class, Tracer.class }) +@AutoConfigureAfter(DataSourceAutoConfiguration.class) +@Import({ P6SpyConfiguration.class, DataSourceProxyConfiguration.class }) +public class TraceDataSourceDecoratorAutoConfiguration { + + @Bean + @ConditionalOnBean(DataSourceDecorator.class) + static TraceDataSourceDecoratorBeanPostProcessor traceDataSourceDecoratorBeanPostProcessor( + TraceDataSourceDecoratorProperties dataSourceDecoratorProperties) { + return new TraceDataSourceDecoratorBeanPostProcessor(dataSourceDecoratorProperties.getExcludedBeans()); + } + + @Bean + @ConditionalOnMissingBean + DataSourceNameResolver traceDataSourceNameResolver() { + return new DataSourceNameResolver(); + } + + @Bean + @ConditionalOnClass(name = "com.zaxxer.hikari.HikariDataSource") + TraceListenerStrategySpanCustomizer hikariTraceListenerStrategySpanCustomizer() { + return new TraceHikariListenerStrategySpanCustomizer(); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorBeanPostProcessor.java new file mode 100644 index 000000000..6fde67b16 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorBeanPostProcessor.java @@ -0,0 +1,140 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariDataSource; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.util.ClassUtils; + +/** + * {@link BeanPostProcessor} that wraps all data source beans in {@link DataSource} + * proxies specified in property 'spring.datasource.type'. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +public class TraceDataSourceDecoratorBeanPostProcessor implements BeanPostProcessor, Ordered, ApplicationContextAware { + + private static final Log log = LogFactory.getLog(TraceDataSourceDecoratorBeanPostProcessor.class); + + private final static boolean HIKARI_AVAILABLE = ClassUtils.isPresent("com.zaxxer.hikari.HikariDataSource", + DataSourceNameResolver.class.getClassLoader()); + + private ApplicationContext applicationContext; + + private DataSourceNameResolver dataSourceNameResolver; + + private final Collection excludedBeans; + + public TraceDataSourceDecoratorBeanPostProcessor(Collection excludedBeans) { + this.excludedBeans = excludedBeans; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof DataSource && !ScopedProxyUtils.isScopedTarget(beanName) + && !this.excludedBeans.contains(beanName)) { + // TODO: This might be a problem + Map decorators = this.applicationContext + .getBeansOfType(DataSourceDecorator.class).entrySet().stream() + .sorted(Entry.comparingByValue(AnnotationAwareOrderComparator.INSTANCE)) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (v1, v2) -> v2, LinkedHashMap::new)); + return decorate((DataSource) bean, getDataSourceName(bean, beanName), decorators); + } + else { + return bean; + } + } + + private String getDataSourceName(Object bean, String beanName) { + if (HIKARI_AVAILABLE && bean instanceof HikariDataSource) { + HikariDataSource hikariDataSource = (HikariDataSource) bean; + if (hikariDataSource.getPoolName() != null && !hikariDataSource.getPoolName().startsWith("HikariPool-")) { + return hikariDataSource.getPoolName(); + } + } + return beanName; + } + + private DataSource decorate(DataSource dataSource, String name, Map decorators) { + getDataSourceNameResolver().addDataSource(name, dataSource); + DataSource decoratedDataSource = dataSource; + for (Entry decoratorEntry : decorators.entrySet()) { + String decoratorBeanName = decoratorEntry.getKey(); + DataSourceDecorator decorator = decoratorEntry.getValue(); + DataSource dataSourceBeforeDecorating = decoratedDataSource; + decoratedDataSource = Objects.requireNonNull(decorator.decorate(name, decoratedDataSource), + "DataSourceDecorator (" + decoratorBeanName + ", " + decorator + ") should not return null"); + if (dataSourceBeforeDecorating != decoratedDataSource) { + getDataSourceNameResolver().addDataSource(name, decoratedDataSource); + } + } + if (dataSource != decoratedDataSource) { + if (log.isDebugEnabled()) { + log.debug("The decorated data source [" + decoratedDataSource + "] will replace the original one [" + + dataSource + "]"); + } + decoratedDataSource = new DataSourceWrapper(dataSource, decoratedDataSource); + getDataSourceNameResolver().addDataSource(name, decoratedDataSource); + } + return decoratedDataSource; + } + + private DataSourceNameResolver getDataSourceNameResolver() { + if (this.dataSourceNameResolver == null) { + this.dataSourceNameResolver = this.applicationContext.getBean(DataSourceNameResolver.class); + } + return this.dataSourceNameResolver; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorProperties.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorProperties.java new file mode 100644 index 000000000..2e655e903 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorProperties.java @@ -0,0 +1,502 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +import net.ttddyy.dsproxy.QueryCountHolder; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceType; + +/** + * Properties for configuring proxy providers. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +@ConfigurationProperties(prefix = "spring.sleuth.jdbc.decorator.datasource") +public class TraceDataSourceDecoratorProperties { + + /** + * Enables data source decorating. + */ + private boolean enabled = true; + + /** + * Beans that won't be decorated. + */ + private Collection excludedBeans = Collections.emptyList(); + + /** + * Which types of tracing we would like to include. + */ + private List includes = Arrays.asList(TraceType.CONNECTION, TraceType.QUERY, TraceType.FETCH); + + private DataSourceProxyProperties datasourceProxy = new DataSourceProxyProperties(); + + private P6SpyProperties p6spy = new P6SpyProperties(); + + public boolean isEnabled() { + return enabled; + } + + public Collection getExcludedBeans() { + return excludedBeans; + } + + public List getIncludes() { + return includes; + } + + public void setIncludes(List includes) { + this.includes = includes; + } + + public DataSourceProxyProperties getDatasourceProxy() { + return datasourceProxy; + } + + public P6SpyProperties getP6spy() { + return p6spy; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setExcludedBeans(Collection excludedBeans) { + this.excludedBeans = excludedBeans; + } + + public void setDatasourceProxy(DataSourceProxyProperties datasourceProxy) { + this.datasourceProxy = datasourceProxy; + } + + public void setP6spy(P6SpyProperties p6spy) { + this.p6spy = p6spy; + } + + /** + * Properties for datasource-proxy. + */ + public static class DataSourceProxyProperties { + + /** + * Logging to use for logging queries. + */ + private DataSourceProxyLogging logging = DataSourceProxyLogging.SLF4J; + + /** + * Query configuration. + */ + private Query query = new Query(); + + /** + * Slow query configuration. + */ + private SlowQuery slowQuery = new SlowQuery(); + + /** + * Use multiline output for logging query. + * + * @see ProxyDataSourceBuilder#multiline() + */ + private boolean multiline = true; + + /** + * Use json output for logging query. + * + * @see ProxyDataSourceBuilder#asJson() + */ + private boolean jsonFormat = false; + + /** + * Creates listener to count queries. + * + * @see ProxyDataSourceBuilder#countQuery() + * @see QueryCountHolder + */ + private boolean countQuery = false; + + public DataSourceProxyLogging getLogging() { + return logging; + } + + public void setLogging(DataSourceProxyLogging logging) { + this.logging = logging; + } + + public Query getQuery() { + return query; + } + + public void setQuery(Query query) { + this.query = query; + } + + public SlowQuery getSlowQuery() { + return slowQuery; + } + + public void setSlowQuery(SlowQuery slowQuery) { + this.slowQuery = slowQuery; + } + + public boolean isMultiline() { + return multiline; + } + + public void setMultiline(boolean multiline) { + this.multiline = multiline; + } + + public boolean isJsonFormat() { + return jsonFormat; + } + + public void setJsonFormat(boolean jsonFormat) { + this.jsonFormat = jsonFormat; + } + + public boolean isCountQuery() { + return countQuery; + } + + public void setCountQuery(boolean countQuery) { + this.countQuery = countQuery; + } + + /** + * Properties to configure query logging listener. + */ + public static class Query { + + /** + * Enable logging all queries to the log. + */ + private boolean enableLogging = true; + + /** + * Name of query logger. + */ + private String loggerName; + + /** + * Severity of query logger. + */ + private String logLevel = "DEBUG"; + + public boolean isEnableLogging() { + return enableLogging; + } + + public void setEnableLogging(boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public String getLoggerName() { + return loggerName; + } + + public void setLoggerName(String loggerName) { + this.loggerName = loggerName; + } + + public String getLogLevel() { + return logLevel; + } + + public void setLogLevel(String logLevel) { + this.logLevel = logLevel; + } + + } + + /** + * Properties to configure slow query logging listener. + */ + public static class SlowQuery { + + /** + * Enable logging slow queries to the log. + */ + private boolean enableLogging = true; + + /** + * Name of slow query logger. + */ + private String loggerName; + + /** + * Severity of slow query logger. + */ + private String logLevel = "WARN"; + + /** + * Number of seconds to consider query as slow. + */ + private long threshold = 300; + + boolean isEnableLogging() { + return enableLogging; + } + + public void setEnableLogging(boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public String getLoggerName() { + return loggerName; + } + + public void setLoggerName(String loggerName) { + this.loggerName = loggerName; + } + + public String getLogLevel() { + return logLevel; + } + + public void setLogLevel(String logLevel) { + this.logLevel = logLevel; + } + + public long getThreshold() { + return threshold; + } + + public void setThreshold(long threshold) { + this.threshold = threshold; + } + + } + + /** + * Query logging listener is the most used listener that logs executing query with + * actual parameters to. You can pick one of the following proxy logging + * mechanisms. + */ + public enum DataSourceProxyLogging { + + /** + * Log using System.out. + */ + SYSOUT, + + /** + * Log using SLF4J. + */ + SLF4J, + + /** + * Log using Commons. + */ + COMMONS, + + /** + * Log using Java Util Logging. + */ + JUL + + } + + } + + /** + * Properties for configuring p6spy. + */ + public static class P6SpyProperties { + + /** + * Enables logging JDBC events. + */ + private boolean enableLogging = true; + + /** + * Enables multiline output. + */ + private boolean multiline = true; + + /** + * Logging to use for logging queries. + */ + private P6SpyLogging logging = P6SpyLogging.SLF4J; + + /** + * Name of log file to use (only with logging=file). + */ + private String logFile = "spy.log"; + + /** + * Custom log format. + */ + private String logFormat; + + /** + * Tracing related properties. + */ + private P6SpyTracing tracing = new P6SpyTracing(); + + /** + * Class file to use (only with logging=custom). The class must implement + * {@link com.p6spy.engine.spy.appender.FormattedLogger}. + */ + private String customAppenderClass; + + /** + * Log filtering related properties. + */ + private P6SpyLogFilter logFilter = new P6SpyLogFilter(); + + public boolean isEnableLogging() { + return enableLogging; + } + + public void setEnableLogging(boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public boolean isMultiline() { + return multiline; + } + + public void setMultiline(boolean multiline) { + this.multiline = multiline; + } + + public P6SpyLogging getLogging() { + return logging; + } + + public void setLogging(P6SpyLogging logging) { + this.logging = logging; + } + + public String getLogFile() { + return logFile; + } + + public void setLogFile(String logFile) { + this.logFile = logFile; + } + + public String getLogFormat() { + return logFormat; + } + + public void setLogFormat(String logFormat) { + this.logFormat = logFormat; + } + + public P6SpyTracing getTracing() { + return tracing; + } + + public void setTracing(P6SpyTracing tracing) { + this.tracing = tracing; + } + + public String getCustomAppenderClass() { + return customAppenderClass; + } + + public void setCustomAppenderClass(String customAppenderClass) { + this.customAppenderClass = customAppenderClass; + } + + public P6SpyLogFilter getLogFilter() { + return logFilter; + } + + public void setLogFilter(P6SpyLogFilter logFilter) { + this.logFilter = logFilter; + } + + /** + * P6Spy logging options. + */ + public enum P6SpyLogging { + + /** + * Log using System.out. + */ + SYSOUT, + + /** + * Log using SLF4J. + */ + SLF4J, + + /** + * Log to file. + */ + FILE, + + /** + * Custom logging. + */ + CUSTOM + + } + + public static class P6SpyTracing { + + /** + * Report the effective sql string (with '?' replaced with real values) to + * tracing systems. + *

+ * NOTE this setting does not affect the logging message. + */ + private boolean includeParameterValues = true; + + public boolean isIncludeParameterValues() { + return includeParameterValues; + } + + public void setIncludeParameterValues(boolean includeParameterValues) { + this.includeParameterValues = includeParameterValues; + } + + } + + public static class P6SpyLogFilter { + + /** + * Use regex pattern to filter log messages. Only matched messages will be + * logged. + */ + private Pattern pattern; + + public Pattern getPattern() { + return pattern; + } + + public void setPattern(Pattern pattern) { + this.pattern = pattern; + } + + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index e4f77461c..becaaeab8 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -9,6 +9,7 @@ org.springframework.cloud.sleuth.autoconfig.instrument.batch.TraceBatchAutoConfi org.springframework.cloud.sleuth.autoconfig.instrument.config.TraceSpringCloudConfigAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.circuitbreaker.TraceCircuitBreakerAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.deployer.TraceDeployerAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.jdbc.TraceDataSourceDecoratorAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.rxjava.TraceRxJavaAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.quartz.TraceQuartzAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.task.TraceTaskAutoConfiguration,\ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java new file mode 100644 index 000000000..734b4f514 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java @@ -0,0 +1,370 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.util.concurrent.ThreadLocalRandom; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +import com.p6spy.engine.spy.P6DataSource; +import com.zaxxer.hikari.HikariDataSource; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import org.apache.commons.dbcp2.BasicDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class DataSourceDecoratorAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, + TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()); + + @Test + void testDecoratingInDefaultOrder() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + + DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; + assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(P6DataSource.class); + P6DataSource p6DataSource = (P6DataSource) DataSourceWrapper.getDecoratedDataSource(); + + DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); + assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; + }); + } + + @Test + void testNoDecoratingForExcludeBeans() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.excluded-beans:dataSource"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + }); + } + + @Test + void testDecoratingWhenDefaultProxyProviderNotAvailable() { + ApplicationContextRunner contextRunner = this.contextRunner; + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + assertThat(((DataSourceWrapper) dataSource).getOriginalDataSource()).isInstanceOf(HikariDataSource.class); + + DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; + assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(P6DataSource.class); + P6DataSource p6DataSource = (P6DataSource) DataSourceWrapper.getDecoratedDataSource(); + + DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); + assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; + + DataSource dsProxyWrappedDataSource = (DataSource) ReflectionTestUtils.getField(proxyDataSource, + "dataSource"); + assertThat(dsProxyWrappedDataSource).isEqualTo(DataSourceWrapper.getOriginalDataSource()); + }); + } + + @Test + void testDecoratedHikariSpecificPropertiesIsSet() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.datasource.type:" + HikariDataSource.class.getName(), + "spring.datasource.hikari.catalog:test_catalog"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isNotNull(); + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); + assertThat(realDataSource).isInstanceOf(HikariDataSource.class); + assertThat(((HikariDataSource) realDataSource).getCatalog()).isEqualTo("test_catalog"); + }); + } + + @Test + void testCustomDataSourceIsDecorated() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(TestDataSourceConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); + assertThat(realDataSource).isInstanceOf(BasicDataSource.class); + }); + } + + @Test + void testScopedDataSourceIsNotDecorated() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(TestScopedDataSourceConfiguration.class); + + contextRunner.run(context -> { + assertThat(context).getBeanNames(DataSource.class).containsOnly("dataSource", "scopedTarget.dataSource"); + assertThat(context).getBean("dataSource").isInstanceOf(DataSourceWrapper.class); + assertThat(context).getBean("scopedTarget.dataSource").isNotInstanceOf(DataSourceWrapper.class); + }); + } + + @Test + void testCustomDataSourceDecoratorApplied() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(TestDataSourceDecoratorConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isNotNull(); + + DataSource customDataSource = ((DataSourceWrapper) dataSource).getDecoratedDataSource(); + assertThat(customDataSource).isInstanceOf(CustomDataSourceProxy.class); + + DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); + assertThat(realDataSource).isInstanceOf(HikariDataSource.class); + + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + + DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; + + assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(CustomDataSourceProxy.class); + CustomDataSourceProxy customDataSourceProxy = (CustomDataSourceProxy) DataSourceWrapper + .getDecoratedDataSource(); + + assertThat(customDataSourceProxy.delegate).isInstanceOf(P6DataSource.class); + P6DataSource p6DataSource = (P6DataSource) customDataSourceProxy.delegate; + + DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); + assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; + }); + } + + @Test + void testDecoratingCanBeDisabled() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.enabled:false"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + }); + } + + @Test + void testDecoratingCanBeDisabledForSpecificBeans() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.excluded-beans:secondDataSource") + .withUserConfiguration(TestMultiDataSourceConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean("dataSource", DataSource.class); + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + + DataSource secondDataSource = context.getBean("secondDataSource", DataSource.class); + assertThat(secondDataSource).isInstanceOf(BasicDataSource.class); + }); + } + + @Test + void testDecoratingChainBuiltCorrectly() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + DataSourceWrapper dataSource1 = context.getBean(DataSourceWrapper.class); + assertThat(dataSource1).isNotNull(); + + DataSource p6DataSource = dataSource1.getDecoratedDataSource(); + assertThat(p6DataSource).isNotNull(); + assertThat(p6DataSource).isInstanceOf(P6DataSource.class); + + DataSource proxyDataSource = (DataSource) new DirectFieldAccessor(p6DataSource) + .getPropertyValue("realDataSource"); + assertThat(proxyDataSource).isNotNull(); + assertThat(proxyDataSource).isInstanceOf(ProxyDataSource.class); + }); + } + + @Test + void testDecorateDynamicallyRegisteredBeans() { + ApplicationContextRunner contextRunner = this.contextRunner.withInitializer(context -> { + GenericApplicationContext gac = (GenericApplicationContext) context; + gac.registerBean("ds1", DataSource.class, () -> new HikariDataSource()); + gac.registerBean("ds2", DataSource.class, () -> new HikariDataSource()); + }); + + contextRunner.run(context -> { + DataSource dataSource1 = context.getBean("ds1", DataSource.class); + assertThat(dataSource1).isNotNull(); + assertThat(dataSource1).isInstanceOf(DataSourceWrapper.class); + + DataSource dataSource2 = context.getBean("ds2", DataSource.class); + assertThat(dataSource2).isNotNull(); + assertThat(dataSource2).isInstanceOf(DataSourceWrapper.class); + }); + } + + @Configuration + static class TestDataSourceConfiguration { + + @Bean + public DataSource dataSource() { + BasicDataSource pool = new BasicDataSource(); + pool.setDriverClassName("org.hsqldb.jdbcDriver"); + pool.setUrl("jdbc:hsqldb:target/overridedb"); + pool.setUsername("sa"); + return pool; + } + + } + + @Configuration + static class TestDataSourceDecoratorConfiguration { + + @Bean + public DataSourceDecorator customDataSourceDecorator() { + return (beanName, dataSource) -> new CustomDataSourceProxy(dataSource); + } + + } + + @Configuration + static class TestMultiDataSourceConfiguration { + + @Bean + @Primary + public DataSource dataSource() { + BasicDataSource pool = new BasicDataSource(); + pool.setDriverClassName("org.hsqldb.jdbcDriver"); + pool.setUrl("jdbc:hsqldb:target/db"); + pool.setUsername("sa"); + return pool; + } + + @Bean + public DataSource secondDataSource() { + BasicDataSource pool = new BasicDataSource(); + pool.setDriverClassName("org.hsqldb.jdbcDriver"); + pool.setUrl("jdbc:hsqldb:target/db2"); + pool.setUsername("sa"); + return pool; + } + + } + + @Configuration + static class TestScopedDataSourceConfiguration { + + @Bean + @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) + public DataSource dataSource() { + BasicDataSource pool = new BasicDataSource(); + pool.setDriverClassName("org.hsqldb.jdbcDriver"); + pool.setUrl("jdbc:hsqldb:target/overridedb"); + pool.setUsername("sa"); + return pool; + } + + } + + /** + * Custom proxy data source for tests. + * + * @author Arthur Gavlyukovskiy + */ + static class CustomDataSourceProxy implements DataSource { + + private final DataSource delegate; + + CustomDataSourceProxy(DataSource delegate) { + this.delegate = delegate; + } + + @Override + public Connection getConnection() { + return null; + } + + @Override + public Connection getConnection(String username, String password) { + return null; + } + + @Override + public T unwrap(Class iface) { + return null; + } + + @Override + public boolean isWrapperFor(Class iface) { + return false; + } + + @Override + public PrintWriter getLogWriter() { + return null; + } + + @Override + public void setLogWriter(PrintWriter out) { + + } + + @Override + public void setLoginTimeout(int seconds) { + + } + + @Override + public int getLoginTimeout() { + return 0; + } + + @Override + public Logger getParentLogger() { + return null; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java new file mode 100644 index 000000000..d790fa2f0 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java @@ -0,0 +1,277 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import com.p6spy.engine.common.ConnectionInformation; +import com.p6spy.engine.common.P6LogQuery; +import com.p6spy.engine.event.CompoundJdbcEventListener; +import com.p6spy.engine.event.JdbcEventListener; +import com.p6spy.engine.logging.Category; +import com.p6spy.engine.logging.LoggingEventListener; +import com.p6spy.engine.spy.JdbcEventListenerFactory; +import com.p6spy.engine.spy.P6DataSource; +import com.p6spy.engine.spy.appender.CustomLineFormat; +import com.p6spy.engine.spy.appender.FormattedLogger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +class P6SpyConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, + TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); + + @BeforeEach + @AfterEach + void resetLogAccumulator() { + LogAccumulator.reset(); + } + + @Test + void testCustomListeners() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomListenerConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + GetCountingListener getCountingListener = context.getBean(GetCountingListener.class); + ClosingCountingListener closingCountingListener = context.getBean(ClosingCountingListener.class); + P6DataSource p6DataSource = (P6DataSource) ((DataSourceWrapper) dataSource).getDecoratedDataSource(); + assertThat(p6DataSource).extracting("jdbcEventListenerFactory").isEqualTo(jdbcEventListenerFactory); + + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).contains(getCountingListener, closingCountingListener); + assertThat(getCountingListener.connectionCount).isEqualTo(0); + + Connection connection1 = p6DataSource.getConnection(); + + assertThat(getCountingListener.connectionCount).isEqualTo(1); + assertThat(closingCountingListener.connectionCount).isEqualTo(0); + + Connection connection2 = p6DataSource.getConnection(); + + assertThat(getCountingListener.connectionCount).isEqualTo(2); + + connection1.close(); + + assertThat(closingCountingListener.connectionCount).isEqualTo(1); + + connection2.close(); + + assertThat(closingCountingListener.connectionCount).isEqualTo(2); + }); + } + + @Test + void testDoesNotRegisterLoggingListenerIfDisabled() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.enable-logging:false"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class") + .doesNotContain(LoggingEventListener.class); + }); + } + + @Test + void testCanSetCustomLoggingFormat() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.log-format:test %{connectionId}"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); + assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") + .isEqualTo(CustomLineFormat.class); + }); + } + + @Test + void testMultilineShouldNotOverrideCustomProperties() { + System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); + System.setProperty("p6spy.config.excludecategories", "debug"); + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.multiline:true"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); + assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") + .isEqualTo(CustomLineFormat.class); + }); + } + + @Test + void testUseCustomLogger() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom", + "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" + + LogAccumulator.class.getName()); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + dataSource.getConnection().close(); + + assertThat(P6LogQuery.getLogger()).isInstanceOf(LogAccumulator.class); + }); + } + + @Test + void testLogFilterPattern() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom", + "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), + "spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern:.*table1.*"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); + PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { + ps1.execute(); + ps2.execute(); + } + + assertThat(LogAccumulator.MESSAGES).hasSize(1); + assertThat(LogAccumulator.MESSAGES).allMatch(message -> message.contains("table1")); + }); + } + + @Test + void testLogFilterPatternMatchAll() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom", + "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), + "spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern:.*"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); + PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { + ps1.execute(); + ps2.execute(); + } + + assertThat(LogAccumulator.MESSAGES).hasSize(2); + }); + } + + @Configuration + static class CustomListenerConfiguration { + + @Bean + public GetCountingListener wrappingCountingListener() { + return new GetCountingListener(); + } + + @Bean + public ClosingCountingListener closingCountingListener() { + return new ClosingCountingListener(); + } + + } + + static class GetCountingListener extends JdbcEventListener { + + int connectionCount = 0; + + @Override + public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) { + connectionCount++; + } + + } + + static class ClosingCountingListener extends JdbcEventListener { + + int connectionCount = 0; + + @Override + public void onAfterConnectionClose(ConnectionInformation connectionInformation, SQLException e) { + connectionCount++; + } + + } + + public static class LogAccumulator extends FormattedLogger { + + static final List MESSAGES = new ArrayList<>(); + static final List EXCEPTIONS = new ArrayList<>(); + + public static void reset() { + MESSAGES.clear(); + EXCEPTIONS.clear(); + } + + @Override + public void logException(Exception e) { + EXCEPTIONS.add(e); + } + + @Override + public void logText(String text) { + MESSAGES.add(text); + } + + @Override + public boolean isCategoryEnabled(Category category) { + return true; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java new file mode 100644 index 000000000..d9103ae27 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java @@ -0,0 +1,238 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import net.ttddyy.dsproxy.ExecutionInfo; +import net.ttddyy.dsproxy.QueryInfo; +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.listener.QueryExecutionListener; +import net.ttddyy.dsproxy.listener.logging.CommonsQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.CommonsSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.JULQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.JULSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.SystemOutSlowQueryListener; +import net.ttddyy.dsproxy.proxy.DefaultConnectionIdManager; +import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import net.ttddyy.dsproxy.transform.ParameterTransformer; +import net.ttddyy.dsproxy.transform.QueryTransformer; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import static org.assertj.core.api.Assertions.assertThat; + +class ProxyDataSourceConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, + TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("com.p6spy")); + + @Test + void testRegisterLogAndSlowQueryLogByDefaultToSlf4j() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogByUsingSlf4j() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasource-proxy.logging:slf4j"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingSystemOut() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasource-proxy.logging:sysout"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingJUL() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasourceProxy.logging:jul"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(JULSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(JULQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingApacheCommons() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasourceProxy.logging:commons"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(CommonsSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(CommonsQueryLoggingListener.class); + }); + } + + @Test + void testCustomParameterAndQueryTransformer() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomDataSourceProxyConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ParameterTransformer parameterTransformer = context.getBean(ParameterTransformer.class); + QueryTransformer queryTransformer = context.getBean(QueryTransformer.class); + assertThat(proxyDataSource.getProxyConfig().getParameterTransformer()).isSameAs(parameterTransformer); + assertThat(proxyDataSource.getProxyConfig().getQueryTransformer()).isSameAs(queryTransformer); + }); + } + + @Test + void testCustomListeners() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomListenerConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + QueryExecutionListener queryExecutionListener = context.getBean(QueryExecutionListener.class); + + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).contains(queryExecutionListener); + }); + } + + @Test + void testGlobalConnectionIdManagerByDefault() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + + assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(GlobalConnectionIdManager.class); + }); + } + + @Test + void testCustomConnectionIdManager() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomDataSourceProxyConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + + assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(DefaultConnectionIdManager.class); + }); + } + + @Configuration + static class CustomDataSourceProxyConfiguration { + + @Bean + public ParameterTransformer parameterTransformer() { + return (replacer, transformInfo) -> { + }; + } + + @Bean + public QueryTransformer queryTransformer() { + return (transformInfo) -> "TestQuery"; + } + + @Bean + public DataSourceProxyConnectionIdManagerProvider connectionIdManagerProvider() { + return DefaultConnectionIdManager::new; + } + + } + + @Configuration + static class CustomListenerConfiguration { + + @Bean + @Primary + public QueryExecutionListener queryExecutionListener() { + return new QueryExecutionListener() { + @Override + public void beforeQuery(ExecutionInfo execInfo, List queryInfoList) { + System.out.println("beforeQuery"); + } + + @Override + public void afterQuery(ExecutionInfo execInfo, List queryInfoList) { + System.out.println("afterQuery"); + } + }; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java new file mode 100644 index 000000000..8255031a5 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.concurrent.ThreadLocalRandom; + +import com.p6spy.engine.event.CompoundJdbcEventListener; +import com.p6spy.engine.spy.JdbcEventListenerFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; + +import static org.assertj.core.api.Assertions.assertThat; + +class SleuthP6SpyListenerAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, + TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); + + @Test + void testAddsP6SpyListener() { + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + assertThat(jdbcEventListener.getEventListeners()).extracting("class") + .contains(TraceJdbcEventListener.class); + }); + } + + @Test + void testDoesNotAddP6SpyListenerIfNoTracer() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues("spring.sleuth.enabled:false"); + + contextRunner.run(context -> { + assertThat(context).doesNotHaveBean(JdbcEventListenerFactory.class); + }); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java new file mode 100644 index 000000000..9b190eb57 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; + +import static org.assertj.core.api.Assertions.assertThat; + +class SleuthProxyDataSourceListenerAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, + TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("com.p6spy")); + + @Test + void testAddsDatasourceProxyListener() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(TraceQueryExecutionListener.class); + }); + } + + @Test + void testDoesNotAddDatasourceProxyListenerIfNoTracer() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues("spring.sleuth.enabled:false"); + + contextRunner.run(context -> { + assertThat(context).doesNotHaveBean(DataSourceWrapper.class); + }); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TestSpanHandlerConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TestSpanHandlerConfiguration.java new file mode 100644 index 000000000..78deb853e --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TestSpanHandlerConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import brave.sampler.Sampler; +import brave.test.TestSpanHandler; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +class TestSpanHandlerConfiguration { + + @Bean + public TestSpanHandler spanHandler() { + return new TestSpanHandler(); + } + + @Bean + public Sampler sampler() { + return Sampler.ALWAYS_SAMPLE; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java new file mode 100644 index 000000000..6a7c231eb --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +import javax.sql.DataSource; + +import brave.handler.MutableSpan; +import brave.test.TestSpanHandler; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +class TracingJdbcEventListenerTests extends TracingListenerStrategyTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, + TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") + .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); + + protected TracingJdbcEventListenerTests() { + super(new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, + TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") + .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy"))); + } + + @Test + void testShouldUsePlaceholderInSqlTagOfSpansForPreparedStatementIfIncludeParameterValuesIsSetToFalse() { + contextRunner + .withPropertyValues( + "spring.sleuth.jdbc.decorator.datasource.p6spy.tracing.include-parameter-values=false") + .run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + PreparedStatement preparedStatement = connection + .prepareStatement("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = ? WHERE 0 = ?"); + preparedStatement.setString(1, ""); + preparedStatement.setInt(2, 1); + preparedStatement.executeUpdate(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan connectionSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("update"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, + "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = ? WHERE 0 = ?"); + assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); + }); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java new file mode 100644 index 000000000..a33d0ec90 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java @@ -0,0 +1,720 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.sql.DataSource; + +import brave.handler.MutableSpan; +import brave.test.TestSpanHandler; +import com.zaxxer.hikari.HikariDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; +import org.springframework.context.annotation.Bean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +abstract class TracingListenerStrategyTests { + + public static final String SPAN_SQL_QUERY_TAG_NAME = "jdbc.query"; + + public static final String SPAN_ROW_COUNT_TAG_NAME = "jdbc.row-count"; + + protected final ApplicationContextRunner contextRunner; + + protected TracingListenerStrategyTests(ApplicationContextRunner contextRunner) { + this.contextRunner = contextRunner; + } + + @Test + void testShouldAddSpanForConnection() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.commit(); + connection.rollback(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(1); + MutableSpan connectionSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(connectionSpan.remoteServiceName()).isEqualTo("TESTDB-BAZ"); + assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.commit"); + assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.rollback"); + }); + } + + @Test + void testShouldAddSpanForConnectionWithFixedRemoteServiceName() { + contextRunner.withPropertyValues("spring.datasource.url:jdbc:h2:mem:testdb-baz?sleuthServiceName=aaaabbbb") + .run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.commit(); + connection.rollback(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(1); + MutableSpan connectionSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(connectionSpan.remoteServiceName()).isEqualTo("aaaabbbb"); + assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.commit"); + assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.rollback"); + }); + } + + @Test + void testShouldAddSpanForPreparedStatementExecute() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.prepareStatement("SELECT NOW()").execute(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan connectionSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(statementSpan.remoteServiceName()).isEqualTo("TESTDB-BAZ"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + }); + } + + @Test + void testShouldAddSpanForPreparedStatementExecuteUpdate() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.prepareStatement("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1") + .executeUpdate(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan connectionSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("update"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, + "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); + assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); + }); + } + + @Test + void testShouldAddSpanForStatementExecuteUpdate() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.createStatement() + .executeUpdate("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan connectionSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("update"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, + "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); + assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); + }); + } + + @Test + void testShouldAddSpanForPreparedStatementExecuteQueryIncludingTimeToCloseResultSet() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + ResultSet resultSet = connection.prepareStatement("SELECT NOW() UNION ALL select NOW()").executeQuery(); + resultSet.next(); + resultSet.next(); + resultSet.close(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(3); + MutableSpan connectionSpan = spanReporter.spans().get(2); + MutableSpan resultSetSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, + "SELECT NOW() UNION ALL select NOW()"); + assertThat(resultSetSpan.name()).isEqualTo("result-set"); + assertThat(resultSetSpan.remoteServiceName()).isEqualTo("TESTDB-BAZ"); + if (isP6Spy(context)) { + assertThat(resultSetSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "2"); + } + }); + } + + @Test + void testShouldAddSpanForStatementAndResultSet() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + ResultSet resultSet = connection.createStatement().executeQuery("SELECT NOW()"); + resultSet.next(); + Thread.sleep(200L); + resultSet.close(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(3); + MutableSpan connectionSpan = spanReporter.spans().get(2); + MutableSpan resultSetSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(resultSetSpan.name()).isEqualTo("result-set"); + if (isP6Spy(context)) { + assertThat(resultSetSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "1"); + } + }); + } + + @Test + void testShouldNotFailWhenStatementIsClosedWihoutResultSet() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + statement.close(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(3); + MutableSpan connectionSpan = spanReporter.spans().get(2); + MutableSpan resultSetSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(resultSetSpan.name()).isEqualTo("result-set"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + }); + } + + @Test + void testShouldNotFailWhenConnectionIsClosedWihoutResultSet() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(3); + MutableSpan connectionSpan = spanReporter.spans().get(2); + MutableSpan resultSetSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(resultSetSpan.name()).isEqualTo("result-set"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + }); + } + + @Test + void testShouldNotFailWhenResultSetNextWasNotCalled() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(3); + MutableSpan connectionSpan = spanReporter.spans().get(2); + MutableSpan resultSetSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(resultSetSpan.name()).isEqualTo("result-set"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + }); + } + + @Test + void testShouldNotFailWhenResourceIsAlreadyClosed() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + resultSet.close(); + resultSet.close(); + statement.close(); + statement.close(); + connection.close(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(3); + MutableSpan connectionSpan = spanReporter.spans().get(2); + MutableSpan resultSetSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(resultSetSpan.name()).isEqualTo("result-set"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + }); + } + + @Test + void testShouldNotFailWhenResourceIsAlreadyClosed2() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + assertThatThrownBy(() -> { + connection.close(); + connection.prepareStatement("SELECT NOW()"); + }).isInstanceOf(SQLException.class); + + assertThat(spanReporter.spans()).hasSize(1); + MutableSpan connectionSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + }); + } + + @Test + void testShouldNotFailWhenResourceIsAlreadyClosed3() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + assertThatThrownBy(() -> { + statement.close(); + statement.executeQuery("SELECT NOW()"); + }).isInstanceOf(SQLException.class); + connection.close(); + }); + } + + @Test + void testShouldNotFailWhenResourceIsAlreadyClosed4() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + assertThatThrownBy(() -> { + resultSet.close(); + resultSet.next(); + }).isInstanceOf(SQLException.class); + statement.close(); + connection.close(); + }); + } + + @Test + void testShouldNotFailToCloseSpanForTwoConsecutiveConnections() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection1 = dataSource.getConnection(); + Connection connection2 = dataSource.getConnection(); + connection1.close(); + connection2.close(); + + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan connection1Span = spanReporter.spans().get(0); + MutableSpan connection2Span = spanReporter.spans().get(1); + assertThat(connection1Span.name()).isEqualTo("connection"); + assertThat(connection2Span.name()).isEqualTo("connection"); + }); + } + + @Test + void testShouldNotFailWhenClosedInReversedOrder() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + connection.close(); + statement.close(); + resultSet.close(); + + assertThat(spanReporter.spans()).hasSize(3); + MutableSpan connectionSpan = spanReporter.spans().get(2); + MutableSpan resultSetSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(resultSetSpan.name()).isEqualTo("result-set"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + }); + } + + @Test + @SuppressWarnings("unchecked") + void testShouldNotCauseMemoryLeakOnTomcatPool() { + contextRunner.withPropertyValues("spring.datasource.type:org.apache.tomcat.jdbc.pool.DataSource") + .run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + Object listener = isP6Spy(context) ? context.getBean(TraceJdbcEventListener.class) + : context.getBean(TraceQueryExecutionListener.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(listener).extracting("strategy").extracting("openConnections") + .isInstanceOfSatisfying(Map.class, map -> assertThat(map).isEmpty()); + }); + } + + private boolean isP6Spy(AssertableApplicationContext context) { + if (context.getBeansOfType(TraceJdbcEventListener.class).size() == 1) { + return true; + } + else if (context.getBeansOfType(TraceQueryExecutionListener.class).size() == 1) { + return false; + } + else { + throw new IllegalStateException("Expected exactly 1 tracing listener bean in the context."); + } + } + + @Test + void testSingleConnectionAcrossMultipleThreads() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + IntStream.range(0, 5).mapToObj(i -> CompletableFuture.runAsync(() -> { + try { + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + statement.close(); + resultSet.close(); + } + catch (SQLException e) { + throw new IllegalStateException(e); + } + })).collect(Collectors.toList()).forEach(CompletableFuture::join); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(1 + 2 * 5); + assertThat(spanReporter.spans()).extracting("name").contains("select", "result-set", "connection"); + }); + } + + @Test + void testShouldIncludeOnlyConnectionTraces() { + contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: connection") + .run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(1); + MutableSpan connectionSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + }); + } + + @Test + void testShouldIncludeOnlyQueryTraces() { + contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: query").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(statementSpan.name()).isEqualTo("select"); + }); + } + + @Test + void testShouldIncludeOnlyFetchTraces() { + contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: fetch").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(1); + MutableSpan resultSetSpan = spanReporter.spans().get(0); + assertThat(resultSetSpan.name()).isEqualTo("result-set"); + }); + } + + @Test + void testShouldIncludeOnlyConnectionAndQueryTraces() { + contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: connection, query") + .run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan connectionSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("select"); + }); + } + + @Test + void testShouldIncludeOnlyConnectionAndFetchTraces() { + contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: connection, fetch") + .run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan connectionSpan = spanReporter.spans().get(1); + MutableSpan resultSetSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(resultSetSpan.name()).isEqualTo("result-set"); + }); + } + + @Test + void testShouldIncludeOnlyQueryAndFetchTraces() { + contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: query, fetch") + .run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan resultSetSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(resultSetSpan.name()).isEqualTo("result-set"); + }); + } + + @Test + void testShouldNotOverrideExceptionWhenConnectionWasClosedBeforeExecutingQuery() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT NOW()"); + connection.close(); + + assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class); + + assertThat(spanReporter.spans()).hasSize(1); + MutableSpan connectionSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + }); + } + + @Test + void testShouldNotOverrideExceptionWhenStatementWasClosedBeforeExecutingQuery() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT NOW()"); + statement.close(); + assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan connectionSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("select"); + }); + } + + @Test + void testShouldNotOverrideExceptionWhenResultSetWasClosedBeforeNext() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.close(); + assertThatThrownBy(resultSet::next).isInstanceOf(SQLException.class); + statement.close(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(3); + MutableSpan connectionSpan = spanReporter.spans().get(2); + MutableSpan resultSetSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(resultSetSpan.name()).isEqualTo("result-set"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + }); + } + + @Test + void testShouldNotFailWhenClosingConnectionFromDifferentDataSource() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(MultiDataSourceConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource1 = context.getBean("test1", DataSource.class); + DataSource dataSource2 = context.getBean("test2", DataSource.class); + + dataSource1.getConnection().close(); + dataSource2.getConnection().close(); + + CompletableFuture future = CompletableFuture.runAsync(() -> { + try { + Connection connection1 = dataSource1.getConnection(); + PreparedStatement statement = connection1.prepareStatement("SELECT NOW()"); + ResultSet resultSet = statement.executeQuery(); + Thread.sleep(200); + resultSet.close(); + statement.close(); + connection1.close(); + } + catch (SQLException | InterruptedException e) { + throw new IllegalStateException(e); + } + }); + Thread.sleep(100); + Connection connection2 = dataSource2.getConnection(); + Thread.sleep(300); + connection2.close(); + + future.join(); + }); + } + + private static class MultiDataSourceConfiguration { + + @Bean + public HikariDataSource test1() { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setJdbcUrl("jdbc:h2:mem:testdb-1-foo"); + dataSource.setPoolName("test1"); + return dataSource; + } + + @Bean + public HikariDataSource test2() { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setJdbcUrl("jdbc:h2:mem:testdb-2-bar"); + dataSource.setPoolName("test2"); + return dataSource; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingQueryExecutionListenerTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingQueryExecutionListenerTests.java new file mode 100644 index 000000000..89e7c601a --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingQueryExecutionListenerTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; + +class TracingQueryExecutionListenerTests extends TracingListenerStrategyTests { + + TracingQueryExecutionListenerTests() { + super(new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, + TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") + .withClassLoader(new FilteredClassLoader("com.p6spy"))); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java index f5f6435f5..165e728c6 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java @@ -26,7 +26,6 @@ import org.springframework.boot.actuate.autoconfigure.security.servlet.Managemen import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; @@ -35,7 +34,6 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; import org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration; import org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration; -import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.instrument.quartz.TracingJobListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -47,8 +45,8 @@ import org.springframework.context.annotation.Primary; public class TraceQuartzAutoConfigurationTest { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("spring.sleuth.noop.enabled=true").withConfiguration( - AutoConfigurations.of(SchedulerConfig.class, TracingConfig.class, EnableAutoConfig.class)); + .withPropertyValues("spring.sleuth.noop.enabled=true") + .withConfiguration(AutoConfigurations.of(SchedulerConfig.class, EnableAutoConfig.class)); @Test public void should_create_job_listener_bean_when_all_conditions_are_met() { @@ -94,9 +92,10 @@ public class TraceQuartzAutoConfigurationTest { @Test public void should_not_create_listener_when_scheduler_bean_is_not_present() { - new ApplicationContextRunner().withConfiguration(AutoConfigurations - // given - .of(TracingConfig.class, TraceQuartzAutoConfiguration.class)) + new ApplicationContextRunner().withPropertyValues("spring.sleuth.noop.enabled=true") + .withConfiguration(AutoConfigurations + // given + .of(TraceQuartzAutoConfiguration.class)) // when .run(context -> { @@ -138,16 +137,4 @@ public class TraceQuartzAutoConfigurationTest { } - @Configuration(proxyBeanMethods = false) - @AutoConfigureBefore(TraceQuartzAutoConfiguration.class) - @ConditionalOnProperty("spring.sleuth.noop.enabled") - public static class TracingConfig { - - @Bean - public Tracer testTracer() { - return Mockito.mock(Tracer.class); - } - - } - } diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpan.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpan.java index 99510fb4b..98ea9bd9d 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpan.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpan.java @@ -97,6 +97,12 @@ public class BraveSpan implements Span { return this; } + @Override + public Span remoteIpAndPort(String ip, int port) { + this.delegate.remoteIpAndPort(ip, port); + return this; + } + @Override public String toString() { return this.delegate != null ? this.delegate.toString() : "null"; diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanBuilder.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanBuilder.java index 5246cd266..ffdcb0c93 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanBuilder.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/BraveSpanBuilder.java @@ -16,12 +16,8 @@ package org.springframework.cloud.sleuth.brave.bridge; -import java.net.URI; - import brave.Tracer; import brave.propagation.TraceContextOrSamplingFlags; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.TraceContext; @@ -34,8 +30,6 @@ import org.springframework.cloud.sleuth.TraceContext; */ class BraveSpanBuilder implements Span.Builder { - private static final Log log = LogFactory.getLog(BraveSpanBuilder.class); - brave.Span delegate; TraceContextOrSamplingFlags parentContext; @@ -114,16 +108,8 @@ class BraveSpanBuilder implements Span.Builder { } @Override - public Span.Builder remoteUrl(String remoteUrl) { - try { - URI uri = URI.create(remoteUrl); - span().remoteIpAndPort(uri.getHost(), uri.getPort()); - } - catch (Exception e) { - if (log.isDebugEnabled()) { - log.debug("Failed to parse url [" + remoteUrl + "]. Will not set the value", e); - } - } + public Span.Builder remoteIpAndPort(String ip, int port) { + span().remoteIpAndPort(ip, port); return this; } diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index f83080117..17b95a094 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -198,6 +198,17 @@ true + + p6spy + p6spy + true + + + net.ttddyy + datasource-proxy + true + + org.springframework.boot spring-boot-starter-test diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceDecorator.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceDecorator.java new file mode 100644 index 000000000..fa2aefb37 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceDecorator.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import javax.sql.DataSource; + +/** + * Decorator for context {@link DataSource} beans. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +public interface DataSourceDecorator { + + /** + * Decorates given {@link DataSource} instance. Should either return wrapped + * {@link DataSource} or same instance. + * @param beanName name of a bean + * @param dataSource bean instance + * @return decorated {@link DataSource} or given {@link DataSource} without changes. + */ + DataSource decorate(String beanName, DataSource dataSource); + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceNameResolver.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceNameResolver.java new file mode 100644 index 000000000..b0d7ec0ec --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceNameResolver.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.util.concurrent.ConcurrentHashMap; + +import javax.sql.CommonDataSource; + +/** + * {@link CommonDataSource} name resolver based on bean name. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +public class DataSourceNameResolver { + + private final ConcurrentHashMap cachedNames = new ConcurrentHashMap<>(); + + public void addDataSource(String name, CommonDataSource dataSource) { + this.cachedNames.put(dataSource, name); + } + + public String resolveDataSourceName(CommonDataSource dataSource) { + return this.cachedNames.getOrDefault(dataSource, "dataSource"); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyBuilderCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyBuilderCustomizer.java new file mode 100644 index 000000000..87fff522b --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyBuilderCustomizer.java @@ -0,0 +1,202 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.logging.Level; + +import net.ttddyy.dsproxy.listener.MethodExecutionListener; +import net.ttddyy.dsproxy.listener.QueryCountStrategy; +import net.ttddyy.dsproxy.listener.QueryExecutionListener; +import net.ttddyy.dsproxy.listener.logging.CommonsLogLevel; +import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; +import net.ttddyy.dsproxy.proxy.ResultSetProxyLogicFactory; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; +import net.ttddyy.dsproxy.transform.ParameterTransformer; +import net.ttddyy.dsproxy.transform.QueryTransformer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.lang.Nullable; + +/** + * Configurer for {@link ProxyDataSourceBuilder} based on the application context. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + * @see ProxyDataSourceBuilder + */ +public class DataSourceProxyBuilderCustomizer { + + private static final Log log = LogFactory.getLog(DataSourceProxyBuilderCustomizer.class); + + private final QueryCountStrategy queryCountStrategy; + + private final List listeners; + + private final List methodExecutionListeners; + + private final ParameterTransformer parameterTransformer; + + private final QueryTransformer queryTransformer; + + private final ResultSetProxyLogicFactory resultSetProxyLogicFactory; + + private final DataSourceProxyConnectionIdManagerProvider dataSourceProxyConnectionIdManagerProvider; + + private final DataSourceProxyProperties datasourceProxy; + + public DataSourceProxyBuilderCustomizer(@Nullable QueryCountStrategy queryCountStrategy, + @Nullable List listeners, + @Nullable List methodExecutionListeners, + @Nullable ParameterTransformer parameterTransformer, @Nullable QueryTransformer queryTransformer, + @Nullable ResultSetProxyLogicFactory resultSetProxyLogicFactory, + @Nullable DataSourceProxyConnectionIdManagerProvider dataSourceProxyConnectionIdManagerProvider, + DataSourceProxyProperties datasourceProxy) { + this.queryCountStrategy = queryCountStrategy; + this.listeners = listeners; + this.methodExecutionListeners = methodExecutionListeners; + this.parameterTransformer = parameterTransformer; + this.queryTransformer = queryTransformer; + this.resultSetProxyLogicFactory = resultSetProxyLogicFactory; + this.dataSourceProxyConnectionIdManagerProvider = dataSourceProxyConnectionIdManagerProvider; + this.datasourceProxy = datasourceProxy; + } + + public ProxyDataSourceBuilder customize(ProxyDataSourceBuilder proxyDataSourceBuilder) { + switch (this.datasourceProxy.getLogging()) { + case SLF4J: + if (this.datasourceProxy.getQuery().isEnableLogging()) { + proxyDataSourceBuilder.logQueryBySlf4j(toSlf4JLogLevel(this.datasourceProxy.getQuery().getLogLevel()), + this.datasourceProxy.getQuery().getLoggerName()); + } + if (this.datasourceProxy.getSlowQuery().isEnableLogging()) { + proxyDataSourceBuilder.logSlowQueryBySlf4j(this.datasourceProxy.getSlowQuery().getThreshold(), + TimeUnit.SECONDS, toSlf4JLogLevel(this.datasourceProxy.getSlowQuery().getLogLevel()), + datasourceProxy.getSlowQuery().getLoggerName()); + } + break; + case JUL: + if (this.datasourceProxy.getQuery().isEnableLogging()) { + proxyDataSourceBuilder.logQueryByJUL(toJULLogLevel(this.datasourceProxy.getQuery().getLogLevel()), + this.datasourceProxy.getQuery().getLoggerName()); + } + if (this.datasourceProxy.getSlowQuery().isEnableLogging()) { + proxyDataSourceBuilder.logSlowQueryByJUL(this.datasourceProxy.getSlowQuery().getThreshold(), + TimeUnit.SECONDS, toJULLogLevel(this.datasourceProxy.getSlowQuery().getLogLevel()), + this.datasourceProxy.getSlowQuery().getLoggerName()); + } + break; + case COMMONS: + if (this.datasourceProxy.getQuery().isEnableLogging()) { + proxyDataSourceBuilder.logQueryByCommons( + toCommonsLogLevel(this.datasourceProxy.getQuery().getLogLevel()), + datasourceProxy.getQuery().getLoggerName()); + } + if (this.datasourceProxy.getSlowQuery().isEnableLogging()) { + proxyDataSourceBuilder.logSlowQueryByCommons(this.datasourceProxy.getSlowQuery().getThreshold(), + TimeUnit.SECONDS, toCommonsLogLevel(this.datasourceProxy.getSlowQuery().getLogLevel()), + this.datasourceProxy.getSlowQuery().getLoggerName()); + } + break; + case SYSOUT: + if (this.datasourceProxy.getQuery().isEnableLogging()) { + proxyDataSourceBuilder.logQueryToSysOut(); + } + if (this.datasourceProxy.getSlowQuery().isEnableLogging()) { + proxyDataSourceBuilder.logSlowQueryToSysOut(this.datasourceProxy.getSlowQuery().getThreshold(), + TimeUnit.SECONDS); + } + break; + } + if (this.datasourceProxy.isMultiline() && this.datasourceProxy.isJsonFormat()) { + log.warn( + "Found opposite multiline and json format, multiline will be used (may depend on library version)"); + } + if (this.datasourceProxy.isMultiline()) { + proxyDataSourceBuilder.multiline(); + } + if (this.datasourceProxy.isJsonFormat()) { + proxyDataSourceBuilder.asJson(); + } + if (this.datasourceProxy.isCountQuery()) { + ifAvailable(this.queryCountStrategy, proxyDataSourceBuilder::countQuery); + } + ifAvailable(this.listeners, l -> l.forEach(proxyDataSourceBuilder::listener)); + ifAvailable(this.methodExecutionListeners, m -> m.forEach(proxyDataSourceBuilder::methodListener)); + ifAvailable(this.parameterTransformer, proxyDataSourceBuilder::parameterTransformer); + ifAvailable(this.queryTransformer, proxyDataSourceBuilder::queryTransformer); + ifAvailable(this.resultSetProxyLogicFactory, proxyDataSourceBuilder::proxyResultSet); + ifAvailable(this.dataSourceProxyConnectionIdManagerProvider, + d -> proxyDataSourceBuilder.connectionIdManager(d.get())); + return proxyDataSourceBuilder; + } + + private void ifAvailable(@Nullable T o, Consumer consumer) { + if (o != null) { + consumer.accept(o); + } + } + + private SLF4JLogLevel toSlf4JLogLevel(String logLevel) { + if (logLevel == null) { + return null; + } + for (SLF4JLogLevel slf4JLogLevel : SLF4JLogLevel.values()) { + if (slf4JLogLevel.name().equalsIgnoreCase(logLevel)) { + return slf4JLogLevel; + } + } + throw new IllegalArgumentException("Unresolved log level " + logLevel + " for slf4j logger, known levels: " + + Arrays.toString(SLF4JLogLevel.values())); + } + + private Level toJULLogLevel(String logLevel) { + if (logLevel == null) { + return null; + } + try { + return Level.parse(logLevel); + } + catch (IllegalArgumentException e) { + if (logLevel.equalsIgnoreCase("DEBUG")) { + return Level.FINE; + } + if (logLevel.equalsIgnoreCase("WARN")) { + return Level.WARNING; + } + throw new IllegalArgumentException("Unresolved log level " + logLevel + " for java.util.logging", e); + } + } + + private CommonsLogLevel toCommonsLogLevel(String logLevel) { + if (logLevel == null) { + return null; + } + for (CommonsLogLevel commonsLogLevel : CommonsLogLevel.values()) { + if (commonsLogLevel.name().equalsIgnoreCase(logLevel)) { + return commonsLogLevel; + } + } + throw new IllegalArgumentException("Unresolved log level " + logLevel + + " for apache commons logger, known levels " + Arrays.toString(CommonsLogLevel.values())); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyConnectionIdManagerProvider.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyConnectionIdManagerProvider.java new file mode 100644 index 000000000..f0c5a6cd0 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyConnectionIdManagerProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.util.function.Supplier; + +import net.ttddyy.dsproxy.ConnectionIdManager; + +/** + * Supplier of a {@link ConnectionIdManager}. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +public interface DataSourceProxyConnectionIdManagerProvider extends Supplier { + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyDataSourceDecorator.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyDataSourceDecorator.java new file mode 100644 index 000000000..000c78808 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyDataSourceDecorator.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import javax.sql.DataSource; + +import net.ttddyy.dsproxy.support.ProxyDataSource; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; + +import org.springframework.core.Ordered; + +/** + * {@link Ordered} decorator for {@link ProxyDataSource}. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +public class DataSourceProxyDataSourceDecorator implements DataSourceDecorator, Ordered { + + private final DataSourceProxyBuilderCustomizer dataSourceProxyBuilderCustomizer; + + private final DataSourceNameResolver dataSourceNameResolver; + + public DataSourceProxyDataSourceDecorator(DataSourceProxyBuilderCustomizer dataSourceProxyBuilderCustomizer, + DataSourceNameResolver dataSourceNameResolver) { + this.dataSourceProxyBuilderCustomizer = dataSourceProxyBuilderCustomizer; + this.dataSourceNameResolver = dataSourceNameResolver; + } + + @Override + public DataSource decorate(String beanName, DataSource dataSource) { + ProxyDataSourceBuilder proxyDataSourceBuilder = ProxyDataSourceBuilder.create(); + proxyDataSourceBuilder = this.dataSourceProxyBuilderCustomizer.customize(proxyDataSourceBuilder); + String dataSourceName = this.dataSourceNameResolver.resolveDataSourceName(dataSource); + return proxyDataSourceBuilder.dataSource(dataSource).name(dataSourceName).build(); + } + + @Override + public int getOrder() { + return 20; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyProperties.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyProperties.java new file mode 100644 index 000000000..802fff5bf --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyProperties.java @@ -0,0 +1,263 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +import net.ttddyy.dsproxy.QueryCountHolder; +import net.ttddyy.dsproxy.listener.logging.CommonsLogLevel; +import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; + +/** + * Properties for datasource-proxy. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +public class DataSourceProxyProperties { + + /** + * Logging to use for logging queries. + */ + private DataSourceProxyLogging logging = DataSourceProxyLogging.SLF4J; + + /** + * Query configuration. + */ + private Query query = new Query(); + + /** + * Slow query configuration. + */ + private SlowQuery slowQuery = new SlowQuery(); + + /** + * Use multiline output for logging query. + * + * @see ProxyDataSourceBuilder#multiline() + */ + private boolean multiline = true; + + /** + * Use json output for logging query. + * + * @see ProxyDataSourceBuilder#asJson() + */ + private boolean jsonFormat = false; + + /** + * Creates listener to count queries. + * + * @see ProxyDataSourceBuilder#countQuery() + * @see QueryCountHolder + */ + private boolean countQuery = false; + + public DataSourceProxyLogging getLogging() { + return logging; + } + + public void setLogging(DataSourceProxyLogging logging) { + this.logging = logging; + } + + public Query getQuery() { + return query; + } + + public void setQuery(Query query) { + this.query = query; + } + + public SlowQuery getSlowQuery() { + return slowQuery; + } + + public void setSlowQuery(SlowQuery slowQuery) { + this.slowQuery = slowQuery; + } + + public boolean isMultiline() { + return multiline; + } + + public void setMultiline(boolean multiline) { + this.multiline = multiline; + } + + public boolean isJsonFormat() { + return jsonFormat; + } + + public void setJsonFormat(boolean jsonFormat) { + this.jsonFormat = jsonFormat; + } + + public boolean isCountQuery() { + return countQuery; + } + + public void setCountQuery(boolean countQuery) { + this.countQuery = countQuery; + } + + /** + * Properties to configure query logging listener. + * + * @see ProxyDataSourceBuilder#logQueryToSysOut() + * @see ProxyDataSourceBuilder#logQueryBySlf4j(SLF4JLogLevel, String) + * @see ProxyDataSourceBuilder#logQueryByCommons(CommonsLogLevel, String) + * @see ProxyDataSourceBuilder#logQueryByJUL(Level, String) + */ + public static class Query { + + /** + * Enable logging all queries to the log. + */ + private boolean enableLogging = true; + + /** + * Name of query logger. + */ + private String loggerName; + + /** + * Severity of query logger. + */ + private String logLevel = "DEBUG"; + + public boolean isEnableLogging() { + return enableLogging; + } + + public void setEnableLogging(boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public String getLoggerName() { + return loggerName; + } + + public void setLoggerName(String loggerName) { + this.loggerName = loggerName; + } + + public String getLogLevel() { + return logLevel; + } + + public void setLogLevel(String logLevel) { + this.logLevel = logLevel; + } + + } + + /** + * Properties to configure slow query logging listener. + * + * @see ProxyDataSourceBuilder#logSlowQueryToSysOut(long, TimeUnit) + * @see ProxyDataSourceBuilder#logSlowQueryBySlf4j(long, TimeUnit) + * @see ProxyDataSourceBuilder#logSlowQueryByCommons(long, TimeUnit) + * @see ProxyDataSourceBuilder#logSlowQueryByJUL(long, TimeUnit) + */ + public static class SlowQuery { + + /** + * Enable logging slow queries to the log. + */ + private boolean enableLogging = true; + + /** + * Name of slow query logger. + */ + private String loggerName; + + /** + * Severity of slow query logger. + */ + private String logLevel = "WARN"; + + /** + * Number of seconds to consider query as slow. + */ + private long threshold = 300; + + boolean isEnableLogging() { + return enableLogging; + } + + public void setEnableLogging(boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public String getLoggerName() { + return loggerName; + } + + public void setLoggerName(String loggerName) { + this.loggerName = loggerName; + } + + public String getLogLevel() { + return logLevel; + } + + public void setLogLevel(String logLevel) { + this.logLevel = logLevel; + } + + public long getThreshold() { + return threshold; + } + + public void setThreshold(long threshold) { + this.threshold = threshold; + } + + } + + /** + * Query logging listener is the most used listener that logs executing query with + * actual parameters to. You can pick one of the following proxy logging mechanisms. + */ + public enum DataSourceProxyLogging { + + /** + * Log using System.out. + */ + SYSOUT, + + /** + * Log using SLF4J. + */ + SLF4J, + + /** + * Log using Commons. + */ + COMMONS, + + /** + * Log using Java Util Logging. + */ + JUL + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceWrapper.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceWrapper.java new file mode 100644 index 000000000..0679b9aaa --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceWrapper.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import javax.sql.DataSource; + +import org.springframework.jdbc.datasource.DelegatingDataSource; + +/** + * Interface that implicitly added to the CGLIB proxy of {@link DataSource}. + * + * Returns link of both real {@link DataSource}, decorated {@link DataSource} and all + * decorating chain including decorator bean name, instance and result of decorating. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +public class DataSourceWrapper extends DelegatingDataSource { + + private final DataSource originalDataSource; + + public DataSourceWrapper(DataSource originalDataSource, DataSource decoratedDataSource) { + super(decoratedDataSource); + this.originalDataSource = originalDataSource; + } + + /** + * Returns original data source, before applying any decorator. + * @return original data source + */ + public DataSource getOriginalDataSource() { + return originalDataSource; + } + + /** + * Returns wrapped data source with the decorator applied. + * @return decorated data source + */ + public DataSource getDecoratedDataSource() { + return getTargetDataSource(); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/P6SpyContextJdbcEventListenerFactory.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/P6SpyContextJdbcEventListenerFactory.java new file mode 100644 index 000000000..98b6ba5a9 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/P6SpyContextJdbcEventListenerFactory.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.util.List; + +import com.p6spy.engine.event.CompoundJdbcEventListener; +import com.p6spy.engine.event.JdbcEventListener; +import com.p6spy.engine.spy.JdbcEventListenerFactory; + +import org.springframework.util.Assert; + +/** + * Factory to support all defined {@link JdbcEventListener} in the context. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +public class P6SpyContextJdbcEventListenerFactory implements JdbcEventListenerFactory { + + private final CompoundJdbcEventListener compoundJdbcEventListener; + + public P6SpyContextJdbcEventListenerFactory(JdbcEventListenerFactory delegate, List listeners) { + Assert.notNull(delegate, "JdbcEventListenerFactory should not be null"); + Assert.notEmpty(listeners, "Listeners should not be empty"); + + JdbcEventListener jdbcEventListener = delegate.createJdbcEventListener(); + if (jdbcEventListener instanceof CompoundJdbcEventListener) { + this.compoundJdbcEventListener = (CompoundJdbcEventListener) jdbcEventListener; + } + else { + this.compoundJdbcEventListener = new CompoundJdbcEventListener(); + this.compoundJdbcEventListener.addListener(jdbcEventListener); + } + listeners.forEach(this.compoundJdbcEventListener::addListener); + } + + @Override + public JdbcEventListener createJdbcEventListener() { + return this.compoundJdbcEventListener; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/P6SpyDataSourceDecorator.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/P6SpyDataSourceDecorator.java new file mode 100644 index 000000000..6766f13a8 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/P6SpyDataSourceDecorator.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import javax.sql.DataSource; + +import com.p6spy.engine.spy.JdbcEventListenerFactory; +import com.p6spy.engine.spy.P6DataSource; + +import org.springframework.core.Ordered; + +/** + * {@link Ordered} decorator for {@link P6DataSource}. + * + * @author Arthur Gavlyukovskiy + */ +public class P6SpyDataSourceDecorator implements DataSourceDecorator, Ordered { + + /** + * Bean order. + */ + public static final int ORDER = 30; + + private final JdbcEventListenerFactory jdbcEventListenerFactory; + + public P6SpyDataSourceDecorator(JdbcEventListenerFactory jdbcEventListenerFactory) { + this.jdbcEventListenerFactory = jdbcEventListenerFactory; + } + + @Override + public DataSource decorate(String beanName, DataSource dataSource) { + P6DataSource p6DataSource = new P6DataSource(dataSource); + p6DataSource.setJdbcEventListenerFactory(this.jdbcEventListenerFactory); + return p6DataSource; + } + + @Override + public int getOrder() { + return P6SpyDataSourceDecorator.ORDER; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/SleuthJdbcSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/SleuthJdbcSpan.java new file mode 100644 index 000000000..f30971726 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/SleuthJdbcSpan.java @@ -0,0 +1,167 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.EventValue; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthJdbcSpan implements DocumentedSpan { + + /** + * Span created when a JDBC query gets executed. + */ + JDBC_QUERY_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return QueryTags.values(); + } + + @Override + public EventValue[] getEvents() { + return QueryEvents.values(); + } + + @Override + public String prefix() { + return "jdbc."; + } + }, + + /** + * Span created when working with JDBC result set. + */ + JDBC_RESULT_SET_SPAN { + @Override + public String getName() { + return "result-set"; + } + + @Override + public TagKey[] getTagKeys() { + return QueryTags.values(); + } + + @Override + public EventValue[] getEvents() { + return QueryEvents.values(); + } + + @Override + public String prefix() { + return "jdbc."; + } + }, + + /** + * Span created when a JDBC connection takes place. + */ + JDBC_CONNECTION_SPAN { + @Override + public String getName() { + return "connection"; + } + + @Override + public TagKey[] getTagKeys() { + return ConnectionTags.values(); + } + + @Override + public String prefix() { + return "jdbc."; + } + }; + + enum ConnectionTags implements TagKey { + + /** + * Name of the JDBC datasource driver. + */ + DATASOURCE_DRIVER { + @Override + public String getKey() { + return "jdbc.datasource.driver"; + } + }, + + /** + * Name of the JDBC datasource pool. + */ + DATASOURCE_POOL { + @Override + public String getKey() { + return "jdbc.datasource.pool"; + } + }, + + } + + enum QueryTags implements TagKey { + + /** + * The SQL query value. + */ + QUERY { + @Override + public String getKey() { + return "jdbc.query"; + } + }, + + /** + * Number of SQL rows. + */ + ROW_COUNT { + @Override + public String getKey() { + return "jdbc.row-count"; + } + } + + } + + enum QueryEvents implements EventValue { + + /** + * When the transaction gets commited. + */ + COMMIT { + @Override + public String getValue() { + return "jdbc.commit"; + } + }, + + /** + * When the transaction gets rolled back. + */ + ROLLBACK { + @Override + public String getValue() { + return "jdbc.rollback"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceHikariListenerStrategySpanCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceHikariListenerStrategySpanCustomizer.java new file mode 100644 index 000000000..683674bd2 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceHikariListenerStrategySpanCustomizer.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import javax.sql.CommonDataSource; + +import com.zaxxer.hikari.HikariDataSource; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; +import org.springframework.util.StringUtils; + +/** + * Customizer for {@link TraceListenerStrategy} for a {@link HikariDataSource}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceHikariListenerStrategySpanCustomizer + implements TraceListenerStrategySpanCustomizer { + + @Override + public void customizeConnectionSpan(HikariDataSource hikariDataSource, Span.Builder spanBuilder) { + AssertingSpanBuilder assertingSpanBuilder = AssertingSpanBuilder.of(SleuthJdbcSpan.JDBC_CONNECTION_SPAN, + spanBuilder); + if (StringUtils.hasText(hikariDataSource.getDriverClassName())) { + assertingSpanBuilder.tag(SleuthJdbcSpan.ConnectionTags.DATASOURCE_DRIVER, + hikariDataSource.getDriverClassName()); + } + if (StringUtils.hasText(hikariDataSource.getPoolName())) { + assertingSpanBuilder.tag(SleuthJdbcSpan.ConnectionTags.DATASOURCE_POOL, hikariDataSource.getPoolName()); + } + } + + @Override + public boolean isApplicable(CommonDataSource dataSource) { + return dataSource instanceof HikariDataSource; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceJdbcEventListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceJdbcEventListener.java new file mode 100644 index 000000000..15a42e81d --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceJdbcEventListener.java @@ -0,0 +1,152 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.sql.SQLException; +import java.util.List; + +import javax.sql.CommonDataSource; + +import com.p6spy.engine.common.ConnectionInformation; +import com.p6spy.engine.common.PreparedStatementInformation; +import com.p6spy.engine.common.ResultSetInformation; +import com.p6spy.engine.common.StatementInformation; +import com.p6spy.engine.event.SimpleJdbcEventListener; + +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.core.Ordered; +import org.springframework.util.StringUtils; + +/** + * Listener to represent each connection and sql query as a span. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +public class TraceJdbcEventListener extends SimpleJdbcEventListener implements Ordered { + + /** + * Bean order. + */ + public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 10; + + private final DataSourceNameResolver dataSourceNameResolver; + + private final TraceListenerStrategy strategy; + + private final boolean includeParameterValues; + + public TraceJdbcEventListener(Tracer tracer, DataSourceNameResolver dataSourceNameResolver, + List traceTypes, boolean includeParameterValues, + List customizers) { + this.dataSourceNameResolver = dataSourceNameResolver; + this.includeParameterValues = includeParameterValues; + this.strategy = new TraceListenerStrategy<>(tracer, traceTypes, customizers); + } + + @Override + public void onBeforeGetConnection(ConnectionInformation connectionInformation) { + CommonDataSource dataSource = connectionInformation.getDataSource(); + String dataSourceName = this.dataSourceNameResolver.resolveDataSourceName(dataSource); + this.strategy.beforeGetConnection(connectionInformation, dataSource, dataSourceName); + } + + @Override + public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) { + this.strategy.afterGetConnection(connectionInformation, connectionInformation.getConnection(), e); + } + + @Override + public void onBeforeAnyExecute(StatementInformation statementInformation) { + String dataSourceName = this.dataSourceNameResolver + .resolveDataSourceName(statementInformation.getConnectionInformation().getDataSource()); + this.strategy.beforeQuery(statementInformation.getConnectionInformation(), + statementInformation.getConnectionInformation().getConnection(), statementInformation, dataSourceName); + } + + @Override + public void onAfterAnyExecute(StatementInformation statementInformation, long timeElapsedNanos, SQLException e) { + this.strategy.afterQuery(statementInformation.getConnectionInformation(), statementInformation, + getSql(statementInformation), e); + } + + @Override + public void onBeforeResultSetNext(ResultSetInformation resultSetInformation) { + String dataSourceName = this.dataSourceNameResolver + .resolveDataSourceName(resultSetInformation.getConnectionInformation().getDataSource()); + this.strategy.beforeResultSetNext(resultSetInformation.getConnectionInformation(), + resultSetInformation.getConnectionInformation().getConnection(), + resultSetInformation.getStatementInformation(), resultSetInformation, dataSourceName); + } + + @Override + public void onAfterExecuteUpdate(PreparedStatementInformation statementInformation, long timeElapsedNanos, + int rowCount, SQLException e) { + if (e == null) { + this.strategy.addQueryRowCount(statementInformation.getConnectionInformation(), statementInformation, + rowCount); + } + super.onAfterExecuteUpdate(statementInformation, timeElapsedNanos, rowCount, e); + } + + @Override + public void onAfterExecuteUpdate(StatementInformation statementInformation, long timeElapsedNanos, String sql, + int rowCount, SQLException e) { + if (e == null) { + this.strategy.addQueryRowCount(statementInformation.getConnectionInformation(), statementInformation, + rowCount); + } + super.onAfterExecuteUpdate(statementInformation, timeElapsedNanos, sql, rowCount, e); + } + + @Override + public void onAfterStatementClose(StatementInformation statementInformation, SQLException e) { + this.strategy.afterStatementClose(statementInformation.getConnectionInformation(), statementInformation); + } + + @Override + public void onAfterResultSetClose(ResultSetInformation resultSetInformation, SQLException e) { + this.strategy.afterResultSetClose(resultSetInformation.getConnectionInformation(), resultSetInformation, + resultSetInformation.getCurrRow() + 1, e); + } + + @Override + public void onAfterCommit(ConnectionInformation connectionInformation, long timeElapsedNanos, SQLException e) { + this.strategy.afterCommit(connectionInformation, e); + } + + @Override + public void onAfterRollback(ConnectionInformation connectionInformation, long timeElapsedNanos, SQLException e) { + this.strategy.afterRollback(connectionInformation, e); + } + + @Override + public void onAfterConnectionClose(ConnectionInformation connectionInformation, SQLException e) { + this.strategy.afterConnectionClose(connectionInformation, e); + } + + private String getSql(StatementInformation statementInformation) { + return this.includeParameterValues && StringUtils.hasText(statementInformation.getSqlWithValues()) + ? statementInformation.getSqlWithValues() : statementInformation.getSql(); + } + + @Override + public int getOrder() { + return ORDER; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategy.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategy.java new file mode 100644 index 000000000..735c0a6f7 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategy.java @@ -0,0 +1,402 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.net.URI; +import java.sql.Connection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.sql.CommonDataSource; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; +import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Partially taken from + * https://github.com/openzipkin/brave/blob/v5.6.4/instrumentation/p6spy/src/main/java/brave/p6spy/TracingJdbcEventListener.java. + * + * @param connection type + * @param statement + * @param result set + */ +class TraceListenerStrategy { + + private final Map openConnections = new ConcurrentHashMap<>(); + + // Captures all the characters between = and either the next & or the end of the + // string. + private static final Pattern URL_SERVICE_NAME_FINDER = Pattern.compile("sleuthServiceName=(.*?)(?:&|$)"); + + private final Tracer tracer; + + private final List traceTypes; + + private final List customizers; + + TraceListenerStrategy(Tracer tracer, List traceTypes, + List customizers) { + this.tracer = tracer; + this.traceTypes = traceTypes; + this.customizers = customizers; + } + + void beforeGetConnection(CON connectionKey, @Nullable CommonDataSource dataSource, String dataSourceName) { + SpanAndScope spanAndScope = null; + if (this.traceTypes.contains(TraceType.CONNECTION)) { + AssertingSpanBuilder connectionSpanBuilder = AssertingSpanBuilder + .of(SleuthJdbcSpan.JDBC_CONNECTION_SPAN, tracer.spanBuilder()) + .name(SleuthJdbcSpan.JDBC_CONNECTION_SPAN.getName()); + connectionSpanBuilder.remoteServiceName(dataSourceName); + connectionSpanBuilder.kind(Span.Kind.CLIENT); + this.customizers.stream().filter(customizer -> customizer.isApplicable(dataSource)) + .forEach(customizer -> customizer.customizeConnectionSpan(dataSource, connectionSpanBuilder)); + Span connectionSpan = connectionSpanBuilder.start(); + spanAndScope = new SpanAndScope(connectionSpan, tracer.withSpan(connectionSpan)); + } + ConnectionInfo connectionInfo = new ConnectionInfo(spanAndScope); + this.openConnections.put(connectionKey, connectionInfo); + } + + void afterGetConnection(CON connectionKey, Connection connection, Throwable t) { + if (t != null) { + ConnectionInfo connectionInfo = this.openConnections.remove(connectionKey); + connectionInfo.getSpan().ifPresent(connectionSpan -> { + parseServerIpAndPort(connection, connectionSpan.getSpan()); + connectionSpan.getSpan().error(t); + connectionSpan.close(); + }); + return; + } + this.openConnections.get(connectionKey).getSpan().ifPresent(spanAndScope -> { + parseServerIpAndPort(connection, spanAndScope.getSpan()); + }); + } + + void beforeQuery(CON connectionKey, Connection connection, STMT statementKey, String dataSourceName) { + SpanAndScope SpanAndScope = null; + if (traceTypes.contains(TraceType.QUERY)) { + Span.Builder statementSpanBuilder = AssertingSpanBuilder + .of(SleuthJdbcSpan.JDBC_QUERY_SPAN, tracer.spanBuilder()) + .name(SleuthJdbcSpan.JDBC_QUERY_SPAN.getName()); + statementSpanBuilder.remoteServiceName(dataSourceName); + parseServerIpAndPort(connection, statementSpanBuilder); + statementSpanBuilder.kind(Span.Kind.CLIENT); + Span statementSpan = statementSpanBuilder.start(); + SpanAndScope = new SpanAndScope(statementSpan, tracer.withSpan(statementSpan)); + } + StatementInfo statementInfo = new StatementInfo(SpanAndScope); + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + // Connection may be closed after statement preparation, but before statement + // execution. + return; + } + connectionInfo.getNestedStatements().put(statementKey, statementInfo); + } + + void addQueryRowCount(CON connectionKey, STMT statementKey, int rowCount) { + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + // Connection is already closed + return; + } + StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); + statementInfo.getSpan() + .ifPresent(statementSpan -> AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, statementSpan.getSpan()) + .tag(SleuthJdbcSpan.QueryTags.ROW_COUNT, String.valueOf(rowCount))); + } + + void afterQuery(CON connectionKey, STMT statementKey, String sql, Throwable t) { + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + // Connection may be closed after statement preparation, but before statement + // execution. + return; + } + StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); + statementInfo.getSpan().ifPresent(statementSpan -> { + AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, statementSpan.getSpan()) + .tag(SleuthJdbcSpan.QueryTags.QUERY, sql).name(spanName(sql)); + if (t != null) { + statementSpan.getSpan().error(t); + } + statementSpan.close(); + }); + } + + void beforeResultSetNext(CON connectionKey, Connection connection, STMT statementKey, RS resultSetKey, + String dataSourceName) { + if (!traceTypes.contains(TraceType.FETCH)) { + return; + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + // ConnectionInfo may be null if Connection was closed before ResultSet + if (connectionInfo == null) { + return; + } + if (connectionInfo.getNestedResultSetSpans().containsKey(resultSetKey)) { + // ResultSet span is already created + return; + } + AssertingSpanBuilder resultSetSpanBuilder = AssertingSpanBuilder + .of(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN, tracer.spanBuilder()) + .name(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN.getName()); + resultSetSpanBuilder.remoteServiceName(dataSourceName); + resultSetSpanBuilder.kind(Span.Kind.CLIENT); + parseServerIpAndPort(connection, resultSetSpanBuilder); + Span resultSetSpan = resultSetSpanBuilder.start(); + SpanAndScope SpanAndScope = new SpanAndScope(resultSetSpan, tracer.withSpan(resultSetSpan)); + connectionInfo.getNestedResultSetSpans().put(resultSetKey, SpanAndScope); + StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); + // StatementInfo may be null when Statement is proxied and instance returned from + // ResultSet is different from instance returned in query method + // in this case if Statement is closed before ResultSet span won't be finished + // immediately, but when Connection is closed + if (statementInfo != null) { + statementInfo.getNestedResultSetSpans().put(resultSetKey, SpanAndScope); + } + } + + void afterResultSetClose(CON connectionKey, RS resultSetKey, int rowCount, Throwable t) { + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + // ConnectionInfo may be null if Connection was closed before ResultSet + if (connectionInfo == null) { + return; + } + SpanAndScope resultSetSpan = connectionInfo.getNestedResultSetSpans().remove(resultSetKey); + // ResultSet span may be null if Statement or ResultSet were already closed + if (resultSetSpan == null) { + return; + } + if (rowCount != -1) { + AssertingSpan.of(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN, resultSetSpan.getSpan()) + .tag(SleuthJdbcSpan.QueryTags.ROW_COUNT, String.valueOf(rowCount)); + } + if (t != null) { + resultSetSpan.getSpan().error(t); + } + resultSetSpan.close(); + } + + void afterStatementClose(CON connectionKey, STMT statementKey) { + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + // ConnectionInfo may be null if Connection was closed before Statement + if (connectionInfo == null) { + return; + } + StatementInfo statementInfo = connectionInfo.getNestedStatements().remove(statementKey); + if (statementInfo != null) { + statementInfo.getNestedResultSetSpans().forEach((resultSetKey, span) -> { + connectionInfo.getNestedResultSetSpans().remove(resultSetKey); + span.close(); + }); + statementInfo.getNestedResultSetSpans().clear(); + } + } + + void afterCommit(CON connectionKey, Throwable t) { + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + // Connection is already closed + return; + } + connectionInfo.getSpan().ifPresent(connectionSpan -> { + if (t != null) { + connectionSpan.getSpan().error(t); + } + AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, connectionSpan.getSpan()) + .event(SleuthJdbcSpan.QueryEvents.COMMIT); + }); + } + + void afterRollback(CON connectionKey, Throwable t) { + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + // Connection is already closed + return; + } + connectionInfo.getSpan().ifPresent(connectionSpan -> { + if (t != null) { + connectionSpan.getSpan().error(t); + } + else { + connectionSpan.getSpan().error(new JdbcException("Transaction rolled back")); + } + AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, connectionSpan.getSpan()) + .event(SleuthJdbcSpan.QueryEvents.ROLLBACK); + }); + } + + void afterConnectionClose(CON connectionKey, Throwable t) { + ConnectionInfo connectionInfo = openConnections.remove(connectionKey); + if (connectionInfo == null) { + // connection is already closed + return; + } + connectionInfo.getNestedResultSetSpans().values().forEach(SpanAndScope::close); + connectionInfo.getNestedStatements().values() + .forEach(statementInfo -> statementInfo.getSpan().ifPresent(SpanAndScope::close)); + connectionInfo.getSpan().ifPresent(connectionSpan -> { + if (t != null) { + connectionSpan.getSpan().error(t); + } + connectionSpan.close(); + }); + } + + private String spanName(String sql) { + return sql.substring(0, sql.indexOf(' ')).toLowerCase(Locale.ROOT); + } + + private void parseServerIpAndPort(Connection connection, Span.Builder span) { + if (connection == null) { + return; + } + UrlAndRemoteServiceName urlAndRemoteServiceName = parseServerIpAndPort(connection); + span.remoteServiceName(urlAndRemoteServiceName.remoteServiceName); + URI url = urlAndRemoteServiceName.url; + if (url != null) { + span.remoteIpAndPort(url.getHost(), url.getPort()); + } + } + + private void parseServerIpAndPort(Connection connection, Span span) { + if (connection == null) { + return; + } + UrlAndRemoteServiceName urlAndRemoteServiceName = parseServerIpAndPort(connection); + span.remoteServiceName(urlAndRemoteServiceName.remoteServiceName); + URI url = urlAndRemoteServiceName.url; + if (url != null) { + span.remoteIpAndPort(url.getHost(), url.getPort()); + } + } + + /** + * This attempts to get the ip and port from the JDBC URL. Ex. localhost and 5555 from + * {@code + * jdbc:mysql://localhost:5555/mydatabase}. + * + * Taken from Brave. + */ + private UrlAndRemoteServiceName parseServerIpAndPort(Connection connection) { + String remoteServiceName = ""; + try { + String urlAsString = connection.getMetaData().getURL().substring(5); // strip + // "jdbc:" + URI url = URI.create(urlAsString.replace(" ", "")); // Remove all white space + // according to RFC 2396 + Matcher matcher = URL_SERVICE_NAME_FINDER.matcher(url.toString()); + if (matcher.find() && matcher.groupCount() == 1) { + String parsedServiceName = matcher.group(1); + if (parsedServiceName != null && !parsedServiceName.isEmpty()) { + remoteServiceName = parsedServiceName; + } + } + if (!StringUtils.hasText(remoteServiceName)) { + String databaseName = connection.getCatalog(); + if (databaseName != null && !databaseName.isEmpty()) { + remoteServiceName = databaseName; + } + } + return new UrlAndRemoteServiceName(url, remoteServiceName); + } + catch (Exception e) { + // remote address is optional + return new UrlAndRemoteServiceName(null, remoteServiceName); + } + } + + private final class ConnectionInfo { + + private final SpanAndScope span; + + private final Map nestedStatements = new ConcurrentHashMap<>(); + + private final Map nestedResultSetSpans = new ConcurrentHashMap<>(); + + private ConnectionInfo(@Nullable SpanAndScope span) { + this.span = span; + } + + Optional getSpan() { + return Optional.ofNullable(span); + } + + Map getNestedStatements() { + return nestedStatements; + } + + Map getNestedResultSetSpans() { + return nestedResultSetSpans; + } + + } + + private final class StatementInfo { + + private final SpanAndScope span; + + private final Map nestedResultSetSpans = new ConcurrentHashMap<>(); + + private StatementInfo(SpanAndScope span) { + this.span = span; + } + + Optional getSpan() { + return Optional.ofNullable(span); + } + + Map getNestedResultSetSpans() { + return nestedResultSetSpans; + } + + } + + private final class UrlAndRemoteServiceName { + + final URI url; + + final String remoteServiceName; + + private UrlAndRemoteServiceName(@Nullable URI url, String remoteServiceName) { + this.url = url; + this.remoteServiceName = remoteServiceName; + } + + } + + private static final class JdbcException extends RuntimeException { + + JdbcException(String message) { + super(message); + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java new file mode 100644 index 000000000..52fe0ae6b --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import javax.sql.CommonDataSource; + +import org.springframework.cloud.sleuth.Span; + +/** + * Customizer for {@link TraceListenerStrategy} client span. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface TraceListenerStrategySpanCustomizer { + + /** + * Customizes the client database span. + * @param spanBuilder span builder + */ + void customizeConnectionSpan(T dataSource, Span.Builder spanBuilder); + + /** + * @param dataSource data source for which we're building the span + * @return {@code true} when this customizer can be applied + */ + boolean isApplicable(CommonDataSource dataSource); + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceQueryExecutionListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceQueryExecutionListener.java new file mode 100644 index 000000000..df4bfa8f3 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceQueryExecutionListener.java @@ -0,0 +1,170 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.stream.Collectors; + +import javax.sql.CommonDataSource; +import javax.sql.DataSource; + +import net.ttddyy.dsproxy.ExecutionInfo; +import net.ttddyy.dsproxy.QueryInfo; +import net.ttddyy.dsproxy.listener.MethodExecutionContext; +import net.ttddyy.dsproxy.listener.MethodExecutionListener; +import net.ttddyy.dsproxy.listener.QueryExecutionListener; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.core.Ordered; + +/** + * Listener to represent each connection and sql query as a span. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +public class TraceQueryExecutionListener implements QueryExecutionListener, MethodExecutionListener, Ordered { + + private static final Log log = LogFactory.getLog(TraceQueryExecutionListener.class); + + private final TraceListenerStrategy strategy; + + public TraceQueryExecutionListener(Tracer tracer, List traceTypes, + List customizers) { + this.strategy = new TraceListenerStrategy<>(tracer, traceTypes, customizers); + } + + @Override + public void beforeQuery(ExecutionInfo execInfo, List queryInfoList) { + this.strategy.beforeQuery(execInfo.getConnectionId(), connection(execInfo), execInfo.getStatement(), + execInfo.getDataSourceName()); + } + + private Connection connection(ExecutionInfo execInfo) { + try { + return execInfo.getStatement().getConnection(); + } + catch (Exception ex) { + if (log.isDebugEnabled()) { + log.debug("Can't retrieve the connection - will return null", ex); + } + return null; + } + } + + @Override + public void afterQuery(ExecutionInfo execInfo, List queryInfoList) { + if (execInfo.getMethod().getName().equals("executeUpdate") && execInfo.getThrowable() == null) { + this.strategy.addQueryRowCount(execInfo.getConnectionId(), execInfo.getStatement(), + (int) execInfo.getResult()); + } + String sql = queryInfoList.stream().map(QueryInfo::getQuery).collect(Collectors.joining("\n")); + this.strategy.afterQuery(execInfo.getConnectionId(), execInfo.getStatement(), sql, execInfo.getThrowable()); + } + + @Override + public void beforeMethod(MethodExecutionContext executionContext) { + Object target = executionContext.getTarget(); + String methodName = executionContext.getMethod().getName(); + String dataSourceName = executionContext.getProxyConfig().getDataSourceName(); + String connectionId = executionContext.getConnectionInfo().getConnectionId(); + if (target instanceof DataSource && methodName.equals("getConnection")) { + this.strategy.beforeGetConnection(connectionId, toCommonDataSource(target), dataSourceName); + } + else if (target instanceof ResultSet) { + ResultSet resultSet = (ResultSet) target; + if (methodName.equals("next")) { + try { + this.strategy.beforeResultSetNext(connectionId, resultSet.getStatement().getConnection(), + resultSet.getStatement(), resultSet, dataSourceName); + } + catch (SQLException ignore) { + } + } + } + } + + private CommonDataSource toCommonDataSource(Object target) { + try { + return ((DataSource) target).unwrap(CommonDataSource.class); + } + catch (Exception ex) { + if (log.isDebugEnabled()) { + log.debug("Failed to cast to common data source. Will return null", ex); + } + return null; + } + } + + private Connection getConnection(DataSource targetDataSource) { + try { + DataSource source = targetDataSource instanceof ProxyDataSource + ? (targetDataSource).unwrap(DataSource.class) : targetDataSource; + return source.getConnection(); + } + catch (Exception ex) { + if (log.isDebugEnabled()) { + log.debug("Failed to retrieve connection. Will return null", ex); + } + return null; + } + } + + @Override + public void afterMethod(MethodExecutionContext executionContext) { + Object target = executionContext.getTarget(); + String methodName = executionContext.getMethod().getName(); + String connectionId = executionContext.getConnectionInfo().getConnectionId(); + Throwable t = executionContext.getThrown(); + if (target instanceof DataSource && methodName.equals("getConnection")) { + this.strategy.afterGetConnection(connectionId, getConnection((DataSource) target), t); + } + else if (target instanceof Connection) { + switch (methodName) { + case "commit": + this.strategy.afterCommit(connectionId, t); + break; + case "rollback": + this.strategy.afterRollback(connectionId, t); + break; + case "close": + this.strategy.afterConnectionClose(connectionId, t); + break; + } + } + else if (target instanceof Statement && methodName.equals("close")) { + this.strategy.afterStatementClose(connectionId, (Statement) target); + } + else if (target instanceof ResultSet && methodName.equals("close")) { + ResultSet resultSet = (ResultSet) target; + this.strategy.afterResultSetClose(connectionId, resultSet, -1, t); + } + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE + 10; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceType.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceType.java new file mode 100644 index 000000000..2b74bec40 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceType.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +/** + * Trace types. + */ +public enum TraceType { + + /** + * Connection. + */ + CONNECTION, + + /** + * Query. + */ + QUERY, + + /** + * Fetch. + */ + FETCH + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListener.java index 35a4f3a1b..8de1d8ba2 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListener.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListener.java @@ -16,6 +16,8 @@ package org.springframework.cloud.sleuth.instrument.r2dbc; +import java.net.URI; + import io.r2dbc.proxy.core.QueryExecutionInfo; import io.r2dbc.proxy.core.QueryInfo; import io.r2dbc.proxy.listener.ProxyExecutionListener; @@ -84,7 +86,15 @@ public class TraceProxyExecutionListener implements ProxyExecutionListener { .tag(SleuthR2dbcSpan.Tags.THREAD, executionInfo.getThreadName()); // @formatter:on if (StringUtils.hasText(url)) { - builder.remoteUrl(url); + try { + URI uri = URI.create(url); + builder.remoteIpAndPort(uri.getHost(), uri.getPort()); + } + catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug("Failed to parse the url [" + url + "]. Won't set this value as atag"); + } + } } return builder.start(); } diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/jdbc/ContextJdbcEventListenerFactoryTest.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/jdbc/ContextJdbcEventListenerFactoryTest.java new file mode 100644 index 000000000..3968553be --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/jdbc/ContextJdbcEventListenerFactoryTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.util.Collections; + +import com.p6spy.engine.event.CompoundJdbcEventListener; +import com.p6spy.engine.event.SimpleJdbcEventListener; +import com.p6spy.engine.spy.JdbcEventListenerFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThat; + +class ContextJdbcEventListenerFactoryTest { + + private JdbcEventListenerFactory delegate; + + @BeforeEach + void setUp() { + delegate = Mockito.mock(JdbcEventListenerFactory.class); + } + + @Test + void shouldUseDelegateToCreateListener() { + SimpleJdbcEventListener listener1 = new SimpleJdbcEventListener() { + }; + SimpleJdbcEventListener listener2 = new SimpleJdbcEventListener() { + }; + Mockito.when(delegate.createJdbcEventListener()).thenReturn(listener1); + P6SpyContextJdbcEventListenerFactory contextJdbcEventListenerFactory = new P6SpyContextJdbcEventListenerFactory( + delegate, Collections.singletonList(listener2)); + + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) contextJdbcEventListenerFactory + .createJdbcEventListener(); + assertThat(jdbcEventListener.getEventListeners()).hasSize(2); + assertThat(jdbcEventListener.getEventListeners()).contains(listener1, listener2); + } + + @Test + void shouldReuseCompoundListenerFromFactory() { + SimpleJdbcEventListener listener1 = new SimpleJdbcEventListener() { + }; + Mockito.when(delegate.createJdbcEventListener()).thenReturn(new CompoundJdbcEventListener()); + P6SpyContextJdbcEventListenerFactory contextJdbcEventListenerFactory = new P6SpyContextJdbcEventListenerFactory( + delegate, Collections.singletonList(listener1)); + + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) contextJdbcEventListenerFactory + .createJdbcEventListener(); + assertThat(jdbcEventListener.getEventListeners()).hasSize(1); + assertThat(jdbcEventListener.getEventListeners()).contains(listener1); + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java index 7cadaef46..b8a862262 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java @@ -140,6 +140,7 @@ public class TraceAsyncIntegrationTests { private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(final Span span) { Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan()).isNotNull(); then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) .isEqualTo(span.context().traceId()); then(this.spans).hasSize(2); From 12737adb67e55a02fc3e7ea28dbb57fbd78552f3 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 26 May 2021 18:05:43 +0200 Subject: [PATCH 37/59] JDBC tracing context not leaking without this change there were cases (that we didn't test) where the tracing context would leak and pollute other parts of the code (including tests) with this change we ensure that in case of errors we don't allow any dangling tracing context --- .../cloud/sleuth/SpanAndScope.java | 8 + .../sleuth/docs/AssertingSpanBuilder.java | 5 + .../jdbc/P6SpyConfigurationTests.java | 558 +++++------ .../ProxyDataSourceConfigurationTests.java | 484 +++++----- ...thP6SpyListenerAutoConfigurationTests.java | 2 + ...aSourceListenerAutoConfigurationTests.java | 2 + .../jdbc/TracingJdbcEventListenerTests.java | 12 +- .../jdbc/TracingListenerStrategyTests.java | 31 +- .../jdbc/TraceListenerStrategy.java | 908 ++++++++++-------- .../jdbc/TraceQueryExecutionListener.java | 3 + .../internal/SleuthContextListener.java | 4 +- 11 files changed, 1088 insertions(+), 929 deletions(-) diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java index b61826426..d981cba7f 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.java @@ -18,6 +18,9 @@ package org.springframework.cloud.sleuth; import java.io.Closeable; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + /** * Container object for {@link Span} and its corresponding {@link Tracer.SpanInScope}. * @@ -26,6 +29,8 @@ import java.io.Closeable; */ public class SpanAndScope implements Closeable { + private static final Log log = LogFactory.getLog(SpanAndScope.class); + private final Span span; private final Tracer.SpanInScope scope; @@ -50,6 +55,9 @@ public class SpanAndScope implements Closeable { @Override public void close() { + if (log.isTraceEnabled()) { + log.trace("Closing span [" + this.span + "]"); + } this.scope.close(); this.span.end(); } diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java index 3ba8e793c..0363928d2 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java @@ -130,6 +130,11 @@ public interface AssertingSpanBuilder extends Span.Builder { public boolean isStarted() { return true; } + + @Override + public String toString() { + return getDelegate().toString(); + } }; } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java index d790fa2f0..c626017e4 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java @@ -1,277 +1,281 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import com.p6spy.engine.common.ConnectionInformation; -import com.p6spy.engine.common.P6LogQuery; -import com.p6spy.engine.event.CompoundJdbcEventListener; -import com.p6spy.engine.event.JdbcEventListener; -import com.p6spy.engine.logging.Category; -import com.p6spy.engine.logging.LoggingEventListener; -import com.p6spy.engine.spy.JdbcEventListenerFactory; -import com.p6spy.engine.spy.P6DataSource; -import com.p6spy.engine.spy.appender.CustomLineFormat; -import com.p6spy.engine.spy.appender.FormattedLogger; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -class P6SpyConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, - TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) - .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); - - @BeforeEach - @AfterEach - void resetLogAccumulator() { - LogAccumulator.reset(); - } - - @Test - void testCustomListeners() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomListenerConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - GetCountingListener getCountingListener = context.getBean(GetCountingListener.class); - ClosingCountingListener closingCountingListener = context.getBean(ClosingCountingListener.class); - P6DataSource p6DataSource = (P6DataSource) ((DataSourceWrapper) dataSource).getDecoratedDataSource(); - assertThat(p6DataSource).extracting("jdbcEventListenerFactory").isEqualTo(jdbcEventListenerFactory); - - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).contains(getCountingListener, closingCountingListener); - assertThat(getCountingListener.connectionCount).isEqualTo(0); - - Connection connection1 = p6DataSource.getConnection(); - - assertThat(getCountingListener.connectionCount).isEqualTo(1); - assertThat(closingCountingListener.connectionCount).isEqualTo(0); - - Connection connection2 = p6DataSource.getConnection(); - - assertThat(getCountingListener.connectionCount).isEqualTo(2); - - connection1.close(); - - assertThat(closingCountingListener.connectionCount).isEqualTo(1); - - connection2.close(); - - assertThat(closingCountingListener.connectionCount).isEqualTo(2); - }); - } - - @Test - void testDoesNotRegisterLoggingListenerIfDisabled() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.enable-logging:false"); - - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).extracting("class") - .doesNotContain(LoggingEventListener.class); - }); - } - - @Test - void testCanSetCustomLoggingFormat() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.log-format:test %{connectionId}"); - - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); - assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") - .isEqualTo(CustomLineFormat.class); - }); - } - - @Test - void testMultilineShouldNotOverrideCustomProperties() { - System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); - System.setProperty("p6spy.config.excludecategories", "debug"); - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.multiline:true"); - - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); - assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") - .isEqualTo(CustomLineFormat.class); - }); - } - - @Test - void testUseCustomLogger() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom", - "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" - + LogAccumulator.class.getName()); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - dataSource.getConnection().close(); - - assertThat(P6LogQuery.getLogger()).isInstanceOf(LogAccumulator.class); - }); - } - - @Test - void testLogFilterPattern() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom", - "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), - "spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern:.*table1.*"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - try (Connection connection = dataSource.getConnection(); - PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); - PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { - ps1.execute(); - ps2.execute(); - } - - assertThat(LogAccumulator.MESSAGES).hasSize(1); - assertThat(LogAccumulator.MESSAGES).allMatch(message -> message.contains("table1")); - }); - } - - @Test - void testLogFilterPatternMatchAll() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom", - "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), - "spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern:.*"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - try (Connection connection = dataSource.getConnection(); - PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); - PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { - ps1.execute(); - ps2.execute(); - } - - assertThat(LogAccumulator.MESSAGES).hasSize(2); - }); - } - - @Configuration - static class CustomListenerConfiguration { - - @Bean - public GetCountingListener wrappingCountingListener() { - return new GetCountingListener(); - } - - @Bean - public ClosingCountingListener closingCountingListener() { - return new ClosingCountingListener(); - } - - } - - static class GetCountingListener extends JdbcEventListener { - - int connectionCount = 0; - - @Override - public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) { - connectionCount++; - } - - } - - static class ClosingCountingListener extends JdbcEventListener { - - int connectionCount = 0; - - @Override - public void onAfterConnectionClose(ConnectionInformation connectionInformation, SQLException e) { - connectionCount++; - } - - } - - public static class LogAccumulator extends FormattedLogger { - - static final List MESSAGES = new ArrayList<>(); - static final List EXCEPTIONS = new ArrayList<>(); - - public static void reset() { - MESSAGES.clear(); - EXCEPTIONS.clear(); - } - - @Override - public void logException(Exception e) { - EXCEPTIONS.add(e); - } - - @Override - public void logText(String text) { - MESSAGES.add(text); - } - - @Override - public boolean isCategoryEnabled(Category category) { - return true; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import com.p6spy.engine.common.ConnectionInformation; +import com.p6spy.engine.common.P6LogQuery; +import com.p6spy.engine.event.CompoundJdbcEventListener; +import com.p6spy.engine.event.JdbcEventListener; +import com.p6spy.engine.logging.Category; +import com.p6spy.engine.logging.LoggingEventListener; +import com.p6spy.engine.spy.JdbcEventListenerFactory; +import com.p6spy.engine.spy.P6DataSource; +import com.p6spy.engine.spy.appender.CustomLineFormat; +import com.p6spy.engine.spy.appender.FormattedLogger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +class P6SpyConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, + TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); + + @BeforeEach + @AfterEach + void resetLogAccumulator() { + LogAccumulator.reset(); + } + + @Test + void testCustomListeners() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomListenerConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + GetCountingListener getCountingListener = context.getBean(GetCountingListener.class); + ClosingCountingListener closingCountingListener = context.getBean(ClosingCountingListener.class); + P6DataSource p6DataSource = (P6DataSource) ((DataSourceWrapper) dataSource).getDecoratedDataSource(); + assertThat(p6DataSource).extracting("jdbcEventListenerFactory").isEqualTo(jdbcEventListenerFactory); + + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).contains(getCountingListener, closingCountingListener); + assertThat(getCountingListener.connectionCount).isEqualTo(0); + + Connection connection1 = p6DataSource.getConnection(); + + assertThat(getCountingListener.connectionCount).isEqualTo(1); + assertThat(closingCountingListener.connectionCount).isEqualTo(0); + + Connection connection2 = p6DataSource.getConnection(); + + assertThat(getCountingListener.connectionCount).isEqualTo(2); + + // order matters! + connection2.close(); + + assertThat(closingCountingListener.connectionCount).isEqualTo(1); + + // order matters! + connection1.close(); + + assertThat(closingCountingListener.connectionCount).isEqualTo(2); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testDoesNotRegisterLoggingListenerIfDisabled() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.enable-logging:false"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class") + .doesNotContain(LoggingEventListener.class); + }); + } + + @Test + void testCanSetCustomLoggingFormat() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.log-format:test %{connectionId}"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); + assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") + .isEqualTo(CustomLineFormat.class); + }); + } + + @Test + void testMultilineShouldNotOverrideCustomProperties() { + System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); + System.setProperty("p6spy.config.excludecategories", "debug"); + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.multiline:true"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); + assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") + .isEqualTo(CustomLineFormat.class); + }); + } + + @Test + void testUseCustomLogger() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom", + "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" + + LogAccumulator.class.getName()); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + dataSource.getConnection().close(); + + assertThat(P6LogQuery.getLogger()).isInstanceOf(LogAccumulator.class); + }); + } + + @Test + void testLogFilterPattern() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom", + "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), + "spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern:.*table1.*"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); + PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { + ps1.execute(); + ps2.execute(); + } + + assertThat(LogAccumulator.MESSAGES).hasSize(1); + assertThat(LogAccumulator.MESSAGES).allMatch(message -> message.contains("table1")); + }); + } + + @Test + void testLogFilterPatternMatchAll() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom", + "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), + "spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern:.*"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); + PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { + ps1.execute(); + ps2.execute(); + } + + assertThat(LogAccumulator.MESSAGES).hasSize(2); + }); + } + + @Configuration + static class CustomListenerConfiguration { + + @Bean + public GetCountingListener wrappingCountingListener() { + return new GetCountingListener(); + } + + @Bean + public ClosingCountingListener closingCountingListener() { + return new ClosingCountingListener(); + } + + } + + static class GetCountingListener extends JdbcEventListener { + + int connectionCount = 0; + + @Override + public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) { + connectionCount++; + } + + } + + static class ClosingCountingListener extends JdbcEventListener { + + int connectionCount = 0; + + @Override + public void onAfterConnectionClose(ConnectionInformation connectionInformation, SQLException e) { + connectionCount++; + } + + } + + public static class LogAccumulator extends FormattedLogger { + + static final List MESSAGES = new ArrayList<>(); + static final List EXCEPTIONS = new ArrayList<>(); + + public static void reset() { + MESSAGES.clear(); + EXCEPTIONS.clear(); + } + + @Override + public void logException(Exception e) { + EXCEPTIONS.add(e); + } + + @Override + public void logText(String text) { + MESSAGES.add(text); + } + + @Override + public boolean isCategoryEnabled(Category category) { + return true; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java index d9103ae27..898a0de7a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java @@ -1,238 +1,246 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import net.ttddyy.dsproxy.ExecutionInfo; -import net.ttddyy.dsproxy.QueryInfo; -import net.ttddyy.dsproxy.listener.ChainListener; -import net.ttddyy.dsproxy.listener.QueryExecutionListener; -import net.ttddyy.dsproxy.listener.logging.CommonsQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.CommonsSlowQueryListener; -import net.ttddyy.dsproxy.listener.logging.JULQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.JULSlowQueryListener; -import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.SLF4JSlowQueryListener; -import net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.SystemOutSlowQueryListener; -import net.ttddyy.dsproxy.proxy.DefaultConnectionIdManager; -import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; -import net.ttddyy.dsproxy.support.ProxyDataSource; -import net.ttddyy.dsproxy.transform.ParameterTransformer; -import net.ttddyy.dsproxy.transform.QueryTransformer; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; - -import static org.assertj.core.api.Assertions.assertThat; - -class ProxyDataSourceConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, - TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) - .withClassLoader(new FilteredClassLoader("com.p6spy")); - - @Test - void testRegisterLogAndSlowQueryLogByDefaultToSlf4j() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogByUsingSlf4j() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasource-proxy.logging:slf4j"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogUsingSystemOut() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasource-proxy.logging:sysout"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutQueryLoggingListener.class); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogUsingJUL() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasourceProxy.logging:jul"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(JULSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(JULQueryLoggingListener.class); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogUsingApacheCommons() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasourceProxy.logging:commons"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(CommonsSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(CommonsQueryLoggingListener.class); - }); - } - - @Test - void testCustomParameterAndQueryTransformer() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomDataSourceProxyConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ParameterTransformer parameterTransformer = context.getBean(ParameterTransformer.class); - QueryTransformer queryTransformer = context.getBean(QueryTransformer.class); - assertThat(proxyDataSource.getProxyConfig().getParameterTransformer()).isSameAs(parameterTransformer); - assertThat(proxyDataSource.getProxyConfig().getQueryTransformer()).isSameAs(queryTransformer); - }); - } - - @Test - void testCustomListeners() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomListenerConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - QueryExecutionListener queryExecutionListener = context.getBean(QueryExecutionListener.class); - - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).contains(queryExecutionListener); - }); - } - - @Test - void testGlobalConnectionIdManagerByDefault() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - - assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(GlobalConnectionIdManager.class); - }); - } - - @Test - void testCustomConnectionIdManager() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomDataSourceProxyConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - - assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(DefaultConnectionIdManager.class); - }); - } - - @Configuration - static class CustomDataSourceProxyConfiguration { - - @Bean - public ParameterTransformer parameterTransformer() { - return (replacer, transformInfo) -> { - }; - } - - @Bean - public QueryTransformer queryTransformer() { - return (transformInfo) -> "TestQuery"; - } - - @Bean - public DataSourceProxyConnectionIdManagerProvider connectionIdManagerProvider() { - return DefaultConnectionIdManager::new; - } - - } - - @Configuration - static class CustomListenerConfiguration { - - @Bean - @Primary - public QueryExecutionListener queryExecutionListener() { - return new QueryExecutionListener() { - @Override - public void beforeQuery(ExecutionInfo execInfo, List queryInfoList) { - System.out.println("beforeQuery"); - } - - @Override - public void afterQuery(ExecutionInfo execInfo, List queryInfoList) { - System.out.println("afterQuery"); - } - }; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import net.ttddyy.dsproxy.ExecutionInfo; +import net.ttddyy.dsproxy.QueryInfo; +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.listener.QueryExecutionListener; +import net.ttddyy.dsproxy.listener.logging.CommonsQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.CommonsSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.JULQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.JULSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.SystemOutSlowQueryListener; +import net.ttddyy.dsproxy.proxy.DefaultConnectionIdManager; +import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import net.ttddyy.dsproxy.transform.ParameterTransformer; +import net.ttddyy.dsproxy.transform.QueryTransformer; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import static org.assertj.core.api.Assertions.assertThat; + +class ProxyDataSourceConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, + TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("com.p6spy")); + + @Test + void testRegisterLogAndSlowQueryLogByDefaultToSlf4j() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogByUsingSlf4j() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasource-proxy.logging:slf4j"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingSystemOut() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasource-proxy.logging:sysout"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutQueryLoggingListener.class); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingJUL() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasourceProxy.logging:jul"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(JULSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(JULQueryLoggingListener.class); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingApacheCommons() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasourceProxy.logging:commons"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(CommonsSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(CommonsQueryLoggingListener.class); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testCustomParameterAndQueryTransformer() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomDataSourceProxyConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ParameterTransformer parameterTransformer = context.getBean(ParameterTransformer.class); + QueryTransformer queryTransformer = context.getBean(QueryTransformer.class); + assertThat(proxyDataSource.getProxyConfig().getParameterTransformer()).isSameAs(parameterTransformer); + assertThat(proxyDataSource.getProxyConfig().getQueryTransformer()).isSameAs(queryTransformer); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testCustomListeners() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomListenerConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + QueryExecutionListener queryExecutionListener = context.getBean(QueryExecutionListener.class); + + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).contains(queryExecutionListener); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testGlobalConnectionIdManagerByDefault() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + + assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(GlobalConnectionIdManager.class); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testCustomConnectionIdManager() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomDataSourceProxyConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + + assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(DefaultConnectionIdManager.class); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Configuration + static class CustomDataSourceProxyConfiguration { + + @Bean + public ParameterTransformer parameterTransformer() { + return (replacer, transformInfo) -> { + }; + } + + @Bean + public QueryTransformer queryTransformer() { + return (transformInfo) -> "TestQuery"; + } + + @Bean + public DataSourceProxyConnectionIdManagerProvider connectionIdManagerProvider() { + return DefaultConnectionIdManager::new; + } + + } + + @Configuration + static class CustomListenerConfiguration { + + @Bean + @Primary + public QueryExecutionListener queryExecutionListener() { + return new QueryExecutionListener() { + @Override + public void beforeQuery(ExecutionInfo execInfo, List queryInfoList) { + System.out.println("beforeQuery"); + } + + @Override + public void afterQuery(ExecutionInfo execInfo, List queryInfoList) { + System.out.println("afterQuery"); + } + }; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java index 8255031a5..5a9af8e3f 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java @@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoCon import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; @@ -50,6 +51,7 @@ class SleuthP6SpyListenerAutoConfigurationTests { .createJdbcEventListener(); assertThat(jdbcEventListener.getEventListeners()).extracting("class") .contains(TraceJdbcEventListener.class); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java index 9b190eb57..f359bd951 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java @@ -29,6 +29,7 @@ import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoCon import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; @@ -53,6 +54,7 @@ class SleuthProxyDataSourceListenerAutoConfigurationTests { .getDecoratedDataSource(); ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); assertThat(chainListener.getListeners()).extracting("class").contains(TraceQueryExecutionListener.class); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java index 6a7c231eb..446a620e7 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java @@ -36,22 +36,16 @@ import static org.assertj.core.api.Assertions.assertThat; class TracingJdbcEventListenerTests extends TracingListenerStrategyTests { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + private static final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") + "spring.datasource.url=jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); protected TracingJdbcEventListenerTests() { - super(new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, - TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") - .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy"))); + super(contextRunner); } @Test diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java index a33d0ec90..604eee779 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java @@ -35,6 +35,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.Tracer; import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; import org.springframework.context.annotation.Bean; @@ -71,6 +72,7 @@ abstract class TracingListenerStrategyTests { assertThat(connectionSpan.remoteServiceName()).isEqualTo("TESTDB-BAZ"); assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.commit"); assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.rollback"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -92,6 +94,7 @@ abstract class TracingListenerStrategyTests { assertThat(connectionSpan.remoteServiceName()).isEqualTo("aaaabbbb"); assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.commit"); assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.rollback"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -112,6 +115,7 @@ abstract class TracingListenerStrategyTests { assertThat(statementSpan.name()).isEqualTo("select"); assertThat(statementSpan.remoteServiceName()).isEqualTo("TESTDB-BAZ"); assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -134,6 +138,7 @@ abstract class TracingListenerStrategyTests { assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -156,6 +161,7 @@ abstract class TracingListenerStrategyTests { assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -185,6 +191,7 @@ abstract class TracingListenerStrategyTests { if (isP6Spy(context)) { assertThat(resultSetSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "2"); } + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -212,6 +219,7 @@ abstract class TracingListenerStrategyTests { if (isP6Spy(context)) { assertThat(resultSetSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "1"); } + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -236,6 +244,7 @@ abstract class TracingListenerStrategyTests { assertThat(statementSpan.name()).isEqualTo("select"); assertThat(resultSetSpan.name()).isEqualTo("result-set"); assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -259,6 +268,7 @@ abstract class TracingListenerStrategyTests { assertThat(statementSpan.name()).isEqualTo("select"); assertThat(resultSetSpan.name()).isEqualTo("result-set"); assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -284,6 +294,7 @@ abstract class TracingListenerStrategyTests { assertThat(statementSpan.name()).isEqualTo("select"); assertThat(resultSetSpan.name()).isEqualTo("result-set"); assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -312,6 +323,7 @@ abstract class TracingListenerStrategyTests { assertThat(statementSpan.name()).isEqualTo("select"); assertThat(resultSetSpan.name()).isEqualTo("result-set"); assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -330,6 +342,7 @@ abstract class TracingListenerStrategyTests { assertThat(spanReporter.spans()).hasSize(1); MutableSpan connectionSpan = spanReporter.spans().get(0); assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -345,6 +358,7 @@ abstract class TracingListenerStrategyTests { statement.executeQuery("SELECT NOW()"); }).isInstanceOf(SQLException.class); connection.close(); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -362,6 +376,7 @@ abstract class TracingListenerStrategyTests { }).isInstanceOf(SQLException.class); statement.close(); connection.close(); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -373,14 +388,15 @@ abstract class TracingListenerStrategyTests { Connection connection1 = dataSource.getConnection(); Connection connection2 = dataSource.getConnection(); - connection1.close(); connection2.close(); + connection1.close(); assertThat(spanReporter.spans()).hasSize(2); MutableSpan connection1Span = spanReporter.spans().get(0); MutableSpan connection2Span = spanReporter.spans().get(1); assertThat(connection1Span.name()).isEqualTo("connection"); assertThat(connection2Span.name()).isEqualTo("connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -406,6 +422,7 @@ abstract class TracingListenerStrategyTests { assertThat(statementSpan.name()).isEqualTo("select"); assertThat(resultSetSpan.name()).isEqualTo("result-set"); assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -428,6 +445,7 @@ abstract class TracingListenerStrategyTests { assertThat(listener).extracting("strategy").extracting("openConnections") .isInstanceOfSatisfying(Map.class, map -> assertThat(map).isEmpty()); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -466,6 +484,7 @@ abstract class TracingListenerStrategyTests { assertThat(spanReporter.spans()).hasSize(1 + 2 * 5); assertThat(spanReporter.spans()).extracting("name").contains("select", "result-set", "connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -487,6 +506,7 @@ abstract class TracingListenerStrategyTests { assertThat(spanReporter.spans()).hasSize(1); MutableSpan connectionSpan = spanReporter.spans().get(0); assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -507,6 +527,7 @@ abstract class TracingListenerStrategyTests { assertThat(spanReporter.spans()).hasSize(1); MutableSpan statementSpan = spanReporter.spans().get(0); assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -527,6 +548,7 @@ abstract class TracingListenerStrategyTests { assertThat(spanReporter.spans()).hasSize(1); MutableSpan resultSetSpan = spanReporter.spans().get(0); assertThat(resultSetSpan.name()).isEqualTo("result-set"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -550,6 +572,7 @@ abstract class TracingListenerStrategyTests { MutableSpan statementSpan = spanReporter.spans().get(0); assertThat(connectionSpan.name()).isEqualTo("connection"); assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -573,6 +596,7 @@ abstract class TracingListenerStrategyTests { MutableSpan resultSetSpan = spanReporter.spans().get(0); assertThat(connectionSpan.name()).isEqualTo("connection"); assertThat(resultSetSpan.name()).isEqualTo("result-set"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -596,6 +620,7 @@ abstract class TracingListenerStrategyTests { MutableSpan statementSpan = spanReporter.spans().get(0); assertThat(statementSpan.name()).isEqualTo("select"); assertThat(resultSetSpan.name()).isEqualTo("result-set"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -608,12 +633,14 @@ abstract class TracingListenerStrategyTests { Connection connection = dataSource.getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT NOW()"); connection.close(); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class); assertThat(spanReporter.spans()).hasSize(1); MutableSpan connectionSpan = spanReporter.spans().get(0); assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -634,6 +661,7 @@ abstract class TracingListenerStrategyTests { MutableSpan statementSpan = spanReporter.spans().get(0); assertThat(connectionSpan.name()).isEqualTo("connection"); assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } @@ -659,6 +687,7 @@ abstract class TracingListenerStrategyTests { assertThat(statementSpan.name()).isEqualTo("select"); assertThat(resultSetSpan.name()).isEqualTo("result-set"); assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); }); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategy.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategy.java index 735c0a6f7..9ebe8e830 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategy.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategy.java @@ -1,402 +1,506 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.jdbc; - -import java.net.URI; -import java.sql.Connection; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.sql.CommonDataSource; - -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.SpanAndScope; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.docs.AssertingSpan; -import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; -import org.springframework.lang.Nullable; -import org.springframework.util.StringUtils; - -/** - * Partially taken from - * https://github.com/openzipkin/brave/blob/v5.6.4/instrumentation/p6spy/src/main/java/brave/p6spy/TracingJdbcEventListener.java. - * - * @param connection type - * @param statement - * @param result set - */ -class TraceListenerStrategy { - - private final Map openConnections = new ConcurrentHashMap<>(); - - // Captures all the characters between = and either the next & or the end of the - // string. - private static final Pattern URL_SERVICE_NAME_FINDER = Pattern.compile("sleuthServiceName=(.*?)(?:&|$)"); - - private final Tracer tracer; - - private final List traceTypes; - - private final List customizers; - - TraceListenerStrategy(Tracer tracer, List traceTypes, - List customizers) { - this.tracer = tracer; - this.traceTypes = traceTypes; - this.customizers = customizers; - } - - void beforeGetConnection(CON connectionKey, @Nullable CommonDataSource dataSource, String dataSourceName) { - SpanAndScope spanAndScope = null; - if (this.traceTypes.contains(TraceType.CONNECTION)) { - AssertingSpanBuilder connectionSpanBuilder = AssertingSpanBuilder - .of(SleuthJdbcSpan.JDBC_CONNECTION_SPAN, tracer.spanBuilder()) - .name(SleuthJdbcSpan.JDBC_CONNECTION_SPAN.getName()); - connectionSpanBuilder.remoteServiceName(dataSourceName); - connectionSpanBuilder.kind(Span.Kind.CLIENT); - this.customizers.stream().filter(customizer -> customizer.isApplicable(dataSource)) - .forEach(customizer -> customizer.customizeConnectionSpan(dataSource, connectionSpanBuilder)); - Span connectionSpan = connectionSpanBuilder.start(); - spanAndScope = new SpanAndScope(connectionSpan, tracer.withSpan(connectionSpan)); - } - ConnectionInfo connectionInfo = new ConnectionInfo(spanAndScope); - this.openConnections.put(connectionKey, connectionInfo); - } - - void afterGetConnection(CON connectionKey, Connection connection, Throwable t) { - if (t != null) { - ConnectionInfo connectionInfo = this.openConnections.remove(connectionKey); - connectionInfo.getSpan().ifPresent(connectionSpan -> { - parseServerIpAndPort(connection, connectionSpan.getSpan()); - connectionSpan.getSpan().error(t); - connectionSpan.close(); - }); - return; - } - this.openConnections.get(connectionKey).getSpan().ifPresent(spanAndScope -> { - parseServerIpAndPort(connection, spanAndScope.getSpan()); - }); - } - - void beforeQuery(CON connectionKey, Connection connection, STMT statementKey, String dataSourceName) { - SpanAndScope SpanAndScope = null; - if (traceTypes.contains(TraceType.QUERY)) { - Span.Builder statementSpanBuilder = AssertingSpanBuilder - .of(SleuthJdbcSpan.JDBC_QUERY_SPAN, tracer.spanBuilder()) - .name(SleuthJdbcSpan.JDBC_QUERY_SPAN.getName()); - statementSpanBuilder.remoteServiceName(dataSourceName); - parseServerIpAndPort(connection, statementSpanBuilder); - statementSpanBuilder.kind(Span.Kind.CLIENT); - Span statementSpan = statementSpanBuilder.start(); - SpanAndScope = new SpanAndScope(statementSpan, tracer.withSpan(statementSpan)); - } - StatementInfo statementInfo = new StatementInfo(SpanAndScope); - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - if (connectionInfo == null) { - // Connection may be closed after statement preparation, but before statement - // execution. - return; - } - connectionInfo.getNestedStatements().put(statementKey, statementInfo); - } - - void addQueryRowCount(CON connectionKey, STMT statementKey, int rowCount) { - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - if (connectionInfo == null) { - // Connection is already closed - return; - } - StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); - statementInfo.getSpan() - .ifPresent(statementSpan -> AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, statementSpan.getSpan()) - .tag(SleuthJdbcSpan.QueryTags.ROW_COUNT, String.valueOf(rowCount))); - } - - void afterQuery(CON connectionKey, STMT statementKey, String sql, Throwable t) { - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - if (connectionInfo == null) { - // Connection may be closed after statement preparation, but before statement - // execution. - return; - } - StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); - statementInfo.getSpan().ifPresent(statementSpan -> { - AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, statementSpan.getSpan()) - .tag(SleuthJdbcSpan.QueryTags.QUERY, sql).name(spanName(sql)); - if (t != null) { - statementSpan.getSpan().error(t); - } - statementSpan.close(); - }); - } - - void beforeResultSetNext(CON connectionKey, Connection connection, STMT statementKey, RS resultSetKey, - String dataSourceName) { - if (!traceTypes.contains(TraceType.FETCH)) { - return; - } - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - // ConnectionInfo may be null if Connection was closed before ResultSet - if (connectionInfo == null) { - return; - } - if (connectionInfo.getNestedResultSetSpans().containsKey(resultSetKey)) { - // ResultSet span is already created - return; - } - AssertingSpanBuilder resultSetSpanBuilder = AssertingSpanBuilder - .of(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN, tracer.spanBuilder()) - .name(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN.getName()); - resultSetSpanBuilder.remoteServiceName(dataSourceName); - resultSetSpanBuilder.kind(Span.Kind.CLIENT); - parseServerIpAndPort(connection, resultSetSpanBuilder); - Span resultSetSpan = resultSetSpanBuilder.start(); - SpanAndScope SpanAndScope = new SpanAndScope(resultSetSpan, tracer.withSpan(resultSetSpan)); - connectionInfo.getNestedResultSetSpans().put(resultSetKey, SpanAndScope); - StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); - // StatementInfo may be null when Statement is proxied and instance returned from - // ResultSet is different from instance returned in query method - // in this case if Statement is closed before ResultSet span won't be finished - // immediately, but when Connection is closed - if (statementInfo != null) { - statementInfo.getNestedResultSetSpans().put(resultSetKey, SpanAndScope); - } - } - - void afterResultSetClose(CON connectionKey, RS resultSetKey, int rowCount, Throwable t) { - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - // ConnectionInfo may be null if Connection was closed before ResultSet - if (connectionInfo == null) { - return; - } - SpanAndScope resultSetSpan = connectionInfo.getNestedResultSetSpans().remove(resultSetKey); - // ResultSet span may be null if Statement or ResultSet were already closed - if (resultSetSpan == null) { - return; - } - if (rowCount != -1) { - AssertingSpan.of(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN, resultSetSpan.getSpan()) - .tag(SleuthJdbcSpan.QueryTags.ROW_COUNT, String.valueOf(rowCount)); - } - if (t != null) { - resultSetSpan.getSpan().error(t); - } - resultSetSpan.close(); - } - - void afterStatementClose(CON connectionKey, STMT statementKey) { - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - // ConnectionInfo may be null if Connection was closed before Statement - if (connectionInfo == null) { - return; - } - StatementInfo statementInfo = connectionInfo.getNestedStatements().remove(statementKey); - if (statementInfo != null) { - statementInfo.getNestedResultSetSpans().forEach((resultSetKey, span) -> { - connectionInfo.getNestedResultSetSpans().remove(resultSetKey); - span.close(); - }); - statementInfo.getNestedResultSetSpans().clear(); - } - } - - void afterCommit(CON connectionKey, Throwable t) { - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - if (connectionInfo == null) { - // Connection is already closed - return; - } - connectionInfo.getSpan().ifPresent(connectionSpan -> { - if (t != null) { - connectionSpan.getSpan().error(t); - } - AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, connectionSpan.getSpan()) - .event(SleuthJdbcSpan.QueryEvents.COMMIT); - }); - } - - void afterRollback(CON connectionKey, Throwable t) { - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - if (connectionInfo == null) { - // Connection is already closed - return; - } - connectionInfo.getSpan().ifPresent(connectionSpan -> { - if (t != null) { - connectionSpan.getSpan().error(t); - } - else { - connectionSpan.getSpan().error(new JdbcException("Transaction rolled back")); - } - AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, connectionSpan.getSpan()) - .event(SleuthJdbcSpan.QueryEvents.ROLLBACK); - }); - } - - void afterConnectionClose(CON connectionKey, Throwable t) { - ConnectionInfo connectionInfo = openConnections.remove(connectionKey); - if (connectionInfo == null) { - // connection is already closed - return; - } - connectionInfo.getNestedResultSetSpans().values().forEach(SpanAndScope::close); - connectionInfo.getNestedStatements().values() - .forEach(statementInfo -> statementInfo.getSpan().ifPresent(SpanAndScope::close)); - connectionInfo.getSpan().ifPresent(connectionSpan -> { - if (t != null) { - connectionSpan.getSpan().error(t); - } - connectionSpan.close(); - }); - } - - private String spanName(String sql) { - return sql.substring(0, sql.indexOf(' ')).toLowerCase(Locale.ROOT); - } - - private void parseServerIpAndPort(Connection connection, Span.Builder span) { - if (connection == null) { - return; - } - UrlAndRemoteServiceName urlAndRemoteServiceName = parseServerIpAndPort(connection); - span.remoteServiceName(urlAndRemoteServiceName.remoteServiceName); - URI url = urlAndRemoteServiceName.url; - if (url != null) { - span.remoteIpAndPort(url.getHost(), url.getPort()); - } - } - - private void parseServerIpAndPort(Connection connection, Span span) { - if (connection == null) { - return; - } - UrlAndRemoteServiceName urlAndRemoteServiceName = parseServerIpAndPort(connection); - span.remoteServiceName(urlAndRemoteServiceName.remoteServiceName); - URI url = urlAndRemoteServiceName.url; - if (url != null) { - span.remoteIpAndPort(url.getHost(), url.getPort()); - } - } - - /** - * This attempts to get the ip and port from the JDBC URL. Ex. localhost and 5555 from - * {@code - * jdbc:mysql://localhost:5555/mydatabase}. - * - * Taken from Brave. - */ - private UrlAndRemoteServiceName parseServerIpAndPort(Connection connection) { - String remoteServiceName = ""; - try { - String urlAsString = connection.getMetaData().getURL().substring(5); // strip - // "jdbc:" - URI url = URI.create(urlAsString.replace(" ", "")); // Remove all white space - // according to RFC 2396 - Matcher matcher = URL_SERVICE_NAME_FINDER.matcher(url.toString()); - if (matcher.find() && matcher.groupCount() == 1) { - String parsedServiceName = matcher.group(1); - if (parsedServiceName != null && !parsedServiceName.isEmpty()) { - remoteServiceName = parsedServiceName; - } - } - if (!StringUtils.hasText(remoteServiceName)) { - String databaseName = connection.getCatalog(); - if (databaseName != null && !databaseName.isEmpty()) { - remoteServiceName = databaseName; - } - } - return new UrlAndRemoteServiceName(url, remoteServiceName); - } - catch (Exception e) { - // remote address is optional - return new UrlAndRemoteServiceName(null, remoteServiceName); - } - } - - private final class ConnectionInfo { - - private final SpanAndScope span; - - private final Map nestedStatements = new ConcurrentHashMap<>(); - - private final Map nestedResultSetSpans = new ConcurrentHashMap<>(); - - private ConnectionInfo(@Nullable SpanAndScope span) { - this.span = span; - } - - Optional getSpan() { - return Optional.ofNullable(span); - } - - Map getNestedStatements() { - return nestedStatements; - } - - Map getNestedResultSetSpans() { - return nestedResultSetSpans; - } - - } - - private final class StatementInfo { - - private final SpanAndScope span; - - private final Map nestedResultSetSpans = new ConcurrentHashMap<>(); - - private StatementInfo(SpanAndScope span) { - this.span = span; - } - - Optional getSpan() { - return Optional.ofNullable(span); - } - - Map getNestedResultSetSpans() { - return nestedResultSetSpans; - } - - } - - private final class UrlAndRemoteServiceName { - - final URI url; - - final String remoteServiceName; - - private UrlAndRemoteServiceName(@Nullable URI url, String remoteServiceName) { - this.url = url; - this.remoteServiceName = remoteServiceName; - } - - } - - private static final class JdbcException extends RuntimeException { - - JdbcException(String message) { - super(message); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.net.URI; +import java.sql.Connection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.sql.CommonDataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; +import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Partially taken from + * https://github.com/openzipkin/brave/blob/v5.6.4/instrumentation/p6spy/src/main/java/brave/p6spy/TracingJdbcEventListener.java. + * + * @param connection type + * @param statement + * @param result set + */ +class TraceListenerStrategy { + + private static final Log log = LogFactory.getLog(TraceListenerStrategy.class); + + private final Map openConnections = new ConcurrentHashMap<>(); + + // Captures all the characters between = and either the next & or the end of the + // string. + private static final Pattern URL_SERVICE_NAME_FINDER = Pattern.compile("sleuthServiceName=(.*?)(?:&|$)"); + + private final Tracer tracer; + + private final List traceTypes; + + private final List customizers; + + TraceListenerStrategy(Tracer tracer, List traceTypes, + List customizers) { + this.tracer = tracer; + this.traceTypes = traceTypes; + this.customizers = customizers; + } + + void beforeGetConnection(CON connectionKey, @Nullable CommonDataSource dataSource, String dataSourceName) { + if (log.isTraceEnabled()) { + log.trace("Before get connection key [" + connectionKey + "] - current span is [" + tracer.currentSpan() + + "]"); + } + SpanAndScope spanAndScope = null; + if (this.traceTypes.contains(TraceType.CONNECTION)) { + AssertingSpanBuilder connectionSpanBuilder = AssertingSpanBuilder + .of(SleuthJdbcSpan.JDBC_CONNECTION_SPAN, tracer.spanBuilder()) + .name(SleuthJdbcSpan.JDBC_CONNECTION_SPAN.getName()); + connectionSpanBuilder.remoteServiceName(dataSourceName); + connectionSpanBuilder.kind(Span.Kind.CLIENT); + this.customizers.stream().filter(customizer -> customizer.isApplicable(dataSource)) + .forEach(customizer -> customizer.customizeConnectionSpan(dataSource, connectionSpanBuilder)); + Span connectionSpan = connectionSpanBuilder.start(); + spanAndScope = new SpanAndScope(connectionSpan, tracer.withSpan(connectionSpan)); + if (log.isTraceEnabled()) { + log.trace("Started client span before connection [" + connectionSpan + "] - current span is [" + + tracer.currentSpan() + "]"); + } + } + ConnectionInfo connectionInfo = new ConnectionInfo(spanAndScope); + this.openConnections.put(connectionKey, connectionInfo); + } + + void afterGetConnection(CON connectionKey, Connection connection, Throwable t) { + if (log.isTraceEnabled()) { + log.trace("After get connection [" + connectionKey + "]. Current span is [" + tracer.currentSpan() + "]"); + } + this.openConnections.get(connectionKey).getSpan().ifPresent(spanAndScope -> { + parseServerIpAndPort(connection, spanAndScope.getSpan()); + }); + if (t != null) { + ConnectionInfo connectionInfo = this.openConnections.remove(connectionKey); + connectionInfo.getSpan().ifPresent(connectionSpan -> { + parseServerIpAndPort(connection, connectionSpan.getSpan()); + if (log.isTraceEnabled()) { + log.trace("Closing client span due to exception [" + connectionSpan.getSpan() + + "] - current span is [" + tracer.currentSpan() + "]"); + } + connectionSpan.getSpan().error(t); + connectionSpan.close(); + if (log.isTraceEnabled()) { + log.trace("Current span [" + tracer.currentSpan() + "]"); + } + }); + } + } + + void beforeQuery(CON connectionKey, Connection connection, STMT statementKey, String dataSourceName) { + if (log.isTraceEnabled()) { + log.trace("Before query - connection [" + connectionKey + "] and current span [" + tracer.currentSpan() + + "]"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + if (log.isTraceEnabled()) { + log.trace("Connection may be closed after statement preparation, but before statement execution"); + } + return; + } + SpanAndScope spanAndScope = null; + if (traceTypes.contains(TraceType.QUERY)) { + Span.Builder statementSpanBuilder = AssertingSpanBuilder + .of(SleuthJdbcSpan.JDBC_QUERY_SPAN, tracer.spanBuilder()) + .name(String.format(SleuthJdbcSpan.JDBC_QUERY_SPAN.getName(), "query")); + statementSpanBuilder.remoteServiceName(dataSourceName); + parseServerIpAndPort(connection, statementSpanBuilder); + statementSpanBuilder.kind(Span.Kind.CLIENT); + Span statementSpan = statementSpanBuilder.start(); + spanAndScope = new SpanAndScope(statementSpan, tracer.withSpan(statementSpan)); + if (log.isTraceEnabled()) { + log.trace("Started client span before query [" + statementSpan + "] - current span is [" + + tracer.currentSpan() + "]"); + } + } + StatementInfo statementInfo = new StatementInfo(spanAndScope); + connectionInfo.getNestedStatements().put(statementKey, statementInfo); + } + + void addQueryRowCount(CON connectionKey, STMT statementKey, int rowCount) { + if (log.isTraceEnabled()) { + log.trace("Add query row count for connection key [" + connectionKey + "]"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + if (log.isTraceEnabled()) { + log.trace("Connection is already closed"); + } + return; + } + StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); + statementInfo.getSpan() + .ifPresent(statementSpan -> AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, statementSpan.getSpan()) + .tag(SleuthJdbcSpan.QueryTags.ROW_COUNT, String.valueOf(rowCount))); + } + + void afterQuery(CON connectionKey, STMT statementKey, String sql, Throwable t) { + if (log.isTraceEnabled()) { + log.trace("After query for connection key [" + connectionKey + "]"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + if (log.isTraceEnabled()) { + log.trace( + "Connection may be closed after statement preparation, but before statement execution. Current span is [" + + tracer.currentSpan() + "]"); + } + return; + } + StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); + statementInfo.getSpan().ifPresent(statementSpan -> { + updateQuerySpan(sql, t, statementSpan); + statementSpan.close(); + if (log.isTraceEnabled()) { + log.trace("Current span [" + tracer.currentSpan() + "]"); + } + }); + } + + private void updateQuerySpan(String sql, Throwable t, SpanAndScope statementSpan) { + AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, statementSpan.getSpan()) + .tag(SleuthJdbcSpan.QueryTags.QUERY, sql).name(spanName(sql)); + if (t != null) { + statementSpan.getSpan().error(t); + } + if (log.isTraceEnabled()) { + log.trace( + "Closing statement span [" + statementSpan + "] - current span is [" + tracer.currentSpan() + "]"); + } + } + + void beforeResultSetNext(CON connectionKey, Connection connection, STMT statementKey, RS resultSetKey, + String dataSourceName) { + if (log.isTraceEnabled()) { + log.trace("Before result set next"); + } + if (!traceTypes.contains(TraceType.FETCH)) { + return; + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + // ConnectionInfo may be null if Connection was closed before ResultSet + if (connectionInfo == null) { + if (log.isTraceEnabled()) { + log.trace("No connection info, skipping"); + } + return; + } + if (connectionInfo.getNestedResultSetSpans().containsKey(resultSetKey)) { + if (log.isTraceEnabled()) { + log.trace("ResultSet span is already created"); + } + return; + } + AssertingSpanBuilder resultSetSpanBuilder = AssertingSpanBuilder + .of(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN, tracer.spanBuilder()) + .name(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN.getName()); + resultSetSpanBuilder.remoteServiceName(dataSourceName); + resultSetSpanBuilder.kind(Span.Kind.CLIENT); + parseServerIpAndPort(connection, resultSetSpanBuilder); + Span resultSetSpan = resultSetSpanBuilder.start(); + SpanAndScope SpanAndScope = new SpanAndScope(resultSetSpan, tracer.withSpan(resultSetSpan)); + if (log.isTraceEnabled()) { + log.trace("Started client result set span [" + resultSetSpan + "] - current span is [" + + tracer.currentSpan() + "]"); + } + connectionInfo.getNestedResultSetSpans().put(resultSetKey, SpanAndScope); + StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); + // StatementInfo may be null when Statement is proxied and instance returned from + // ResultSet is different from instance returned in query method + // in this case if Statement is closed before ResultSet span won't be finished + // immediately, but when Connection is closed + if (statementInfo != null) { + statementInfo.getNestedResultSetSpans().put(resultSetKey, SpanAndScope); + } + } + + void afterResultSetClose(CON connectionKey, RS resultSetKey, int rowCount, Throwable t) { + if (log.isTraceEnabled()) { + log.trace("After result set close"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + // ConnectionInfo may be null if Connection was closed before ResultSet + if (connectionInfo == null) { + return; + } + SpanAndScope resultSetSpan = connectionInfo.getNestedResultSetSpans().remove(resultSetKey); + // ResultSet span may be null if Statement or ResultSet were already closed + if (resultSetSpan == null) { + return; + } + if (rowCount != -1) { + AssertingSpan.of(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN, resultSetSpan.getSpan()) + .tag(SleuthJdbcSpan.QueryTags.ROW_COUNT, String.valueOf(rowCount)); + } + if (t != null) { + resultSetSpan.getSpan().error(t); + } + if (log.isTraceEnabled()) { + log.trace("Closing client result set span [" + resultSetSpan + "] - current span is [" + + tracer.currentSpan() + "]"); + } + resultSetSpan.close(); + if (log.isTraceEnabled()) { + log.trace("Current span [" + tracer.currentSpan() + "]"); + } + } + + void afterStatementClose(CON connectionKey, STMT statementKey) { + if (log.isTraceEnabled()) { + log.trace("After statement close"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + // ConnectionInfo may be null if Connection was closed before Statement + if (connectionInfo == null) { + return; + } + StatementInfo statementInfo = connectionInfo.getNestedStatements().remove(statementKey); + if (statementInfo != null) { + statementInfo.getNestedResultSetSpans().forEach((resultSetKey, span) -> { + connectionInfo.getNestedResultSetSpans().remove(resultSetKey); + if (log.isTraceEnabled()) { + log.trace("Closing span after statement close [" + span.getSpan() + "] - current span is [" + + tracer.currentSpan() + "]"); + } + span.close(); + if (log.isTraceEnabled()) { + log.trace("Current span [" + tracer.currentSpan() + "]"); + } + }); + statementInfo.getNestedResultSetSpans().clear(); + } + } + + void afterCommit(CON connectionKey, Throwable t) { + if (log.isTraceEnabled()) { + log.trace("After commit"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + // Connection is already closed + return; + } + connectionInfo.getSpan().ifPresent(connectionSpan -> { + if (t != null) { + connectionSpan.getSpan().error(t); + } + AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, connectionSpan.getSpan()) + .event(SleuthJdbcSpan.QueryEvents.COMMIT); + }); + } + + void afterRollback(CON connectionKey, Throwable t) { + if (log.isTraceEnabled()) { + log.trace("After rollback"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + // Connection is already closed + return; + } + connectionInfo.getSpan().ifPresent(connectionSpan -> { + if (t != null) { + connectionSpan.getSpan().error(t); + } + else { + connectionSpan.getSpan().error(new JdbcException("Transaction rolled back")); + } + AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, connectionSpan.getSpan()) + .event(SleuthJdbcSpan.QueryEvents.ROLLBACK); + }); + } + + void afterConnectionClose(CON connectionKey, Throwable t) { + if (log.isTraceEnabled()) { + log.trace("After connection close with key [" + connectionKey + "]"); + } + ConnectionInfo connectionInfo = openConnections.remove(connectionKey); + if (connectionInfo == null) { + // connection is already closed + return; + } + connectionInfo.getNestedResultSetSpans().values().forEach(SpanAndScope::close); + connectionInfo.getNestedStatements().values() + .forEach(statementInfo -> statementInfo.getSpan().ifPresent(SpanAndScope::close)); + if (log.isTraceEnabled()) { + log.trace("Current span after closing statements [" + tracer.currentSpan() + "]"); + } + connectionInfo.getSpan().ifPresent(connectionSpan -> { + if (t != null) { + connectionSpan.getSpan().error(t); + } + if (log.isTraceEnabled()) { + log.trace("Closing span after connection close [" + connectionSpan.getSpan() + "] - current span is [" + + tracer.currentSpan() + "]"); + } + connectionSpan.close(); + if (log.isTraceEnabled()) { + log.trace("Current span [" + tracer.currentSpan() + "]"); + } + }); + } + + private String spanName(String sql) { + return sql.substring(0, sql.indexOf(' ')).toLowerCase(Locale.ROOT); + } + + private void parseServerIpAndPort(Connection connection, Span.Builder span) { + if (connection == null) { + return; + } + UrlAndRemoteServiceName urlAndRemoteServiceName = parseServerIpAndPort(connection); + span.remoteServiceName(urlAndRemoteServiceName.remoteServiceName); + URI url = urlAndRemoteServiceName.url; + if (url != null) { + span.remoteIpAndPort(url.getHost(), url.getPort()); + } + } + + private void parseServerIpAndPort(Connection connection, Span span) { + if (connection == null) { + return; + } + UrlAndRemoteServiceName urlAndRemoteServiceName = parseServerIpAndPort(connection); + span.remoteServiceName(urlAndRemoteServiceName.remoteServiceName); + URI url = urlAndRemoteServiceName.url; + if (url != null) { + span.remoteIpAndPort(url.getHost(), url.getPort()); + } + } + + /** + * This attempts to get the ip and port from the JDBC URL. Ex. localhost and 5555 from + * {@code + * jdbc:mysql://localhost:5555/mydatabase}. + * + * Taken from Brave. + */ + private UrlAndRemoteServiceName parseServerIpAndPort(Connection connection) { + String remoteServiceName = ""; + try { + String urlAsString = connection.getMetaData().getURL().substring(5); // strip + // "jdbc:" + URI url = URI.create(urlAsString.replace(" ", "")); // Remove all white space + // according to RFC 2396 + Matcher matcher = URL_SERVICE_NAME_FINDER.matcher(url.toString()); + if (matcher.find() && matcher.groupCount() == 1) { + String parsedServiceName = matcher.group(1); + if (parsedServiceName != null && !parsedServiceName.isEmpty()) { + remoteServiceName = parsedServiceName; + } + } + if (!StringUtils.hasText(remoteServiceName)) { + String databaseName = connection.getCatalog(); + if (databaseName != null && !databaseName.isEmpty()) { + remoteServiceName = databaseName; + } + } + return new UrlAndRemoteServiceName(url, remoteServiceName); + } + catch (Exception e) { + // remote address is optional + return new UrlAndRemoteServiceName(null, remoteServiceName); + } + } + + private final class ConnectionInfo { + + private final SpanAndScope span; + + private final Map nestedStatements = new ConcurrentHashMap<>(); + + private final Map nestedResultSetSpans = new ConcurrentHashMap<>(); + + private ConnectionInfo(@Nullable SpanAndScope span) { + this.span = span; + } + + Optional getSpan() { + return Optional.ofNullable(span); + } + + Map getNestedStatements() { + return nestedStatements; + } + + Map getNestedResultSetSpans() { + return nestedResultSetSpans; + } + + } + + private final class StatementInfo { + + private final SpanAndScope span; + + private final Map nestedResultSetSpans = new ConcurrentHashMap<>(); + + private StatementInfo(SpanAndScope span) { + this.span = span; + } + + Optional getSpan() { + return Optional.ofNullable(span); + } + + Map getNestedResultSetSpans() { + return nestedResultSetSpans; + } + + } + + private final class UrlAndRemoteServiceName { + + final URI url; + + final String remoteServiceName; + + private UrlAndRemoteServiceName(@Nullable URI url, String remoteServiceName) { + this.url = url; + this.remoteServiceName = remoteServiceName; + } + + } + + private static final class JdbcException extends RuntimeException { + + JdbcException(String message) { + super(message); + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceQueryExecutionListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceQueryExecutionListener.java index df4bfa8f3..2206a287e 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceQueryExecutionListener.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceQueryExecutionListener.java @@ -121,6 +121,9 @@ public class TraceQueryExecutionListener implements QueryExecutionListener, Meth try { DataSource source = targetDataSource instanceof ProxyDataSource ? (targetDataSource).unwrap(DataSource.class) : targetDataSource; + if (source == null) { + return null; + } return source.getConnection(); } catch (Exception ex) { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/SleuthContextListener.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/SleuthContextListener.java index b4daa79f7..587ffc163 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/SleuthContextListener.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/SleuthContextListener.java @@ -81,8 +81,8 @@ public class SleuthContextListener implements SmartApplicationListener { @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ContextRefreshedEvent || event instanceof ContextClosedEvent) { - if (log.isDebugEnabled()) { - log.debug("Context refreshed or closed [" + event + "]"); + if (log.isTraceEnabled()) { + log.trace("Context refreshed or closed [" + event + "]"); } ApplicationContextEvent contextEvent = (ApplicationContextEvent) event; ApplicationContext context = contextEvent.getApplicationContext(); From 682398daecb02ca24593b8c830b4d7cc2600c98b Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 26 May 2021 18:36:50 +0200 Subject: [PATCH 38/59] Polish --- docs/src/main/asciidoc/_configprops.adoc | 224 ++-- docs/src/main/asciidoc/integrations.adoc | 4 +- .../TraceAsyncDefaultAutoConfiguration.java | 304 ++--- .../jdbc/DataSourceProxyConfiguration.java | 3 +- .../jdbc/P6SpyPropertiesSetter.java | 20 +- ...eDataSourceDecoratorAutoConfiguration.java | 3 +- .../TraceDataSourceDecoratorProperties.java | 19 +- .../instrument/web/client/WebClientTests.java | 411 +++--- ...SourceDecoratorAutoConfigurationTests.java | 6 +- .../jdbc/P6SpyConfigurationTests.java | 561 ++++---- .../ProxyDataSourceConfigurationTests.java | 492 +++---- .../jdbc/TracingJdbcEventListenerTests.java | 4 +- .../jdbc/TracingListenerStrategyTests.java | 140 +- .../DataSourceProxyBuilderCustomizer.java | 3 - .../jdbc/DataSourceProxyProperties.java | 17 - .../instrument/jdbc/DataSourceWrapper.java | 5 +- .../jdbc/TraceListenerStrategy.java | 1012 +++++++------- .../instrument/kafka/KafkaProducerTest.java | 132 +- .../web/TraceAsyncIntegrationTests.java | 470 +++---- .../r2dbc/R2dbcIntegrationTests.java | 204 +-- .../integration/sampled/WebClientTests.java | 1194 ++++++++--------- 21 files changed, 2592 insertions(+), 2636 deletions(-) diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 1399bc59a..26042bb09 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -1,112 +1,112 @@ -|=== -|Name | Default | Description - -|spring.sleuth.async.configurer.enabled | `true` | Enable default AsyncConfigurer. -|spring.sleuth.async.enabled | `true` | Enable instrumenting async related components so that the tracing information is passed between threads. -|spring.sleuth.async.ignored-beans | | List of {@link java.util.concurrent.Executor} bean names that should be ignored and not wrapped in a trace representation. -|spring.sleuth.baggage.correlation-enabled | `true` | Enables correlating the baggage context with logging contexts. -|spring.sleuth.baggage.correlation-fields | | List of fields that should be propagated over the wire. -|spring.sleuth.baggage.local-fields | | List of fields that should be accessible within the JVM process but not propagated over the wire. -|spring.sleuth.baggage.remote-fields | | List of fields that are referenced the same in-process as it is on the wire. For example, the field "x-vcap-request-id" would be set as-is including the prefix. -|spring.sleuth.baggage.tag-fields | | List of fields that should automatically become tags. -|spring.sleuth.batch.enabled | `true` | Enable Spring Batch instrumentation. -|spring.sleuth.circuitbreaker.enabled | `true` | Enable Spring Cloud CircuitBreaker instrumentation. -|spring.sleuth.config.server.enabled | `true` | Enable Spring Cloud Config Server instrumentation. -|spring.sleuth.deployer.enabled | `true` | Enable Spring Cloud Deployer instrumentation. -|spring.sleuth.deployer.status-poll-delay | `500` | Default poll delay to retrieve the deployed application status. -|spring.sleuth.enabled | `true` | -|spring.sleuth.feign.enabled | `true` | Enable span information propagation when using Feign. -|spring.sleuth.feign.processor.enabled | `true` | Enable post processor that wraps Feign Context in its tracing representations. -|spring.sleuth.function.enabled | `true` | Enable instrumenting of Spring Cloud Function and Spring Cloud Function based projects (e.g. Spring Cloud Stream). -|spring.sleuth.grpc.enabled | `true` | Enable span information propagation when using GRPC. -|spring.sleuth.http.enabled | `true` | Enables HTTP support. -|spring.sleuth.integration.enabled | `true` | Enable Spring Integration instrumentation. -|spring.sleuth.integration.patterns | `[!hystrixStreamOutput*, *, !channel*]` | An array of patterns against which channel names will be matched. @see org.springframework.integration.config.GlobalChannelInterceptor#patterns() Defaults to any channel name not matching the Hystrix Stream and functional Stream channel names. -|spring.sleuth.integration.websockets.enabled | `true` | Enable tracing for WebSockets. -|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.count-query | `false` | Creates listener to count queries. @see ProxyDataSourceBuilder#countQuery() @see QueryCountHolder -|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.json-format | `false` | Use json output for logging query. @see ProxyDataSourceBuilder#asJson() -|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.logging | | Logging to use for logging queries. -|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.multiline | `true` | Use multiline output for logging query. @see ProxyDataSourceBuilder#multiline() -|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.query.enable-logging | `true` | Enable logging all queries to the log. -|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.query.log-level | `DEBUG` | Severity of query logger. -|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.query.logger-name | | Name of query logger. -|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.slow-query.log-level | `WARN` | Severity of slow query logger. -|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.slow-query.logger-name | | Name of slow query logger. -|spring.sleuth.jdbc.decorator.datasource.datasource-proxy.slow-query.threshold | `300` | Number of seconds to consider query as slow. -|spring.sleuth.jdbc.decorator.datasource.enabled | `true` | Enables data source decorating. -|spring.sleuth.jdbc.decorator.datasource.excluded-beans | | Beans that won't be decorated. -|spring.sleuth.jdbc.decorator.datasource.includes | | Which types of tracing we would like to include. -|spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class | | Class file to use (only with logging=custom). The class must implement {@link com.p6spy.engine.spy.appender.FormattedLogger}. -|spring.sleuth.jdbc.decorator.datasource.p6spy.enable-logging | `true` | Enables logging JDBC events. -|spring.sleuth.jdbc.decorator.datasource.p6spy.log-file | `spy.log` | Name of log file to use (only with logging=file). -|spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern | | Use regex pattern to filter log messages. Only matched messages will be logged. -|spring.sleuth.jdbc.decorator.datasource.p6spy.log-format | | Custom log format. -|spring.sleuth.jdbc.decorator.datasource.p6spy.logging | | Logging to use for logging queries. -|spring.sleuth.jdbc.decorator.datasource.p6spy.multiline | `true` | Enables multiline output. -|spring.sleuth.jdbc.decorator.datasource.p6spy.tracing.include-parameter-values | `true` | Report the effective sql string (with '?' replaced with real values) to tracing systems.

NOTE this setting does not affect the logging message. -|spring.sleuth.kafka.enabled | `true` | Enable instrumenting of Apache Kafka clients. -|spring.sleuth.messaging.aspect.enabled | `false` | Should {@link MessageMapping} wrapping be enabled. -|spring.sleuth.messaging.enabled | `false` | Should messaging be turned on. -|spring.sleuth.messaging.jms.enabled | `true` | Enable tracing of JMS. -|spring.sleuth.messaging.jms.remote-service-name | `jms` | JMS remote service name. -|spring.sleuth.messaging.kafka.enabled | `true` | Enable tracing of Kafka. -|spring.sleuth.messaging.kafka.mapper.enabled | `true` | Enable DefaultKafkaHeaderMapper tracing for Kafka. -|spring.sleuth.messaging.kafka.remote-service-name | `kafka` | Kafka remote service name. -|spring.sleuth.messaging.kafka.streams.enabled | `false` | Should Kafka Streams be turned on. -|spring.sleuth.messaging.rabbit.enabled | `true` | Enable tracing of RabbitMQ. -|spring.sleuth.messaging.rabbit.remote-service-name | `rabbitmq` | Rabbit remote service name. -|spring.sleuth.mongodb.enabled | `true` | Enable tracing for MongoDb. -|spring.sleuth.opentracing.enabled | `true` | Enables OpenTracing support. -|spring.sleuth.propagation.type | | Tracing context propagation types. -|spring.sleuth.quartz.enabled | `true` | Enable tracing for Quartz. -|spring.sleuth.r2dbc.enabled | `true` | Enable R2dbc instrumentation. -|spring.sleuth.reactor.decorate-on-each | `true` | When true decorates on each operator, will be less performing, but logging will always contain the tracing entries in each operator. When false decorates on last operator, will be more performing, but logging might not always contain the tracing entries. @deprecated use explicit value via {@link SleuthReactorProperties#instrumentationType} -|spring.sleuth.reactor.enabled | `true` | When true enables instrumentation for reactor. -|spring.sleuth.reactor.instrumentation-type | | -|spring.sleuth.redis.enabled | `true` | Enable span information propagation when using Redis. -|spring.sleuth.redis.remote-service-name | `redis` | Service name for the remote Redis endpoint. -|spring.sleuth.rpc.enabled | `true` | Enable tracing of RPC. -|spring.sleuth.rsocket.enabled | `true` | When true enables instrumentation for rsocket. -|spring.sleuth.rxjava.schedulers.hook.enabled | `true` | Enable support for RxJava via RxJavaSchedulersHook. -|spring.sleuth.rxjava.schedulers.ignoredthreads | `[HystrixMetricPoller, ^RxComputation.*$]` | Thread names for which spans will not be sampled. -|spring.sleuth.sampler.probability | | Probability of requests that should be sampled. E.g. 1.0 - 100% requests should be sampled. The precision is whole-numbers only (i.e. there's no support for 0.1% of the traces). -|spring.sleuth.sampler.rate | `10` | A rate per second can be a nice choice for low-traffic endpoints as it allows you surge protection. For example, you may never expect the endpoint to get more than 50 requests per second. If there was a sudden surge of traffic, to 5000 requests per second, you would still end up with 50 traces per second. Conversely, if you had a percentage, like 10%, the same surge would end up with 500 traces per second, possibly overloading your storage. Amazon X-Ray includes a rate-limited sampler (named Reservoir) for this purpose. Brave has taken the same approach via the {@link brave.sampler.RateLimitingSampler}. -|spring.sleuth.sampler.refresh.enabled | `true` | Enable refresh scope for sampler. -|spring.sleuth.scheduled.enabled | `true` | Enable tracing for {@link org.springframework.scheduling.annotation.Scheduled}. -|spring.sleuth.scheduled.skip-pattern | | Pattern for the fully qualified name of a class that should be skipped. -|spring.sleuth.span-filter.additional-span-name-patterns-to-ignore | | Additional list of span names to ignore. Will be appended to {@link #spanNamePatternsToSkip}. -|spring.sleuth.span-filter.enabled | `false` | Will turn on the default Sleuth handler mechanism. Might ignore exporting of certain spans; -|spring.sleuth.span-filter.span-name-patterns-to-skip | `^catalogWatchTaskScheduler$` | List of span names to ignore. They will not be sent to external systems. -|spring.sleuth.supports-join | `true` | True means the tracing system supports sharing a span ID between a client and server. -|spring.sleuth.task.enabled | `true` | Enable Spring Cloud Task instrumentation. -|spring.sleuth.trace-id128 | `false` | When true, generate 128-bit trace IDs instead of 64-bit ones. -|spring.sleuth.tracer.mode | | Set which tracer implementation should be picked. -|spring.sleuth.tx.enabled | `true` | Enable Spring TX instrumentation. -|spring.sleuth.vault.enabled | `true` | Enable Spring Vault instrumentation. -|spring.sleuth.web.additional-skip-pattern | | Additional pattern for URLs that should be skipped in tracing. This will be appended to the {@link SleuthWebProperties#skipPattern}. -|spring.sleuth.web.client.enabled | `true` | Enable interceptor injecting into {@link org.springframework.web.client.RestTemplate}. -|spring.sleuth.web.client.skip-pattern | | Pattern for URLs that should be skipped in client side tracing. -|spring.sleuth.web.enabled | `true` | When true enables instrumentation for web applications. -|spring.sleuth.web.filter-order | `0` | Order in which the tracing filters should be registered. -|spring.sleuth.web.ignore-auto-configured-skip-patterns | `false` | If set to true, auto-configured skip patterns will be ignored. -|spring.sleuth.web.servlet.enabled | `true` | Enable servlet instrumentation. -|spring.sleuth.web.skip-pattern | `/api-docs.*\|/swagger.*\|.*\.png\|.*\.css\|.*\.js\|.*\.html\|/favicon.ico\|/hystrix.stream` | Pattern for URLs that should be skipped in tracing. -|spring.sleuth.web.tomcat.enabled | `true` | Enable tracing instrumentation for Tomcat. -|spring.sleuth.web.webclient.enabled | `true` | Enable tracing instrumentation for WebClient. -|spring.zipkin.activemq.message-max-bytes | `100000` | Maximum number of bytes for a given message with spans sent to Zipkin over ActiveMQ. -|spring.zipkin.activemq.queue | `zipkin` | Name of the ActiveMQ queue where spans should be sent to Zipkin. -|spring.zipkin.api-path | | The API path to append to baseUrl (above) as suffix. This applies if you use other monitoring tools, such as New Relic. The trace API doesn't need the API path, so you can set it to blank ("") in the configuration. -|spring.zipkin.base-url | `http://localhost:9411/` | URL of the zipkin query server instance. You can also provide the service id of the Zipkin server if Zipkin's registered in service discovery (e.g. https://zipkinserver/). -|spring.zipkin.compression.enabled | `false` | -|spring.zipkin.discovery-client-enabled | | If set to {@code false}, will treat the {@link ZipkinProperties#baseUrl} as a URL always. -|spring.zipkin.enabled | `true` | Enables sending spans to Zipkin. -|spring.zipkin.encoder | | Encoding type of spans sent to Zipkin. Set to {@link SpanBytesEncoder#JSON_V1} if your server is not recent. -|spring.zipkin.kafka.topic | `zipkin` | Name of the Kafka topic where spans should be sent to Zipkin. -|spring.zipkin.locator.discovery.enabled | `false` | Enabling of locating the host name via service discovery. -|spring.zipkin.message-timeout | `1` | Timeout in seconds before pending spans will be sent in batches to Zipkin. -|spring.zipkin.rabbitmq.addresses | | Addresses of the RabbitMQ brokers used to send spans to Zipkin -|spring.zipkin.rabbitmq.queue | `zipkin` | Name of the RabbitMQ queue where spans should be sent to Zipkin. -|spring.zipkin.sender.type | | Means of sending spans to Zipkin. -|spring.zipkin.service.name | | The name of the service, from which the Span was sent via HTTP, that should appear in Zipkin. - -|=== \ No newline at end of file +|=== +|Name | Default | Description + +|spring.sleuth.async.configurer.enabled | `true` | Enable default AsyncConfigurer. +|spring.sleuth.async.enabled | `true` | Enable instrumenting async related components so that the tracing information is passed between threads. +|spring.sleuth.async.ignored-beans | | List of {@link java.util.concurrent.Executor} bean names that should be ignored and not wrapped in a trace representation. +|spring.sleuth.baggage.correlation-enabled | `true` | Enables correlating the baggage context with logging contexts. +|spring.sleuth.baggage.correlation-fields | | List of fields that should be propagated over the wire. +|spring.sleuth.baggage.local-fields | | List of fields that should be accessible within the JVM process but not propagated over the wire. +|spring.sleuth.baggage.remote-fields | | List of fields that are referenced the same in-process as it is on the wire. For example, the field "x-vcap-request-id" would be set as-is including the prefix. +|spring.sleuth.baggage.tag-fields | | List of fields that should automatically become tags. +|spring.sleuth.batch.enabled | `true` | Enable Spring Batch instrumentation. +|spring.sleuth.circuitbreaker.enabled | `true` | Enable Spring Cloud CircuitBreaker instrumentation. +|spring.sleuth.config.server.enabled | `true` | Enable Spring Cloud Config Server instrumentation. +|spring.sleuth.deployer.enabled | `true` | Enable Spring Cloud Deployer instrumentation. +|spring.sleuth.deployer.status-poll-delay | `500` | Default poll delay to retrieve the deployed application status. +|spring.sleuth.enabled | `true` | +|spring.sleuth.feign.enabled | `true` | Enable span information propagation when using Feign. +|spring.sleuth.feign.processor.enabled | `true` | Enable post processor that wraps Feign Context in its tracing representations. +|spring.sleuth.function.enabled | `true` | Enable instrumenting of Spring Cloud Function and Spring Cloud Function based projects (e.g. Spring Cloud Stream). +|spring.sleuth.grpc.enabled | `true` | Enable span information propagation when using GRPC. +|spring.sleuth.http.enabled | `true` | Enables HTTP support. +|spring.sleuth.integration.enabled | `true` | Enable Spring Integration instrumentation. +|spring.sleuth.integration.patterns | `[!hystrixStreamOutput*, *, !channel*]` | An array of patterns against which channel names will be matched. @see org.springframework.integration.config.GlobalChannelInterceptor#patterns() Defaults to any channel name not matching the Hystrix Stream and functional Stream channel names. +|spring.sleuth.integration.websockets.enabled | `true` | Enable tracing for WebSockets. +|spring.sleuth.jdbc.datasource-proxy.count-query | `false` | Creates listener to count queries. @see ProxyDataSourceBuilder#countQuery() @see QueryCountHolder +|spring.sleuth.jdbc.datasource-proxy.json-format | `false` | Use json output for logging query. @see ProxyDataSourceBuilder#asJson() +|spring.sleuth.jdbc.datasource-proxy.logging | | Logging to use for logging queries. +|spring.sleuth.jdbc.datasource-proxy.multiline | `true` | Use multiline output for logging query. @see ProxyDataSourceBuilder#multiline() +|spring.sleuth.jdbc.datasource-proxy.query.enable-logging | `true` | Enable logging all queries to the log. +|spring.sleuth.jdbc.datasource-proxy.query.log-level | `DEBUG` | Severity of query logger. +|spring.sleuth.jdbc.datasource-proxy.query.logger-name | | Name of query logger. +|spring.sleuth.jdbc.datasource-proxy.slow-query.log-level | `WARN` | Severity of slow query logger. +|spring.sleuth.jdbc.datasource-proxy.slow-query.logger-name | | Name of slow query logger. +|spring.sleuth.jdbc.datasource-proxy.slow-query.threshold | `300` | Number of seconds to consider query as slow. +|spring.sleuth.jdbc.enabled | `true` | Enables data source decorating. +|spring.sleuth.jdbc.excluded-beans | | Beans that won't be decorated. +|spring.sleuth.jdbc.includes | | Which types of tracing we would like to include. +|spring.sleuth.jdbc.p6spy.custom-appender-class | | Class file to use (only with logging=custom). The class must implement {@link com.p6spy.engine.spy.appender.FormattedLogger}. +|spring.sleuth.jdbc.p6spy.enable-logging | `true` | Enables logging JDBC events. +|spring.sleuth.jdbc.p6spy.log-file | `spy.log` | Name of log file to use (only with logging=file). +|spring.sleuth.jdbc.p6spy.log-filter.pattern | | Use regex pattern to filter log messages. Only matched messages will be logged. +|spring.sleuth.jdbc.p6spy.log-format | | Custom log format. +|spring.sleuth.jdbc.p6spy.logging | | Logging to use for logging queries. +|spring.sleuth.jdbc.p6spy.multiline | `true` | Enables multiline output. +|spring.sleuth.jdbc.p6spy.tracing.include-parameter-values | `true` | Report the effective sql string (with '?' replaced with real values) to tracing systems.

NOTE this setting does not affect the logging message. +|spring.sleuth.kafka.enabled | `true` | Enable instrumenting of Apache Kafka clients. +|spring.sleuth.messaging.aspect.enabled | `false` | Should {@link MessageMapping} wrapping be enabled. +|spring.sleuth.messaging.enabled | `false` | Should messaging be turned on. +|spring.sleuth.messaging.jms.enabled | `true` | Enable tracing of JMS. +|spring.sleuth.messaging.jms.remote-service-name | `jms` | JMS remote service name. +|spring.sleuth.messaging.kafka.enabled | `true` | Enable tracing of Kafka. +|spring.sleuth.messaging.kafka.mapper.enabled | `true` | Enable DefaultKafkaHeaderMapper tracing for Kafka. +|spring.sleuth.messaging.kafka.remote-service-name | `kafka` | Kafka remote service name. +|spring.sleuth.messaging.kafka.streams.enabled | `false` | Should Kafka Streams be turned on. +|spring.sleuth.messaging.rabbit.enabled | `true` | Enable tracing of RabbitMQ. +|spring.sleuth.messaging.rabbit.remote-service-name | `rabbitmq` | Rabbit remote service name. +|spring.sleuth.mongodb.enabled | `true` | Enable tracing for MongoDb. +|spring.sleuth.opentracing.enabled | `true` | Enables OpenTracing support. +|spring.sleuth.propagation.type | | Tracing context propagation types. +|spring.sleuth.quartz.enabled | `true` | Enable tracing for Quartz. +|spring.sleuth.r2dbc.enabled | `true` | Enable R2dbc instrumentation. +|spring.sleuth.reactor.decorate-on-each | `true` | When true decorates on each operator, will be less performing, but logging will always contain the tracing entries in each operator. When false decorates on last operator, will be more performing, but logging might not always contain the tracing entries. @deprecated use explicit value via {@link SleuthReactorProperties#instrumentationType} +|spring.sleuth.reactor.enabled | `true` | When true enables instrumentation for reactor. +|spring.sleuth.reactor.instrumentation-type | | +|spring.sleuth.redis.enabled | `true` | Enable span information propagation when using Redis. +|spring.sleuth.redis.remote-service-name | `redis` | Service name for the remote Redis endpoint. +|spring.sleuth.rpc.enabled | `true` | Enable tracing of RPC. +|spring.sleuth.rsocket.enabled | `true` | When true enables instrumentation for rsocket. +|spring.sleuth.rxjava.schedulers.hook.enabled | `true` | Enable support for RxJava via RxJavaSchedulersHook. +|spring.sleuth.rxjava.schedulers.ignoredthreads | `[HystrixMetricPoller, ^RxComputation.*$]` | Thread names for which spans will not be sampled. +|spring.sleuth.sampler.probability | | Probability of requests that should be sampled. E.g. 1.0 - 100% requests should be sampled. The precision is whole-numbers only (i.e. there's no support for 0.1% of the traces). +|spring.sleuth.sampler.rate | `10` | A rate per second can be a nice choice for low-traffic endpoints as it allows you surge protection. For example, you may never expect the endpoint to get more than 50 requests per second. If there was a sudden surge of traffic, to 5000 requests per second, you would still end up with 50 traces per second. Conversely, if you had a percentage, like 10%, the same surge would end up with 500 traces per second, possibly overloading your storage. Amazon X-Ray includes a rate-limited sampler (named Reservoir) for this purpose. Brave has taken the same approach via the {@link brave.sampler.RateLimitingSampler}. +|spring.sleuth.sampler.refresh.enabled | `true` | Enable refresh scope for sampler. +|spring.sleuth.scheduled.enabled | `true` | Enable tracing for {@link org.springframework.scheduling.annotation.Scheduled}. +|spring.sleuth.scheduled.skip-pattern | | Pattern for the fully qualified name of a class that should be skipped. +|spring.sleuth.span-filter.additional-span-name-patterns-to-ignore | | Additional list of span names to ignore. Will be appended to {@link #spanNamePatternsToSkip}. +|spring.sleuth.span-filter.enabled | `false` | Will turn on the default Sleuth handler mechanism. Might ignore exporting of certain spans; +|spring.sleuth.span-filter.span-name-patterns-to-skip | `^catalogWatchTaskScheduler$` | List of span names to ignore. They will not be sent to external systems. +|spring.sleuth.supports-join | `true` | True means the tracing system supports sharing a span ID between a client and server. +|spring.sleuth.task.enabled | `true` | Enable Spring Cloud Task instrumentation. +|spring.sleuth.trace-id128 | `false` | When true, generate 128-bit trace IDs instead of 64-bit ones. +|spring.sleuth.tracer.mode | | Set which tracer implementation should be picked. +|spring.sleuth.tx.enabled | `true` | Enable Spring TX instrumentation. +|spring.sleuth.vault.enabled | `true` | Enable Spring Vault instrumentation. +|spring.sleuth.web.additional-skip-pattern | | Additional pattern for URLs that should be skipped in tracing. This will be appended to the {@link SleuthWebProperties#skipPattern}. +|spring.sleuth.web.client.enabled | `true` | Enable interceptor injecting into {@link org.springframework.web.client.RestTemplate}. +|spring.sleuth.web.client.skip-pattern | | Pattern for URLs that should be skipped in client side tracing. +|spring.sleuth.web.enabled | `true` | When true enables instrumentation for web applications. +|spring.sleuth.web.filter-order | `0` | Order in which the tracing filters should be registered. +|spring.sleuth.web.ignore-auto-configured-skip-patterns | `false` | If set to true, auto-configured skip patterns will be ignored. +|spring.sleuth.web.servlet.enabled | `true` | Enable servlet instrumentation. +|spring.sleuth.web.skip-pattern | `/api-docs.*\|/swagger.*\|.*\.png\|.*\.css\|.*\.js\|.*\.html\|/favicon.ico\|/hystrix.stream` | Pattern for URLs that should be skipped in tracing. +|spring.sleuth.web.tomcat.enabled | `true` | Enable tracing instrumentation for Tomcat. +|spring.sleuth.web.webclient.enabled | `true` | Enable tracing instrumentation for WebClient. +|spring.zipkin.activemq.message-max-bytes | `100000` | Maximum number of bytes for a given message with spans sent to Zipkin over ActiveMQ. +|spring.zipkin.activemq.queue | `zipkin` | Name of the ActiveMQ queue where spans should be sent to Zipkin. +|spring.zipkin.api-path | | The API path to append to baseUrl (above) as suffix. This applies if you use other monitoring tools, such as New Relic. The trace API doesn't need the API path, so you can set it to blank ("") in the configuration. +|spring.zipkin.base-url | `http://localhost:9411/` | URL of the zipkin query server instance. You can also provide the service id of the Zipkin server if Zipkin's registered in service discovery (e.g. https://zipkinserver/). +|spring.zipkin.compression.enabled | `false` | +|spring.zipkin.discovery-client-enabled | | If set to {@code false}, will treat the {@link ZipkinProperties#baseUrl} as a URL always. +|spring.zipkin.enabled | `true` | Enables sending spans to Zipkin. +|spring.zipkin.encoder | | Encoding type of spans sent to Zipkin. Set to {@link SpanBytesEncoder#JSON_V1} if your server is not recent. +|spring.zipkin.kafka.topic | `zipkin` | Name of the Kafka topic where spans should be sent to Zipkin. +|spring.zipkin.locator.discovery.enabled | `false` | Enabling of locating the host name via service discovery. +|spring.zipkin.message-timeout | `1` | Timeout in seconds before pending spans will be sent in batches to Zipkin. +|spring.zipkin.rabbitmq.addresses | | Addresses of the RabbitMQ brokers used to send spans to Zipkin +|spring.zipkin.rabbitmq.queue | `zipkin` | Name of the RabbitMQ queue where spans should be sent to Zipkin. +|spring.zipkin.sender.type | | Means of sending spans to Zipkin. +|spring.zipkin.service.name | | The name of the service, from which the Span was sent via HTTP, that should appear in Zipkin. + +|=== diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 719002b3a..e57563d3a 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -695,8 +695,8 @@ runtimeOnly "net.ttddyy:datasource-proxy:${datasourceProxyVersion}" ---- ==== -Please check the <> page under `spring.sleuth.jdbc.decorator.datasource.p6spy` for all p6spy configuration options and `spring.sleuth.jdbc.decorator.datasource.datasource-proxy` for all datasource proxy configuration options. +Please check the <> page under `spring.sleuth.jdbc.p6spy` for all p6spy configuration options and `spring.sleuth.jdbc.datasource-proxy` for all datasource proxy configuration options. You can configure P6Spy manually using one of available configuration methods. For more information please refer to the http://p6spy.readthedocs.io/en/latest/configandusage.html[P6Spy Configuration Guide]. -In order to disable this instrumentation set `spring.sleuth.jdbc.decorator.datasource.enabled` to `false`. +In order to disable this instrumentation set `spring.sleuth.jdbc.enabled` to `false`. diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfiguration.java index 0fa6d75f0..72feab40a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfiguration.java @@ -1,151 +1,153 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.async; - -import java.util.concurrent.Executor; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.aop.interceptor.AsyncExecutionAspectSupport; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.NoUniqueBeanDefinitionException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.sleuth.SpanNamer; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.async.LazyTraceExecutor; -import org.springframework.cloud.sleuth.instrument.async.TraceAsyncAspect; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Role; -import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.core.task.TaskExecutor; -import org.springframework.scheduling.annotation.AsyncConfigurer; -import org.springframework.scheduling.annotation.AsyncConfigurerSupport; - -/** - * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration - * Auto-configuration} enabling async related processing. - * - * @author Dave Syer - * @author Marcin Grzejszczak - * @since 1.0.0 - * @see LazyTraceExecutor - * @see TraceAsyncAspect - */ -@Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(SleuthAsyncProperties.class) -@ConditionalOnProperty(value = "spring.sleuth.async.enabled", matchIfMissing = true) -@ConditionalOnBean(Tracer.class) -@AutoConfigureAfter(BraveAutoConfiguration.class) -public class TraceAsyncDefaultAutoConfiguration { - - @Bean - @ConditionalOnProperty(value = "spring.sleuth.scheduled.enabled", matchIfMissing = true) - static ExecutorBeanPostProcessor executorBeanPostProcessor(BeanFactory beanFactory) { - return new ExecutorBeanPostProcessor(beanFactory); - } - - @Bean - TraceAsyncAspect traceAsyncAspect(Tracer tracer, SpanNamer spanNamer) { - return new TraceAsyncAspect(tracer, spanNamer); - } - - /** - * Wrapper for the async executor. - */ - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(AsyncConfigurer.class) - @ConditionalOnMissingBean(AsyncConfigurer.class) - @ConditionalOnProperty(value = "spring.sleuth.async.configurer.enabled", matchIfMissing = true) - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static class DefaultAsyncConfigurerSupport extends AsyncConfigurerSupport { - - private static final Log log = LogFactory.getLog(DefaultAsyncConfigurerSupport.class); - - @Autowired - private BeanFactory beanFactory; - - @Override - public Executor getAsyncExecutor() { - Executor delegate = getDefaultExecutor(); - return new LazyTraceExecutor(this.beanFactory, delegate); - } - - /** - * Retrieve or build a default executor for this advice instance. An executor - * returned from here will be cached for further use. - *

- * The default implementation searches for a unique {@link TaskExecutor} bean in - * the context, or for an {@link Executor} bean named "taskExecutor" otherwise. If - * neither of the two is resolvable, this implementation will return {@code null}. - * @return the default executor, or {@code null} if none available - * @see AsyncExecutionAspectSupport#getDefaultExecutor(org.springframework.beans.factory.BeanFactory) - */ - private Executor getDefaultExecutor() { - try { - // Search for TaskExecutor bean... not plain Executor since that would - // match with ScheduledExecutorService as well, which is unusable for - // our purposes here. TaskExecutor is more clearly designed for it. - return this.beanFactory.getBean(TaskExecutor.class); - } - catch (NoUniqueBeanDefinitionException ex) { - log.debug("Could not find unique TaskExecutor bean", ex); - try { - return this.beanFactory.getBean(AsyncExecutionAspectSupport.DEFAULT_TASK_EXECUTOR_BEAN_NAME, - Executor.class); - } - catch (NoSuchBeanDefinitionException ex2) { - if (log.isInfoEnabled()) { - log.info("More than one TaskExecutor bean found within the context, and none is named " - + "'taskExecutor'. Mark one of them as primary or name it 'taskExecutor' (possibly " - + "as an alias) in order to use it for async processing: " + ex.getBeanNamesFound()); - } - } - } - catch (NoSuchBeanDefinitionException ex) { - log.debug("Could not find default TaskExecutor bean", ex); - try { - return this.beanFactory.getBean(AsyncExecutionAspectSupport.DEFAULT_TASK_EXECUTOR_BEAN_NAME, - Executor.class); - } - catch (NoSuchBeanDefinitionException ex2) { - log.info("No task executor bean found for async processing: " - + "no bean of type TaskExecutor and no bean named 'taskExecutor' either"); - } - // Giving up -> either using local default executor or none at all... - } - // backward compatibility - if (log.isInfoEnabled()) { - log.info( - "For backward compatibility, will fallback to the default, SimpleAsyncTaskExecutor implementation"); - } - return new SimpleAsyncTaskExecutor(); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.async; + +import java.util.concurrent.Executor; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.interceptor.AsyncExecutionAspectSupport; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.sleuth.SpanNamer; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.async.LazyTraceExecutor; +import org.springframework.cloud.sleuth.instrument.async.TraceAsyncAspect; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.AsyncConfigurerSupport; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} enabling async related processing. + * + * @author Dave Syer + * @author Marcin Grzejszczak + * @since 1.0.0 + * @see LazyTraceExecutor + * @see TraceAsyncAspect + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(SleuthAsyncProperties.class) +@ConditionalOnProperty(value = "spring.sleuth.async.enabled", matchIfMissing = true) +@ConditionalOnBean(Tracer.class) +@AutoConfigureAfter(BraveAutoConfiguration.class) +public class TraceAsyncDefaultAutoConfiguration { + + @Bean + @ConditionalOnProperty(value = "spring.sleuth.scheduled.enabled", matchIfMissing = true) + static ExecutorBeanPostProcessor executorBeanPostProcessor(BeanFactory beanFactory) { + return new ExecutorBeanPostProcessor(beanFactory); + } + + @Bean + TraceAsyncAspect traceAsyncAspect(Tracer tracer, SpanNamer spanNamer) { + return new TraceAsyncAspect(tracer, spanNamer); + } + + /** + * Wrapper for the async executor. + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(AsyncConfigurer.class) + @ConditionalOnMissingBean(AsyncConfigurer.class) + @ConditionalOnProperty(value = "spring.sleuth.async.configurer.enabled", matchIfMissing = true) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static class DefaultAsyncConfigurerSupport extends AsyncConfigurerSupport { + + private static final Log log = LogFactory.getLog(DefaultAsyncConfigurerSupport.class); + + @Autowired + private BeanFactory beanFactory; + + @Override + public Executor getAsyncExecutor() { + Executor delegate = getDefaultExecutor(); + return new LazyTraceExecutor(this.beanFactory, delegate); + } + + /** + * Retrieve or build a default executor for this advice instance. An executor + * returned from here will be cached for further use. + *

+ * The default implementation searches for a unique {@link TaskExecutor} bean in + * the context, or for an {@link Executor} bean named "taskExecutor" otherwise. If + * neither of the two is resolvable, this implementation will return {@code null}. + * @return the default executor, or {@code null} if none available + * @see AsyncExecutionAspectSupport#getDefaultExecutor(org.springframework.beans.factory.BeanFactory) + */ + private Executor getDefaultExecutor() { + try { + // Search for TaskExecutor bean... not plain Executor since that would + // match with ScheduledExecutorService as well, which is unusable for + // our purposes here. TaskExecutor is more clearly designed for it. + return this.beanFactory.getBean(TaskExecutor.class); + } + catch (NoUniqueBeanDefinitionException ex) { + if (log.isDebugEnabled()) { + log.debug("Could not find unique TaskExecutor bean", ex); + } + try { + return this.beanFactory.getBean(AsyncExecutionAspectSupport.DEFAULT_TASK_EXECUTOR_BEAN_NAME, + Executor.class); + } + catch (NoSuchBeanDefinitionException ex2) { + if (log.isInfoEnabled()) { + log.info("More than one TaskExecutor bean found within the context, and none is named " + + "'taskExecutor'. Mark one of them as primary or name it 'taskExecutor' (possibly " + + "as an alias) in order to use it for async processing: " + ex.getBeanNamesFound()); + } + } + } + catch (NoSuchBeanDefinitionException ex) { + log.debug("Could not find default TaskExecutor bean", ex); + try { + return this.beanFactory.getBean(AsyncExecutionAspectSupport.DEFAULT_TASK_EXECUTOR_BEAN_NAME, + Executor.class); + } + catch (NoSuchBeanDefinitionException ex2) { + log.info("No task executor bean found for async processing: " + + "no bean of type TaskExecutor and no bean named 'taskExecutor' either"); + } + // Giving up -> either using local default executor or none at all... + } + // backward compatibility + if (log.isInfoEnabled()) { + log.info( + "For backward compatibility, will fallback to the default, SimpleAsyncTaskExecutor implementation"); + } + return new SimpleAsyncTaskExecutor(); + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java index fb0daac9b..9775cb354 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java @@ -97,8 +97,7 @@ class DataSourceProxyConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(value = "spring.sleuth.jdbc.decorator.datasource.datasource-proxy.count-query", - havingValue = "true") + @ConditionalOnProperty(value = "spring.sleuth.jdbc.datasource-proxy.count-query", havingValue = "true") QueryCountStrategy queryCountStrategy() { return new SingleQueryCountHolder(); } diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java index dc97a2740..c0227ac10 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java @@ -66,8 +66,8 @@ class P6SpyPropertiesSetter implements BeanDefinitionRegistryPostProcessor, Clos public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { ConfigurableEnvironment environment = this.context.getEnvironment(); String customModuleList = initialP6SpyOptions.get("modulelist"); - boolean isEnableLogging = environment - .getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.enable-logging", Boolean.class, true); + boolean isEnableLogging = environment.getProperty("spring.sleuth.jdbc.p6spy.enable-logging", Boolean.class, + true); if (customModuleList != null) { log.info("P6Spy modulelist is overridden, some p6spy configuration features will not be applied"); } @@ -94,7 +94,7 @@ class P6SpyPropertiesSetter implements BeanDefinitionRegistryPostProcessor, Clos if (isEnableLogging && !initialP6SpyOptions.containsKey("appender")) { TraceDataSourceDecoratorProperties.P6SpyProperties.P6SpyLogging logging = TraceDataSourceDecoratorProperties.P6SpyProperties.P6SpyLogging .valueOf(environment - .getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.logging", String.class, + .getProperty("spring.sleuth.jdbc.p6spy.logging", String.class, TraceDataSourceDecoratorProperties.P6SpyProperties.P6SpyLogging.SLF4J.toString()) .toUpperCase()); switch (logging) { @@ -108,19 +108,17 @@ class P6SpyPropertiesSetter implements BeanDefinitionRegistryPostProcessor, Clos System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.FileLogger"); break; case CUSTOM: - String customAppender = environment.getProperty( - "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class", String.class, ""); + String customAppender = environment.getProperty("spring.sleuth.jdbc.p6spy.custom-appender-class", + String.class, ""); System.setProperty("p6spy.config.appender", customAppender); break; } } if (!initialP6SpyOptions.containsKey("logfile")) { - String logFile = environment.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.log-file", - String.class, "spy.log"); + String logFile = environment.getProperty("spring.sleuth.jdbc.p6spy.log-file", String.class, "spy.log"); System.setProperty("p6spy.config.logfile", logFile); } - String pattern = environment.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern", - String.class); + String pattern = environment.getProperty("spring.sleuth.jdbc.p6spy.log-filter.pattern", String.class); if (pattern != null) { System.setProperty("p6spy.config.filter", "true"); System.setProperty("p6spy.config.sqlexpression", pattern); @@ -133,11 +131,11 @@ class P6SpyPropertiesSetter implements BeanDefinitionRegistryPostProcessor, Clos } private Boolean multiLine(ConfigurableEnvironment environment) { - return environment.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.multiline", Boolean.class, true); + return environment.getProperty("spring.sleuth.jdbc.p6spy.multiline", Boolean.class, true); } private String logFormat(ConfigurableEnvironment environment) { - return environment.getProperty("spring.sleuth.jdbc.decorator.datasource.p6spy.log-format", String.class); + return environment.getProperty("spring.sleuth.jdbc.p6spy.log-format", String.class); } @Override diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorAutoConfiguration.java index 2066149c7..0c0c46f9e 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorAutoConfiguration.java @@ -42,8 +42,7 @@ import org.springframework.context.annotation.Import; */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(TraceDataSourceDecoratorProperties.class) -@ConditionalOnProperty(name = "spring.sleuth.jdbc.decorator.datasource.enabled", havingValue = "true", - matchIfMissing = true) +@ConditionalOnProperty(name = "spring.sleuth.jdbc.enabled", havingValue = "true", matchIfMissing = true) @ConditionalOnBean({ DataSource.class, Tracer.class }) @AutoConfigureAfter(DataSourceAutoConfiguration.class) @Import({ P6SpyConfiguration.class, DataSourceProxyConfiguration.class }) diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorProperties.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorProperties.java index 2e655e903..9e71c9013 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorProperties.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorProperties.java @@ -22,7 +22,6 @@ import java.util.Collections; import java.util.List; import java.util.regex.Pattern; -import net.ttddyy.dsproxy.QueryCountHolder; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -34,7 +33,7 @@ import org.springframework.cloud.sleuth.instrument.jdbc.TraceType; * @author Arthur Gavlyukovskiy * @since 3.1.0 */ -@ConfigurationProperties(prefix = "spring.sleuth.jdbc.decorator.datasource") +@ConfigurationProperties(prefix = "spring.sleuth.jdbc") public class TraceDataSourceDecoratorProperties { /** @@ -130,14 +129,6 @@ public class TraceDataSourceDecoratorProperties { */ private boolean jsonFormat = false; - /** - * Creates listener to count queries. - * - * @see ProxyDataSourceBuilder#countQuery() - * @see QueryCountHolder - */ - private boolean countQuery = false; - public DataSourceProxyLogging getLogging() { return logging; } @@ -178,14 +169,6 @@ public class TraceDataSourceDecoratorProperties { this.jsonFormat = jsonFormat; } - public boolean isCountQuery() { - return countQuery; - } - - public void setCountQuery(boolean countQuery) { - this.countQuery = countQuery; - } - /** * Properties to configure query logging listener. */ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java index eef976520..5da06cf57 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java @@ -1,205 +1,206 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.brave.instrument.web.client; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Future; -import java.util.stream.Collectors; - -import brave.Span; -import brave.Tracer; -import brave.baggage.BaggagePropagation; -import brave.handler.SpanHandler; -import brave.propagation.B3Propagation; -import brave.sampler.Sampler; -import brave.test.TestSpanHandler; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.concurrent.FutureCallback; -import org.apache.http.impl.client.BasicResponseHandler; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; -import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; -import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; -import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; -import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.web.server.LocalServerPort; -import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; -import org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration; -import org.springframework.cloud.sleuth.DisableSecurity; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import static brave.Span.Kind.CLIENT; -import static brave.propagation.B3Propagation.Format.SINGLE_NO_PARENT; -import static org.assertj.core.api.BDDAssertions.then; - -@SpringBootTest(classes = WebClientTests.TestConfiguration.class, - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, - properties = {"spring.sleuth.web.servlet.enabled=false", "spring.application.name=fooservice", - "spring.sleuth.web.client.skip-pattern=/skip.*"}) -@DirtiesContext -public class WebClientTests { - - @Autowired - HttpClientBuilder httpClientBuilder; // #845 - - @Autowired - HttpAsyncClientBuilder httpAsyncClientBuilder; // #845 - - @Autowired - TestSpanHandler spans; - - @Autowired - Tracer tracer; - - @LocalServerPort - int port; - - @Autowired - FooController fooController; - - @AfterEach - @BeforeEach - public void close() { - this.spans.clear(); - this.fooController.clear(); - } - - @Test - @SuppressWarnings("unchecked") - public void shouldAttachTraceIdWhenCallingAnotherServiceForHttpClient() throws Exception { - then(this.spans).isEmpty(); - Span span = this.tracer.nextSpan().name("foo").start(); - - try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { - String response = this.httpClientBuilder.build().execute(new HttpGet("http://localhost:" + this.port), - new BasicResponseHandler()); - - then(response).isNotEmpty(); - } - - then(this.tracer.currentSpan()).isNull(); - then(this.spans).isNotEmpty().extracting("traceId", String.class).containsOnly(span.context().traceIdString()); - then(this.spans.spans().stream().map(s -> s.kind().name()).collect(Collectors.toList())).contains("CLIENT"); - } - - @Test - @SuppressWarnings("unchecked") - public void shouldAttachTraceIdWhenCallingAnotherServiceForAsyncHttpClient() throws Exception { - Span span = this.tracer.nextSpan().name("foo").start(); - - CloseableHttpAsyncClient client = this.httpAsyncClientBuilder.build(); - try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { - client.start(); - Future future = client.execute(new HttpGet("http://localhost:" + this.port), - new FutureCallback() { - @Override - public void completed(HttpResponse result) { - - } - - @Override - public void failed(Exception ex) { - - } - - @Override - public void cancelled() { - - } - }); - then(future.get()).isNotNull(); - } - finally { - client.close(); - } - - then(this.tracer.currentSpan()).isNull(); - then(this.spans).isNotEmpty().extracting("traceId", String.class).containsOnly(span.context().traceIdString()); - then(this.spans.spans().stream().map(s -> s.kind().name()).collect(Collectors.toList())).contains("CLIENT"); - } - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration(exclude = {GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, - R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}) - @DisableSecurity - public static class TestConfiguration { - - @Bean - BaggagePropagation.FactoryBuilder baggagePropagationFactoryBuilder() { - // Use b3 single format as it is less verbose - return BaggagePropagation.newFactoryBuilder( - B3Propagation.newFactoryBuilder().injectFormat(CLIENT, SINGLE_NO_PARENT).build()); - } - - @Bean - FooController fooController() { - return new FooController(); - } - - @Bean - Sampler testSampler() { - return Sampler.ALWAYS_SAMPLE; - } - - @Bean - SpanHandler testSpanHandler() { - return new TestSpanHandler(); - } - - } - - @RestController - public static class FooController { - - Span span; - - @RequestMapping("/") - public Map home(@RequestHeader HttpHeaders headers) { - Map map = new HashMap<>(); - for (String key : headers.keySet()) { - map.put(key, headers.getFirst(key)); - } - return map; - } - - public Span getSpan() { - return this.span; - } - - public void clear() { - this.span = null; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.brave.instrument.web.client; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Future; +import java.util.stream.Collectors; + +import brave.Span; +import brave.Tracer; +import brave.baggage.BaggagePropagation; +import brave.handler.SpanHandler; +import brave.propagation.B3Propagation; +import brave.sampler.Sampler; +import brave.test.TestSpanHandler; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; +import org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration; +import org.springframework.cloud.sleuth.DisableSecurity; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static brave.Span.Kind.CLIENT; +import static brave.propagation.B3Propagation.Format.SINGLE_NO_PARENT; +import static org.assertj.core.api.BDDAssertions.then; + +@SpringBootTest(classes = WebClientTests.TestConfiguration.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { "spring.sleuth.web.servlet.enabled=false", "spring.application.name=fooservice", + "spring.sleuth.web.client.skip-pattern=/skip.*" }) +@DirtiesContext +public class WebClientTests { + + @Autowired + HttpClientBuilder httpClientBuilder; // #845 + + @Autowired + HttpAsyncClientBuilder httpAsyncClientBuilder; // #845 + + @Autowired + TestSpanHandler spans; + + @Autowired + Tracer tracer; + + @LocalServerPort + int port; + + @Autowired + FooController fooController; + + @AfterEach + @BeforeEach + public void close() { + this.spans.clear(); + this.fooController.clear(); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldAttachTraceIdWhenCallingAnotherServiceForHttpClient() throws Exception { + then(this.spans).isEmpty(); + Span span = this.tracer.nextSpan().name("foo").start(); + + try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { + String response = this.httpClientBuilder.build().execute(new HttpGet("http://localhost:" + this.port), + new BasicResponseHandler()); + + then(response).isNotEmpty(); + } + + then(this.tracer.currentSpan()).isNull(); + then(this.spans).isNotEmpty().extracting("traceId", String.class).containsOnly(span.context().traceIdString()); + then(this.spans.spans().stream().map(s -> s.kind().name()).collect(Collectors.toList())).contains("CLIENT"); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldAttachTraceIdWhenCallingAnotherServiceForAsyncHttpClient() throws Exception { + Span span = this.tracer.nextSpan().name("foo").start(); + + CloseableHttpAsyncClient client = this.httpAsyncClientBuilder.build(); + try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { + client.start(); + Future future = client.execute(new HttpGet("http://localhost:" + this.port), + new FutureCallback() { + @Override + public void completed(HttpResponse result) { + + } + + @Override + public void failed(Exception ex) { + + } + + @Override + public void cancelled() { + + } + }); + then(future.get()).isNotNull(); + } + finally { + client.close(); + } + + then(this.tracer.currentSpan()).isNull(); + then(this.spans).isNotEmpty().extracting("traceId", String.class).containsOnly(span.context().traceIdString()); + then(this.spans.spans().stream().map(s -> s.kind().name()).collect(Collectors.toList())).contains("CLIENT"); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, + R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, MongoAutoConfiguration.class, + MongoDataAutoConfiguration.class }) + @DisableSecurity + public static class TestConfiguration { + + @Bean + BaggagePropagation.FactoryBuilder baggagePropagationFactoryBuilder() { + // Use b3 single format as it is less verbose + return BaggagePropagation.newFactoryBuilder( + B3Propagation.newFactoryBuilder().injectFormat(CLIENT, SINGLE_NO_PARENT).build()); + } + + @Bean + FooController fooController() { + return new FooController(); + } + + @Bean + Sampler testSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + SpanHandler testSpanHandler() { + return new TestSpanHandler(); + } + + } + + @RestController + public static class FooController { + + Span span; + + @RequestMapping("/") + public Map home(@RequestHeader HttpHeaders headers) { + Map map = new HashMap<>(); + for (String key : headers.keySet()) { + map.put(key, headers.getFirst(key)); + } + return map; + } + + public Span getSpan() { + return this.span; + } + + public void clear() { + this.span = null; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java index 734b4f514..b8c38ee16 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java @@ -75,7 +75,7 @@ class DataSourceDecoratorAutoConfigurationTests { @Test void testNoDecoratingForExcludeBeans() { ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.excluded-beans:dataSource"); + .withPropertyValues("spring.sleuth.jdbc.excluded-beans:dataSource"); contextRunner.run(context -> { DataSource dataSource = context.getBean(DataSource.class); @@ -183,7 +183,7 @@ class DataSourceDecoratorAutoConfigurationTests { @Test void testDecoratingCanBeDisabled() { ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.enabled:false"); + .withPropertyValues("spring.sleuth.jdbc.enabled:false"); contextRunner.run(context -> { DataSource dataSource = context.getBean(DataSource.class); @@ -194,7 +194,7 @@ class DataSourceDecoratorAutoConfigurationTests { @Test void testDecoratingCanBeDisabledForSpecificBeans() { ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.excluded-beans:secondDataSource") + .withPropertyValues("spring.sleuth.jdbc.excluded-beans:secondDataSource") .withUserConfiguration(TestMultiDataSourceConfiguration.class); contextRunner.run(context -> { diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java index c626017e4..cb45faada 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java @@ -1,281 +1,280 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import com.p6spy.engine.common.ConnectionInformation; -import com.p6spy.engine.common.P6LogQuery; -import com.p6spy.engine.event.CompoundJdbcEventListener; -import com.p6spy.engine.event.JdbcEventListener; -import com.p6spy.engine.logging.Category; -import com.p6spy.engine.logging.LoggingEventListener; -import com.p6spy.engine.spy.JdbcEventListenerFactory; -import com.p6spy.engine.spy.P6DataSource; -import com.p6spy.engine.spy.appender.CustomLineFormat; -import com.p6spy.engine.spy.appender.FormattedLogger; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -class P6SpyConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, - TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) - .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); - - @BeforeEach - @AfterEach - void resetLogAccumulator() { - LogAccumulator.reset(); - } - - @Test - void testCustomListeners() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomListenerConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - GetCountingListener getCountingListener = context.getBean(GetCountingListener.class); - ClosingCountingListener closingCountingListener = context.getBean(ClosingCountingListener.class); - P6DataSource p6DataSource = (P6DataSource) ((DataSourceWrapper) dataSource).getDecoratedDataSource(); - assertThat(p6DataSource).extracting("jdbcEventListenerFactory").isEqualTo(jdbcEventListenerFactory); - - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).contains(getCountingListener, closingCountingListener); - assertThat(getCountingListener.connectionCount).isEqualTo(0); - - Connection connection1 = p6DataSource.getConnection(); - - assertThat(getCountingListener.connectionCount).isEqualTo(1); - assertThat(closingCountingListener.connectionCount).isEqualTo(0); - - Connection connection2 = p6DataSource.getConnection(); - - assertThat(getCountingListener.connectionCount).isEqualTo(2); - - // order matters! - connection2.close(); - - assertThat(closingCountingListener.connectionCount).isEqualTo(1); - - // order matters! - connection1.close(); - - assertThat(closingCountingListener.connectionCount).isEqualTo(2); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testDoesNotRegisterLoggingListenerIfDisabled() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.enable-logging:false"); - - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).extracting("class") - .doesNotContain(LoggingEventListener.class); - }); - } - - @Test - void testCanSetCustomLoggingFormat() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.log-format:test %{connectionId}"); - - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); - assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") - .isEqualTo(CustomLineFormat.class); - }); - } - - @Test - void testMultilineShouldNotOverrideCustomProperties() { - System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); - System.setProperty("p6spy.config.excludecategories", "debug"); - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.p6spy.multiline:true"); - - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); - assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") - .isEqualTo(CustomLineFormat.class); - }); - } - - @Test - void testUseCustomLogger() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom", - "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" - + LogAccumulator.class.getName()); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - dataSource.getConnection().close(); - - assertThat(P6LogQuery.getLogger()).isInstanceOf(LogAccumulator.class); - }); - } - - @Test - void testLogFilterPattern() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom", - "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), - "spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern:.*table1.*"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - try (Connection connection = dataSource.getConnection(); - PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); - PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { - ps1.execute(); - ps2.execute(); - } - - assertThat(LogAccumulator.MESSAGES).hasSize(1); - assertThat(LogAccumulator.MESSAGES).allMatch(message -> message.contains("table1")); - }); - } - - @Test - void testLogFilterPatternMatchAll() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.sleuth.jdbc.decorator.datasource.p6spy.logging:custom", - "spring.sleuth.jdbc.decorator.datasource.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), - "spring.sleuth.jdbc.decorator.datasource.p6spy.log-filter.pattern:.*"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - try (Connection connection = dataSource.getConnection(); - PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); - PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { - ps1.execute(); - ps2.execute(); - } - - assertThat(LogAccumulator.MESSAGES).hasSize(2); - }); - } - - @Configuration - static class CustomListenerConfiguration { - - @Bean - public GetCountingListener wrappingCountingListener() { - return new GetCountingListener(); - } - - @Bean - public ClosingCountingListener closingCountingListener() { - return new ClosingCountingListener(); - } - - } - - static class GetCountingListener extends JdbcEventListener { - - int connectionCount = 0; - - @Override - public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) { - connectionCount++; - } - - } - - static class ClosingCountingListener extends JdbcEventListener { - - int connectionCount = 0; - - @Override - public void onAfterConnectionClose(ConnectionInformation connectionInformation, SQLException e) { - connectionCount++; - } - - } - - public static class LogAccumulator extends FormattedLogger { - - static final List MESSAGES = new ArrayList<>(); - static final List EXCEPTIONS = new ArrayList<>(); - - public static void reset() { - MESSAGES.clear(); - EXCEPTIONS.clear(); - } - - @Override - public void logException(Exception e) { - EXCEPTIONS.add(e); - } - - @Override - public void logText(String text) { - MESSAGES.add(text); - } - - @Override - public boolean isCategoryEnabled(Category category) { - return true; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import com.p6spy.engine.common.ConnectionInformation; +import com.p6spy.engine.common.P6LogQuery; +import com.p6spy.engine.event.CompoundJdbcEventListener; +import com.p6spy.engine.event.JdbcEventListener; +import com.p6spy.engine.logging.Category; +import com.p6spy.engine.logging.LoggingEventListener; +import com.p6spy.engine.spy.JdbcEventListenerFactory; +import com.p6spy.engine.spy.P6DataSource; +import com.p6spy.engine.spy.appender.CustomLineFormat; +import com.p6spy.engine.spy.appender.FormattedLogger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +class P6SpyConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, + TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); + + @BeforeEach + @AfterEach + void resetLogAccumulator() { + LogAccumulator.reset(); + } + + @Test + void testCustomListeners() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomListenerConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + GetCountingListener getCountingListener = context.getBean(GetCountingListener.class); + ClosingCountingListener closingCountingListener = context.getBean(ClosingCountingListener.class); + P6DataSource p6DataSource = (P6DataSource) ((DataSourceWrapper) dataSource).getDecoratedDataSource(); + assertThat(p6DataSource).extracting("jdbcEventListenerFactory").isEqualTo(jdbcEventListenerFactory); + + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).contains(getCountingListener, closingCountingListener); + assertThat(getCountingListener.connectionCount).isEqualTo(0); + + Connection connection1 = p6DataSource.getConnection(); + + assertThat(getCountingListener.connectionCount).isEqualTo(1); + assertThat(closingCountingListener.connectionCount).isEqualTo(0); + + Connection connection2 = p6DataSource.getConnection(); + + assertThat(getCountingListener.connectionCount).isEqualTo(2); + + // order matters! + connection2.close(); + + assertThat(closingCountingListener.connectionCount).isEqualTo(1); + + // order matters! + connection1.close(); + + assertThat(closingCountingListener.connectionCount).isEqualTo(2); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testDoesNotRegisterLoggingListenerIfDisabled() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.p6spy.enable-logging:false"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class") + .doesNotContain(LoggingEventListener.class); + }); + } + + @Test + void testCanSetCustomLoggingFormat() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.p6spy.log-format:test %{connectionId}"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); + assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") + .isEqualTo(CustomLineFormat.class); + }); + } + + @Test + void testMultilineShouldNotOverrideCustomProperties() { + System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); + System.setProperty("p6spy.config.excludecategories", "debug"); + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.p6spy.multiline:true"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); + assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") + .isEqualTo(CustomLineFormat.class); + }); + } + + @Test + void testUseCustomLogger() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.p6spy.logging:custom", + "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName()); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + dataSource.getConnection().close(); + + assertThat(P6LogQuery.getLogger()).isInstanceOf(LogAccumulator.class); + }); + } + + @Test + void testLogFilterPattern() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.p6spy.logging:custom", + "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), + "spring.sleuth.jdbc.p6spy.log-filter.pattern:.*table1.*"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); + PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { + ps1.execute(); + ps2.execute(); + } + + assertThat(LogAccumulator.MESSAGES).hasSize(1); + assertThat(LogAccumulator.MESSAGES).allMatch(message -> message.contains("table1")); + }); + } + + @Test + void testLogFilterPatternMatchAll() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.p6spy.logging:custom", + "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), + "spring.sleuth.jdbc.p6spy.log-filter.pattern:.*"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); + PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { + ps1.execute(); + ps2.execute(); + } + + assertThat(LogAccumulator.MESSAGES).hasSize(2); + }); + } + + @Configuration + static class CustomListenerConfiguration { + + @Bean + public GetCountingListener wrappingCountingListener() { + return new GetCountingListener(); + } + + @Bean + public ClosingCountingListener closingCountingListener() { + return new ClosingCountingListener(); + } + + } + + static class GetCountingListener extends JdbcEventListener { + + int connectionCount = 0; + + @Override + public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) { + connectionCount++; + } + + } + + static class ClosingCountingListener extends JdbcEventListener { + + int connectionCount = 0; + + @Override + public void onAfterConnectionClose(ConnectionInformation connectionInformation, SQLException e) { + connectionCount++; + } + + } + + public static class LogAccumulator extends FormattedLogger { + + static final List MESSAGES = new ArrayList<>(); + static final List EXCEPTIONS = new ArrayList<>(); + + public static void reset() { + MESSAGES.clear(); + EXCEPTIONS.clear(); + } + + @Override + public void logException(Exception e) { + EXCEPTIONS.add(e); + } + + @Override + public void logText(String text) { + MESSAGES.add(text); + } + + @Override + public boolean isCategoryEnabled(Category category) { + return true; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java index 898a0de7a..ca3528afb 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java @@ -1,246 +1,246 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import net.ttddyy.dsproxy.ExecutionInfo; -import net.ttddyy.dsproxy.QueryInfo; -import net.ttddyy.dsproxy.listener.ChainListener; -import net.ttddyy.dsproxy.listener.QueryExecutionListener; -import net.ttddyy.dsproxy.listener.logging.CommonsQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.CommonsSlowQueryListener; -import net.ttddyy.dsproxy.listener.logging.JULQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.JULSlowQueryListener; -import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.SLF4JSlowQueryListener; -import net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.SystemOutSlowQueryListener; -import net.ttddyy.dsproxy.proxy.DefaultConnectionIdManager; -import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; -import net.ttddyy.dsproxy.support.ProxyDataSource; -import net.ttddyy.dsproxy.transform.ParameterTransformer; -import net.ttddyy.dsproxy.transform.QueryTransformer; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; - -import static org.assertj.core.api.Assertions.assertThat; - -class ProxyDataSourceConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, - TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) - .withClassLoader(new FilteredClassLoader("com.p6spy")); - - @Test - void testRegisterLogAndSlowQueryLogByDefaultToSlf4j() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogByUsingSlf4j() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasource-proxy.logging:slf4j"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogUsingSystemOut() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasource-proxy.logging:sysout"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutQueryLoggingListener.class); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogUsingJUL() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasourceProxy.logging:jul"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(JULSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(JULQueryLoggingListener.class); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogUsingApacheCommons() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.decorator.datasource.datasourceProxy.logging:commons"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(CommonsSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(CommonsQueryLoggingListener.class); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testCustomParameterAndQueryTransformer() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomDataSourceProxyConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ParameterTransformer parameterTransformer = context.getBean(ParameterTransformer.class); - QueryTransformer queryTransformer = context.getBean(QueryTransformer.class); - assertThat(proxyDataSource.getProxyConfig().getParameterTransformer()).isSameAs(parameterTransformer); - assertThat(proxyDataSource.getProxyConfig().getQueryTransformer()).isSameAs(queryTransformer); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testCustomListeners() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomListenerConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - QueryExecutionListener queryExecutionListener = context.getBean(QueryExecutionListener.class); - - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).contains(queryExecutionListener); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testGlobalConnectionIdManagerByDefault() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - - assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(GlobalConnectionIdManager.class); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testCustomConnectionIdManager() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomDataSourceProxyConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - - assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(DefaultConnectionIdManager.class); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Configuration - static class CustomDataSourceProxyConfiguration { - - @Bean - public ParameterTransformer parameterTransformer() { - return (replacer, transformInfo) -> { - }; - } - - @Bean - public QueryTransformer queryTransformer() { - return (transformInfo) -> "TestQuery"; - } - - @Bean - public DataSourceProxyConnectionIdManagerProvider connectionIdManagerProvider() { - return DefaultConnectionIdManager::new; - } - - } - - @Configuration - static class CustomListenerConfiguration { - - @Bean - @Primary - public QueryExecutionListener queryExecutionListener() { - return new QueryExecutionListener() { - @Override - public void beforeQuery(ExecutionInfo execInfo, List queryInfoList) { - System.out.println("beforeQuery"); - } - - @Override - public void afterQuery(ExecutionInfo execInfo, List queryInfoList) { - System.out.println("afterQuery"); - } - }; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import net.ttddyy.dsproxy.ExecutionInfo; +import net.ttddyy.dsproxy.QueryInfo; +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.listener.QueryExecutionListener; +import net.ttddyy.dsproxy.listener.logging.CommonsQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.CommonsSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.JULQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.JULSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.SystemOutSlowQueryListener; +import net.ttddyy.dsproxy.proxy.DefaultConnectionIdManager; +import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import net.ttddyy.dsproxy.transform.ParameterTransformer; +import net.ttddyy.dsproxy.transform.QueryTransformer; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import static org.assertj.core.api.Assertions.assertThat; + +class ProxyDataSourceConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, + TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("com.p6spy")); + + @Test + void testRegisterLogAndSlowQueryLogByDefaultToSlf4j() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogByUsingSlf4j() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.datasource-proxy.logging:slf4j"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingSystemOut() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.datasource-proxy.logging:sysout"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutQueryLoggingListener.class); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingJUL() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.datasourceProxy.logging:jul"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(JULSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(JULQueryLoggingListener.class); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingApacheCommons() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.datasourceProxy.logging:commons"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(CommonsSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(CommonsQueryLoggingListener.class); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testCustomParameterAndQueryTransformer() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomDataSourceProxyConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ParameterTransformer parameterTransformer = context.getBean(ParameterTransformer.class); + QueryTransformer queryTransformer = context.getBean(QueryTransformer.class); + assertThat(proxyDataSource.getProxyConfig().getParameterTransformer()).isSameAs(parameterTransformer); + assertThat(proxyDataSource.getProxyConfig().getQueryTransformer()).isSameAs(queryTransformer); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testCustomListeners() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomListenerConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + QueryExecutionListener queryExecutionListener = context.getBean(QueryExecutionListener.class); + + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).contains(queryExecutionListener); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testGlobalConnectionIdManagerByDefault() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + + assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(GlobalConnectionIdManager.class); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testCustomConnectionIdManager() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomDataSourceProxyConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + + assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(DefaultConnectionIdManager.class); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Configuration + static class CustomDataSourceProxyConfiguration { + + @Bean + public ParameterTransformer parameterTransformer() { + return (replacer, transformInfo) -> { + }; + } + + @Bean + public QueryTransformer queryTransformer() { + return (transformInfo) -> "TestQuery"; + } + + @Bean + public DataSourceProxyConnectionIdManagerProvider connectionIdManagerProvider() { + return DefaultConnectionIdManager::new; + } + + } + + @Configuration + static class CustomListenerConfiguration { + + @Bean + @Primary + public QueryExecutionListener queryExecutionListener() { + return new QueryExecutionListener() { + @Override + public void beforeQuery(ExecutionInfo execInfo, List queryInfoList) { + System.out.println("beforeQuery"); + } + + @Override + public void afterQuery(ExecutionInfo execInfo, List queryInfoList) { + System.out.println("afterQuery"); + } + }; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java index 446a620e7..ba2ad8b26 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java @@ -50,9 +50,7 @@ class TracingJdbcEventListenerTests extends TracingListenerStrategyTests { @Test void testShouldUsePlaceholderInSqlTagOfSpansForPreparedStatementIfIncludeParameterValuesIsSetToFalse() { - contextRunner - .withPropertyValues( - "spring.sleuth.jdbc.decorator.datasource.p6spy.tracing.include-parameter-values=false") + contextRunner.withPropertyValues("spring.sleuth.jdbc.p6spy.tracing.include-parameter-values=false") .run(context -> { DataSource dataSource = context.getBean(DataSource.class); TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java index 604eee779..9ff9efa2b 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java @@ -490,29 +490,28 @@ abstract class TracingListenerStrategyTests { @Test void testShouldIncludeOnlyConnectionTraces() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: connection") - .run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + contextRunner.withPropertyValues("spring.sleuth.jdbc.includes: connection").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); - assertThat(spanReporter.spans()).hasSize(1); - MutableSpan connectionSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); + assertThat(spanReporter.spans()).hasSize(1); + MutableSpan connectionSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); } @Test void testShouldIncludeOnlyQueryTraces() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: query").run(context -> { + contextRunner.withPropertyValues("spring.sleuth.jdbc.includes: query").run(context -> { DataSource dataSource = context.getBean(DataSource.class); TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); @@ -533,7 +532,7 @@ abstract class TracingListenerStrategyTests { @Test void testShouldIncludeOnlyFetchTraces() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: fetch").run(context -> { + contextRunner.withPropertyValues("spring.sleuth.jdbc.includes: fetch").run(context -> { DataSource dataSource = context.getBean(DataSource.class); TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); @@ -554,74 +553,71 @@ abstract class TracingListenerStrategyTests { @Test void testShouldIncludeOnlyConnectionAndQueryTraces() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: connection, query") - .run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + contextRunner.withPropertyValues("spring.sleuth.jdbc.includes: connection, query").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan connectionSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan connectionSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); } @Test void testShouldIncludeOnlyConnectionAndFetchTraces() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: connection, fetch") - .run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + contextRunner.withPropertyValues("spring.sleuth.jdbc.includes: connection, fetch").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan connectionSpan = spanReporter.spans().get(1); - MutableSpan resultSetSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(resultSetSpan.name()).isEqualTo("result-set"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan connectionSpan = spanReporter.spans().get(1); + MutableSpan resultSetSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(resultSetSpan.name()).isEqualTo("result-set"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); } @Test void testShouldIncludeOnlyQueryAndFetchTraces() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.decorator.datasource.includes: query, fetch") - .run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + contextRunner.withPropertyValues("spring.sleuth.jdbc.includes: query, fetch").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan resultSetSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(resultSetSpan.name()).isEqualTo("result-set"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan resultSetSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(statementSpan.name()).isEqualTo("select"); + assertThat(resultSetSpan.name()).isEqualTo("result-set"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); } @Test diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyBuilderCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyBuilderCustomizer.java index 87fff522b..befaa4f62 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyBuilderCustomizer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyBuilderCustomizer.java @@ -136,9 +136,6 @@ public class DataSourceProxyBuilderCustomizer { if (this.datasourceProxy.isJsonFormat()) { proxyDataSourceBuilder.asJson(); } - if (this.datasourceProxy.isCountQuery()) { - ifAvailable(this.queryCountStrategy, proxyDataSourceBuilder::countQuery); - } ifAvailable(this.listeners, l -> l.forEach(proxyDataSourceBuilder::listener)); ifAvailable(this.methodExecutionListeners, m -> m.forEach(proxyDataSourceBuilder::methodListener)); ifAvailable(this.parameterTransformer, proxyDataSourceBuilder::parameterTransformer); diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyProperties.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyProperties.java index 802fff5bf..be8a40839 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyProperties.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyProperties.java @@ -19,7 +19,6 @@ package org.springframework.cloud.sleuth.instrument.jdbc; import java.util.concurrent.TimeUnit; import java.util.logging.Level; -import net.ttddyy.dsproxy.QueryCountHolder; import net.ttddyy.dsproxy.listener.logging.CommonsLogLevel; import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; @@ -61,14 +60,6 @@ public class DataSourceProxyProperties { */ private boolean jsonFormat = false; - /** - * Creates listener to count queries. - * - * @see ProxyDataSourceBuilder#countQuery() - * @see QueryCountHolder - */ - private boolean countQuery = false; - public DataSourceProxyLogging getLogging() { return logging; } @@ -109,14 +100,6 @@ public class DataSourceProxyProperties { this.jsonFormat = jsonFormat; } - public boolean isCountQuery() { - return countQuery; - } - - public void setCountQuery(boolean countQuery) { - this.countQuery = countQuery; - } - /** * Properties to configure query logging listener. * diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceWrapper.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceWrapper.java index 0679b9aaa..3a3a8511e 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceWrapper.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceWrapper.java @@ -21,10 +21,7 @@ import javax.sql.DataSource; import org.springframework.jdbc.datasource.DelegatingDataSource; /** - * Interface that implicitly added to the CGLIB proxy of {@link DataSource}. - * - * Returns link of both real {@link DataSource}, decorated {@link DataSource} and all - * decorating chain including decorator bean name, instance and result of decorating. + * Returns link of both real {@link DataSource}, decorated {@link DataSource}. * * @author Arthur Gavlyukovskiy * @since 3.1.0 diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategy.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategy.java index 9ebe8e830..c89cbb2d2 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategy.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategy.java @@ -1,506 +1,506 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.jdbc; - -import java.net.URI; -import java.sql.Connection; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.sql.CommonDataSource; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.SpanAndScope; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.docs.AssertingSpan; -import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; -import org.springframework.lang.Nullable; -import org.springframework.util.StringUtils; - -/** - * Partially taken from - * https://github.com/openzipkin/brave/blob/v5.6.4/instrumentation/p6spy/src/main/java/brave/p6spy/TracingJdbcEventListener.java. - * - * @param connection type - * @param statement - * @param result set - */ -class TraceListenerStrategy { - - private static final Log log = LogFactory.getLog(TraceListenerStrategy.class); - - private final Map openConnections = new ConcurrentHashMap<>(); - - // Captures all the characters between = and either the next & or the end of the - // string. - private static final Pattern URL_SERVICE_NAME_FINDER = Pattern.compile("sleuthServiceName=(.*?)(?:&|$)"); - - private final Tracer tracer; - - private final List traceTypes; - - private final List customizers; - - TraceListenerStrategy(Tracer tracer, List traceTypes, - List customizers) { - this.tracer = tracer; - this.traceTypes = traceTypes; - this.customizers = customizers; - } - - void beforeGetConnection(CON connectionKey, @Nullable CommonDataSource dataSource, String dataSourceName) { - if (log.isTraceEnabled()) { - log.trace("Before get connection key [" + connectionKey + "] - current span is [" + tracer.currentSpan() - + "]"); - } - SpanAndScope spanAndScope = null; - if (this.traceTypes.contains(TraceType.CONNECTION)) { - AssertingSpanBuilder connectionSpanBuilder = AssertingSpanBuilder - .of(SleuthJdbcSpan.JDBC_CONNECTION_SPAN, tracer.spanBuilder()) - .name(SleuthJdbcSpan.JDBC_CONNECTION_SPAN.getName()); - connectionSpanBuilder.remoteServiceName(dataSourceName); - connectionSpanBuilder.kind(Span.Kind.CLIENT); - this.customizers.stream().filter(customizer -> customizer.isApplicable(dataSource)) - .forEach(customizer -> customizer.customizeConnectionSpan(dataSource, connectionSpanBuilder)); - Span connectionSpan = connectionSpanBuilder.start(); - spanAndScope = new SpanAndScope(connectionSpan, tracer.withSpan(connectionSpan)); - if (log.isTraceEnabled()) { - log.trace("Started client span before connection [" + connectionSpan + "] - current span is [" - + tracer.currentSpan() + "]"); - } - } - ConnectionInfo connectionInfo = new ConnectionInfo(spanAndScope); - this.openConnections.put(connectionKey, connectionInfo); - } - - void afterGetConnection(CON connectionKey, Connection connection, Throwable t) { - if (log.isTraceEnabled()) { - log.trace("After get connection [" + connectionKey + "]. Current span is [" + tracer.currentSpan() + "]"); - } - this.openConnections.get(connectionKey).getSpan().ifPresent(spanAndScope -> { - parseServerIpAndPort(connection, spanAndScope.getSpan()); - }); - if (t != null) { - ConnectionInfo connectionInfo = this.openConnections.remove(connectionKey); - connectionInfo.getSpan().ifPresent(connectionSpan -> { - parseServerIpAndPort(connection, connectionSpan.getSpan()); - if (log.isTraceEnabled()) { - log.trace("Closing client span due to exception [" + connectionSpan.getSpan() - + "] - current span is [" + tracer.currentSpan() + "]"); - } - connectionSpan.getSpan().error(t); - connectionSpan.close(); - if (log.isTraceEnabled()) { - log.trace("Current span [" + tracer.currentSpan() + "]"); - } - }); - } - } - - void beforeQuery(CON connectionKey, Connection connection, STMT statementKey, String dataSourceName) { - if (log.isTraceEnabled()) { - log.trace("Before query - connection [" + connectionKey + "] and current span [" + tracer.currentSpan() - + "]"); - } - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - if (connectionInfo == null) { - if (log.isTraceEnabled()) { - log.trace("Connection may be closed after statement preparation, but before statement execution"); - } - return; - } - SpanAndScope spanAndScope = null; - if (traceTypes.contains(TraceType.QUERY)) { - Span.Builder statementSpanBuilder = AssertingSpanBuilder - .of(SleuthJdbcSpan.JDBC_QUERY_SPAN, tracer.spanBuilder()) - .name(String.format(SleuthJdbcSpan.JDBC_QUERY_SPAN.getName(), "query")); - statementSpanBuilder.remoteServiceName(dataSourceName); - parseServerIpAndPort(connection, statementSpanBuilder); - statementSpanBuilder.kind(Span.Kind.CLIENT); - Span statementSpan = statementSpanBuilder.start(); - spanAndScope = new SpanAndScope(statementSpan, tracer.withSpan(statementSpan)); - if (log.isTraceEnabled()) { - log.trace("Started client span before query [" + statementSpan + "] - current span is [" - + tracer.currentSpan() + "]"); - } - } - StatementInfo statementInfo = new StatementInfo(spanAndScope); - connectionInfo.getNestedStatements().put(statementKey, statementInfo); - } - - void addQueryRowCount(CON connectionKey, STMT statementKey, int rowCount) { - if (log.isTraceEnabled()) { - log.trace("Add query row count for connection key [" + connectionKey + "]"); - } - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - if (connectionInfo == null) { - if (log.isTraceEnabled()) { - log.trace("Connection is already closed"); - } - return; - } - StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); - statementInfo.getSpan() - .ifPresent(statementSpan -> AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, statementSpan.getSpan()) - .tag(SleuthJdbcSpan.QueryTags.ROW_COUNT, String.valueOf(rowCount))); - } - - void afterQuery(CON connectionKey, STMT statementKey, String sql, Throwable t) { - if (log.isTraceEnabled()) { - log.trace("After query for connection key [" + connectionKey + "]"); - } - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - if (connectionInfo == null) { - if (log.isTraceEnabled()) { - log.trace( - "Connection may be closed after statement preparation, but before statement execution. Current span is [" - + tracer.currentSpan() + "]"); - } - return; - } - StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); - statementInfo.getSpan().ifPresent(statementSpan -> { - updateQuerySpan(sql, t, statementSpan); - statementSpan.close(); - if (log.isTraceEnabled()) { - log.trace("Current span [" + tracer.currentSpan() + "]"); - } - }); - } - - private void updateQuerySpan(String sql, Throwable t, SpanAndScope statementSpan) { - AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, statementSpan.getSpan()) - .tag(SleuthJdbcSpan.QueryTags.QUERY, sql).name(spanName(sql)); - if (t != null) { - statementSpan.getSpan().error(t); - } - if (log.isTraceEnabled()) { - log.trace( - "Closing statement span [" + statementSpan + "] - current span is [" + tracer.currentSpan() + "]"); - } - } - - void beforeResultSetNext(CON connectionKey, Connection connection, STMT statementKey, RS resultSetKey, - String dataSourceName) { - if (log.isTraceEnabled()) { - log.trace("Before result set next"); - } - if (!traceTypes.contains(TraceType.FETCH)) { - return; - } - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - // ConnectionInfo may be null if Connection was closed before ResultSet - if (connectionInfo == null) { - if (log.isTraceEnabled()) { - log.trace("No connection info, skipping"); - } - return; - } - if (connectionInfo.getNestedResultSetSpans().containsKey(resultSetKey)) { - if (log.isTraceEnabled()) { - log.trace("ResultSet span is already created"); - } - return; - } - AssertingSpanBuilder resultSetSpanBuilder = AssertingSpanBuilder - .of(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN, tracer.spanBuilder()) - .name(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN.getName()); - resultSetSpanBuilder.remoteServiceName(dataSourceName); - resultSetSpanBuilder.kind(Span.Kind.CLIENT); - parseServerIpAndPort(connection, resultSetSpanBuilder); - Span resultSetSpan = resultSetSpanBuilder.start(); - SpanAndScope SpanAndScope = new SpanAndScope(resultSetSpan, tracer.withSpan(resultSetSpan)); - if (log.isTraceEnabled()) { - log.trace("Started client result set span [" + resultSetSpan + "] - current span is [" - + tracer.currentSpan() + "]"); - } - connectionInfo.getNestedResultSetSpans().put(resultSetKey, SpanAndScope); - StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); - // StatementInfo may be null when Statement is proxied and instance returned from - // ResultSet is different from instance returned in query method - // in this case if Statement is closed before ResultSet span won't be finished - // immediately, but when Connection is closed - if (statementInfo != null) { - statementInfo.getNestedResultSetSpans().put(resultSetKey, SpanAndScope); - } - } - - void afterResultSetClose(CON connectionKey, RS resultSetKey, int rowCount, Throwable t) { - if (log.isTraceEnabled()) { - log.trace("After result set close"); - } - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - // ConnectionInfo may be null if Connection was closed before ResultSet - if (connectionInfo == null) { - return; - } - SpanAndScope resultSetSpan = connectionInfo.getNestedResultSetSpans().remove(resultSetKey); - // ResultSet span may be null if Statement or ResultSet were already closed - if (resultSetSpan == null) { - return; - } - if (rowCount != -1) { - AssertingSpan.of(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN, resultSetSpan.getSpan()) - .tag(SleuthJdbcSpan.QueryTags.ROW_COUNT, String.valueOf(rowCount)); - } - if (t != null) { - resultSetSpan.getSpan().error(t); - } - if (log.isTraceEnabled()) { - log.trace("Closing client result set span [" + resultSetSpan + "] - current span is [" - + tracer.currentSpan() + "]"); - } - resultSetSpan.close(); - if (log.isTraceEnabled()) { - log.trace("Current span [" + tracer.currentSpan() + "]"); - } - } - - void afterStatementClose(CON connectionKey, STMT statementKey) { - if (log.isTraceEnabled()) { - log.trace("After statement close"); - } - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - // ConnectionInfo may be null if Connection was closed before Statement - if (connectionInfo == null) { - return; - } - StatementInfo statementInfo = connectionInfo.getNestedStatements().remove(statementKey); - if (statementInfo != null) { - statementInfo.getNestedResultSetSpans().forEach((resultSetKey, span) -> { - connectionInfo.getNestedResultSetSpans().remove(resultSetKey); - if (log.isTraceEnabled()) { - log.trace("Closing span after statement close [" + span.getSpan() + "] - current span is [" - + tracer.currentSpan() + "]"); - } - span.close(); - if (log.isTraceEnabled()) { - log.trace("Current span [" + tracer.currentSpan() + "]"); - } - }); - statementInfo.getNestedResultSetSpans().clear(); - } - } - - void afterCommit(CON connectionKey, Throwable t) { - if (log.isTraceEnabled()) { - log.trace("After commit"); - } - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - if (connectionInfo == null) { - // Connection is already closed - return; - } - connectionInfo.getSpan().ifPresent(connectionSpan -> { - if (t != null) { - connectionSpan.getSpan().error(t); - } - AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, connectionSpan.getSpan()) - .event(SleuthJdbcSpan.QueryEvents.COMMIT); - }); - } - - void afterRollback(CON connectionKey, Throwable t) { - if (log.isTraceEnabled()) { - log.trace("After rollback"); - } - ConnectionInfo connectionInfo = openConnections.get(connectionKey); - if (connectionInfo == null) { - // Connection is already closed - return; - } - connectionInfo.getSpan().ifPresent(connectionSpan -> { - if (t != null) { - connectionSpan.getSpan().error(t); - } - else { - connectionSpan.getSpan().error(new JdbcException("Transaction rolled back")); - } - AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, connectionSpan.getSpan()) - .event(SleuthJdbcSpan.QueryEvents.ROLLBACK); - }); - } - - void afterConnectionClose(CON connectionKey, Throwable t) { - if (log.isTraceEnabled()) { - log.trace("After connection close with key [" + connectionKey + "]"); - } - ConnectionInfo connectionInfo = openConnections.remove(connectionKey); - if (connectionInfo == null) { - // connection is already closed - return; - } - connectionInfo.getNestedResultSetSpans().values().forEach(SpanAndScope::close); - connectionInfo.getNestedStatements().values() - .forEach(statementInfo -> statementInfo.getSpan().ifPresent(SpanAndScope::close)); - if (log.isTraceEnabled()) { - log.trace("Current span after closing statements [" + tracer.currentSpan() + "]"); - } - connectionInfo.getSpan().ifPresent(connectionSpan -> { - if (t != null) { - connectionSpan.getSpan().error(t); - } - if (log.isTraceEnabled()) { - log.trace("Closing span after connection close [" + connectionSpan.getSpan() + "] - current span is [" - + tracer.currentSpan() + "]"); - } - connectionSpan.close(); - if (log.isTraceEnabled()) { - log.trace("Current span [" + tracer.currentSpan() + "]"); - } - }); - } - - private String spanName(String sql) { - return sql.substring(0, sql.indexOf(' ')).toLowerCase(Locale.ROOT); - } - - private void parseServerIpAndPort(Connection connection, Span.Builder span) { - if (connection == null) { - return; - } - UrlAndRemoteServiceName urlAndRemoteServiceName = parseServerIpAndPort(connection); - span.remoteServiceName(urlAndRemoteServiceName.remoteServiceName); - URI url = urlAndRemoteServiceName.url; - if (url != null) { - span.remoteIpAndPort(url.getHost(), url.getPort()); - } - } - - private void parseServerIpAndPort(Connection connection, Span span) { - if (connection == null) { - return; - } - UrlAndRemoteServiceName urlAndRemoteServiceName = parseServerIpAndPort(connection); - span.remoteServiceName(urlAndRemoteServiceName.remoteServiceName); - URI url = urlAndRemoteServiceName.url; - if (url != null) { - span.remoteIpAndPort(url.getHost(), url.getPort()); - } - } - - /** - * This attempts to get the ip and port from the JDBC URL. Ex. localhost and 5555 from - * {@code - * jdbc:mysql://localhost:5555/mydatabase}. - * - * Taken from Brave. - */ - private UrlAndRemoteServiceName parseServerIpAndPort(Connection connection) { - String remoteServiceName = ""; - try { - String urlAsString = connection.getMetaData().getURL().substring(5); // strip - // "jdbc:" - URI url = URI.create(urlAsString.replace(" ", "")); // Remove all white space - // according to RFC 2396 - Matcher matcher = URL_SERVICE_NAME_FINDER.matcher(url.toString()); - if (matcher.find() && matcher.groupCount() == 1) { - String parsedServiceName = matcher.group(1); - if (parsedServiceName != null && !parsedServiceName.isEmpty()) { - remoteServiceName = parsedServiceName; - } - } - if (!StringUtils.hasText(remoteServiceName)) { - String databaseName = connection.getCatalog(); - if (databaseName != null && !databaseName.isEmpty()) { - remoteServiceName = databaseName; - } - } - return new UrlAndRemoteServiceName(url, remoteServiceName); - } - catch (Exception e) { - // remote address is optional - return new UrlAndRemoteServiceName(null, remoteServiceName); - } - } - - private final class ConnectionInfo { - - private final SpanAndScope span; - - private final Map nestedStatements = new ConcurrentHashMap<>(); - - private final Map nestedResultSetSpans = new ConcurrentHashMap<>(); - - private ConnectionInfo(@Nullable SpanAndScope span) { - this.span = span; - } - - Optional getSpan() { - return Optional.ofNullable(span); - } - - Map getNestedStatements() { - return nestedStatements; - } - - Map getNestedResultSetSpans() { - return nestedResultSetSpans; - } - - } - - private final class StatementInfo { - - private final SpanAndScope span; - - private final Map nestedResultSetSpans = new ConcurrentHashMap<>(); - - private StatementInfo(SpanAndScope span) { - this.span = span; - } - - Optional getSpan() { - return Optional.ofNullable(span); - } - - Map getNestedResultSetSpans() { - return nestedResultSetSpans; - } - - } - - private final class UrlAndRemoteServiceName { - - final URI url; - - final String remoteServiceName; - - private UrlAndRemoteServiceName(@Nullable URI url, String remoteServiceName) { - this.url = url; - this.remoteServiceName = remoteServiceName; - } - - } - - private static final class JdbcException extends RuntimeException { - - JdbcException(String message) { - super(message); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.net.URI; +import java.sql.Connection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.sql.CommonDataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; +import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * Partially taken from + * https://github.com/openzipkin/brave/blob/v5.6.4/instrumentation/p6spy/src/main/java/brave/p6spy/TracingJdbcEventListener.java. + * + * @param connection type + * @param statement + * @param result set + */ +class TraceListenerStrategy { + + private static final Log log = LogFactory.getLog(TraceListenerStrategy.class); + + private final Map openConnections = new ConcurrentHashMap<>(); + + // Captures all the characters between = and either the next & or the end of the + // string. + private static final Pattern URL_SERVICE_NAME_FINDER = Pattern.compile("sleuthServiceName=(.*?)(?:&|$)"); + + private final Tracer tracer; + + private final List traceTypes; + + private final List customizers; + + TraceListenerStrategy(Tracer tracer, List traceTypes, + List customizers) { + this.tracer = tracer; + this.traceTypes = traceTypes; + this.customizers = customizers; + } + + void beforeGetConnection(CON connectionKey, @Nullable CommonDataSource dataSource, String dataSourceName) { + if (log.isTraceEnabled()) { + log.trace("Before get connection key [" + connectionKey + "] - current span is [" + tracer.currentSpan() + + "]"); + } + SpanAndScope spanAndScope = null; + if (this.traceTypes.contains(TraceType.CONNECTION)) { + AssertingSpanBuilder connectionSpanBuilder = AssertingSpanBuilder + .of(SleuthJdbcSpan.JDBC_CONNECTION_SPAN, tracer.spanBuilder()) + .name(SleuthJdbcSpan.JDBC_CONNECTION_SPAN.getName()); + connectionSpanBuilder.remoteServiceName(dataSourceName); + connectionSpanBuilder.kind(Span.Kind.CLIENT); + this.customizers.stream().filter(customizer -> customizer.isApplicable(dataSource)) + .forEach(customizer -> customizer.customizeConnectionSpan(dataSource, connectionSpanBuilder)); + Span connectionSpan = connectionSpanBuilder.start(); + spanAndScope = new SpanAndScope(connectionSpan, tracer.withSpan(connectionSpan)); + if (log.isTraceEnabled()) { + log.trace("Started client span before connection [" + connectionSpan + "] - current span is [" + + tracer.currentSpan() + "]"); + } + } + ConnectionInfo connectionInfo = new ConnectionInfo(spanAndScope); + this.openConnections.put(connectionKey, connectionInfo); + } + + void afterGetConnection(CON connectionKey, Connection connection, Throwable t) { + if (log.isTraceEnabled()) { + log.trace("After get connection [" + connectionKey + "]. Current span is [" + tracer.currentSpan() + "]"); + } + this.openConnections.get(connectionKey).getSpan().ifPresent(spanAndScope -> { + parseServerIpAndPort(connection, spanAndScope.getSpan()); + }); + if (t != null) { + ConnectionInfo connectionInfo = this.openConnections.remove(connectionKey); + connectionInfo.getSpan().ifPresent(connectionSpan -> { + parseServerIpAndPort(connection, connectionSpan.getSpan()); + if (log.isTraceEnabled()) { + log.trace("Closing client span due to exception [" + connectionSpan.getSpan() + + "] - current span is [" + tracer.currentSpan() + "]"); + } + connectionSpan.getSpan().error(t); + connectionSpan.close(); + if (log.isTraceEnabled()) { + log.trace("Current span [" + tracer.currentSpan() + "]"); + } + }); + } + } + + void beforeQuery(CON connectionKey, Connection connection, STMT statementKey, String dataSourceName) { + if (log.isTraceEnabled()) { + log.trace("Before query - connection [" + connectionKey + "] and current span [" + tracer.currentSpan() + + "]"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + if (log.isTraceEnabled()) { + log.trace("Connection may be closed after statement preparation, but before statement execution"); + } + return; + } + SpanAndScope spanAndScope = null; + if (traceTypes.contains(TraceType.QUERY)) { + Span.Builder statementSpanBuilder = AssertingSpanBuilder + .of(SleuthJdbcSpan.JDBC_QUERY_SPAN, tracer.spanBuilder()) + .name(String.format(SleuthJdbcSpan.JDBC_QUERY_SPAN.getName(), "query")); + statementSpanBuilder.remoteServiceName(dataSourceName); + parseServerIpAndPort(connection, statementSpanBuilder); + statementSpanBuilder.kind(Span.Kind.CLIENT); + Span statementSpan = statementSpanBuilder.start(); + spanAndScope = new SpanAndScope(statementSpan, tracer.withSpan(statementSpan)); + if (log.isTraceEnabled()) { + log.trace("Started client span before query [" + statementSpan + "] - current span is [" + + tracer.currentSpan() + "]"); + } + } + StatementInfo statementInfo = new StatementInfo(spanAndScope); + connectionInfo.getNestedStatements().put(statementKey, statementInfo); + } + + void addQueryRowCount(CON connectionKey, STMT statementKey, int rowCount) { + if (log.isTraceEnabled()) { + log.trace("Add query row count for connection key [" + connectionKey + "]"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + if (log.isTraceEnabled()) { + log.trace("Connection is already closed"); + } + return; + } + StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); + statementInfo.getSpan() + .ifPresent(statementSpan -> AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, statementSpan.getSpan()) + .tag(SleuthJdbcSpan.QueryTags.ROW_COUNT, String.valueOf(rowCount))); + } + + void afterQuery(CON connectionKey, STMT statementKey, String sql, Throwable t) { + if (log.isTraceEnabled()) { + log.trace("After query for connection key [" + connectionKey + "]"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + if (log.isTraceEnabled()) { + log.trace( + "Connection may be closed after statement preparation, but before statement execution. Current span is [" + + tracer.currentSpan() + "]"); + } + return; + } + StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); + statementInfo.getSpan().ifPresent(statementSpan -> { + updateQuerySpan(sql, t, statementSpan); + statementSpan.close(); + if (log.isTraceEnabled()) { + log.trace("Current span [" + tracer.currentSpan() + "]"); + } + }); + } + + private void updateQuerySpan(String sql, Throwable t, SpanAndScope statementSpan) { + AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, statementSpan.getSpan()) + .tag(SleuthJdbcSpan.QueryTags.QUERY, sql).name(spanName(sql)); + if (t != null) { + statementSpan.getSpan().error(t); + } + if (log.isTraceEnabled()) { + log.trace( + "Closing statement span [" + statementSpan + "] - current span is [" + tracer.currentSpan() + "]"); + } + } + + void beforeResultSetNext(CON connectionKey, Connection connection, STMT statementKey, RS resultSetKey, + String dataSourceName) { + if (log.isTraceEnabled()) { + log.trace("Before result set next"); + } + if (!traceTypes.contains(TraceType.FETCH)) { + return; + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + // ConnectionInfo may be null if Connection was closed before ResultSet + if (connectionInfo == null) { + if (log.isTraceEnabled()) { + log.trace("No connection info, skipping"); + } + return; + } + if (connectionInfo.getNestedResultSetSpans().containsKey(resultSetKey)) { + if (log.isTraceEnabled()) { + log.trace("ResultSet span is already created"); + } + return; + } + AssertingSpanBuilder resultSetSpanBuilder = AssertingSpanBuilder + .of(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN, tracer.spanBuilder()) + .name(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN.getName()); + resultSetSpanBuilder.remoteServiceName(dataSourceName); + resultSetSpanBuilder.kind(Span.Kind.CLIENT); + parseServerIpAndPort(connection, resultSetSpanBuilder); + Span resultSetSpan = resultSetSpanBuilder.start(); + SpanAndScope SpanAndScope = new SpanAndScope(resultSetSpan, tracer.withSpan(resultSetSpan)); + if (log.isTraceEnabled()) { + log.trace("Started client result set span [" + resultSetSpan + "] - current span is [" + + tracer.currentSpan() + "]"); + } + connectionInfo.getNestedResultSetSpans().put(resultSetKey, SpanAndScope); + StatementInfo statementInfo = connectionInfo.getNestedStatements().get(statementKey); + // StatementInfo may be null when Statement is proxied and instance returned from + // ResultSet is different from instance returned in query method + // in this case if Statement is closed before ResultSet span won't be finished + // immediately, but when Connection is closed + if (statementInfo != null) { + statementInfo.getNestedResultSetSpans().put(resultSetKey, SpanAndScope); + } + } + + void afterResultSetClose(CON connectionKey, RS resultSetKey, int rowCount, Throwable t) { + if (log.isTraceEnabled()) { + log.trace("After result set close"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + // ConnectionInfo may be null if Connection was closed before ResultSet + if (connectionInfo == null) { + return; + } + SpanAndScope resultSetSpan = connectionInfo.getNestedResultSetSpans().remove(resultSetKey); + // ResultSet span may be null if Statement or ResultSet were already closed + if (resultSetSpan == null) { + return; + } + if (rowCount != -1) { + AssertingSpan.of(SleuthJdbcSpan.JDBC_RESULT_SET_SPAN, resultSetSpan.getSpan()) + .tag(SleuthJdbcSpan.QueryTags.ROW_COUNT, String.valueOf(rowCount)); + } + if (t != null) { + resultSetSpan.getSpan().error(t); + } + if (log.isTraceEnabled()) { + log.trace("Closing client result set span [" + resultSetSpan + "] - current span is [" + + tracer.currentSpan() + "]"); + } + resultSetSpan.close(); + if (log.isTraceEnabled()) { + log.trace("Current span [" + tracer.currentSpan() + "]"); + } + } + + void afterStatementClose(CON connectionKey, STMT statementKey) { + if (log.isTraceEnabled()) { + log.trace("After statement close"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + // ConnectionInfo may be null if Connection was closed before Statement + if (connectionInfo == null) { + return; + } + StatementInfo statementInfo = connectionInfo.getNestedStatements().remove(statementKey); + if (statementInfo != null) { + statementInfo.getNestedResultSetSpans().forEach((resultSetKey, span) -> { + connectionInfo.getNestedResultSetSpans().remove(resultSetKey); + if (log.isTraceEnabled()) { + log.trace("Closing span after statement close [" + span.getSpan() + "] - current span is [" + + tracer.currentSpan() + "]"); + } + span.close(); + if (log.isTraceEnabled()) { + log.trace("Current span [" + tracer.currentSpan() + "]"); + } + }); + statementInfo.getNestedResultSetSpans().clear(); + } + } + + void afterCommit(CON connectionKey, Throwable t) { + if (log.isTraceEnabled()) { + log.trace("After commit"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + // Connection is already closed + return; + } + connectionInfo.getSpan().ifPresent(connectionSpan -> { + if (t != null) { + connectionSpan.getSpan().error(t); + } + AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, connectionSpan.getSpan()) + .event(SleuthJdbcSpan.QueryEvents.COMMIT); + }); + } + + void afterRollback(CON connectionKey, Throwable t) { + if (log.isTraceEnabled()) { + log.trace("After rollback"); + } + ConnectionInfo connectionInfo = openConnections.get(connectionKey); + if (connectionInfo == null) { + // Connection is already closed + return; + } + connectionInfo.getSpan().ifPresent(connectionSpan -> { + if (t != null) { + connectionSpan.getSpan().error(t); + } + else { + connectionSpan.getSpan().error(new JdbcException("Transaction rolled back")); + } + AssertingSpan.of(SleuthJdbcSpan.JDBC_QUERY_SPAN, connectionSpan.getSpan()) + .event(SleuthJdbcSpan.QueryEvents.ROLLBACK); + }); + } + + void afterConnectionClose(CON connectionKey, Throwable t) { + if (log.isTraceEnabled()) { + log.trace("After connection close with key [" + connectionKey + "]"); + } + ConnectionInfo connectionInfo = openConnections.remove(connectionKey); + if (connectionInfo == null) { + // connection is already closed + return; + } + connectionInfo.getNestedResultSetSpans().values().forEach(SpanAndScope::close); + connectionInfo.getNestedStatements().values() + .forEach(statementInfo -> statementInfo.getSpan().ifPresent(SpanAndScope::close)); + if (log.isTraceEnabled()) { + log.trace("Current span after closing statements [" + tracer.currentSpan() + "]"); + } + connectionInfo.getSpan().ifPresent(connectionSpan -> { + if (t != null) { + connectionSpan.getSpan().error(t); + } + if (log.isTraceEnabled()) { + log.trace("Closing span after connection close [" + connectionSpan.getSpan() + "] - current span is [" + + tracer.currentSpan() + "]"); + } + connectionSpan.close(); + if (log.isTraceEnabled()) { + log.trace("Current span [" + tracer.currentSpan() + "]"); + } + }); + } + + private String spanName(String sql) { + return sql.substring(0, sql.indexOf(' ')).toLowerCase(Locale.ROOT); + } + + private void parseServerIpAndPort(Connection connection, Span.Builder span) { + if (connection == null) { + return; + } + UrlAndRemoteServiceName urlAndRemoteServiceName = parseServerIpAndPort(connection); + span.remoteServiceName(urlAndRemoteServiceName.remoteServiceName); + URI url = urlAndRemoteServiceName.url; + if (url != null) { + span.remoteIpAndPort(url.getHost(), url.getPort()); + } + } + + private void parseServerIpAndPort(Connection connection, Span span) { + if (connection == null) { + return; + } + UrlAndRemoteServiceName urlAndRemoteServiceName = parseServerIpAndPort(connection); + span.remoteServiceName(urlAndRemoteServiceName.remoteServiceName); + URI url = urlAndRemoteServiceName.url; + if (url != null) { + span.remoteIpAndPort(url.getHost(), url.getPort()); + } + } + + /** + * This attempts to get the ip and port from the JDBC URL. Ex. localhost and 5555 from + * {@code + * jdbc:mysql://localhost:5555/mydatabase}. + * + * Taken from Brave. + */ + private UrlAndRemoteServiceName parseServerIpAndPort(Connection connection) { + String remoteServiceName = ""; + try { + String urlAsString = connection.getMetaData().getURL().substring(5); // strip + // "jdbc:" + URI url = URI.create(urlAsString.replace(" ", "")); // Remove all white space + // according to RFC 2396 + Matcher matcher = URL_SERVICE_NAME_FINDER.matcher(url.toString()); + if (matcher.find() && matcher.groupCount() == 1) { + String parsedServiceName = matcher.group(1); + if (parsedServiceName != null && !parsedServiceName.isEmpty()) { + remoteServiceName = parsedServiceName; + } + } + if (!StringUtils.hasText(remoteServiceName)) { + String databaseName = connection.getCatalog(); + if (databaseName != null && !databaseName.isEmpty()) { + remoteServiceName = databaseName; + } + } + return new UrlAndRemoteServiceName(url, remoteServiceName); + } + catch (Exception e) { + // remote address is optional + return new UrlAndRemoteServiceName(null, remoteServiceName); + } + } + + private final class ConnectionInfo { + + private final SpanAndScope span; + + private final Map nestedStatements = new ConcurrentHashMap<>(); + + private final Map nestedResultSetSpans = new ConcurrentHashMap<>(); + + private ConnectionInfo(@Nullable SpanAndScope span) { + this.span = span; + } + + Optional getSpan() { + return Optional.ofNullable(span); + } + + Map getNestedStatements() { + return nestedStatements; + } + + Map getNestedResultSetSpans() { + return nestedResultSetSpans; + } + + } + + private final class StatementInfo { + + private final SpanAndScope span; + + private final Map nestedResultSetSpans = new ConcurrentHashMap<>(); + + private StatementInfo(SpanAndScope span) { + this.span = span; + } + + Optional getSpan() { + return Optional.ofNullable(span); + } + + Map getNestedResultSetSpans() { + return nestedResultSetSpans; + } + + } + + private final class UrlAndRemoteServiceName { + + final URI url; + + final String remoteServiceName; + + private UrlAndRemoteServiceName(@Nullable URI url, String remoteServiceName) { + this.url = url; + this.remoteServiceName = remoteServiceName; + } + + } + + private static final class JdbcException extends RuntimeException { + + JdbcException(String message) { + super(message); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java index 1692beac8..a7e76064f 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/kafka/KafkaProducerTest.java @@ -1,66 +1,66 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.brave.instrument.kafka; - -import java.util.Optional; -import java.util.concurrent.TimeUnit; - -import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.common.header.Header; -import org.assertj.core.api.BDDAssertions; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Test; - -import org.springframework.cloud.sleuth.brave.BraveTestTracing; -import org.springframework.cloud.sleuth.test.TestTracingAware; - -public class KafkaProducerTest extends org.springframework.cloud.sleuth.instrument.kafka.KafkaProducerTest { - - BraveTestTracing testTracing; - - @Override - public TestTracingAware tracerTest() { - if (this.testTracing == null) { - this.testTracing = new BraveTestTracing(); - } - return this.testTracing; - } - - @Test - public void should_inject_native_headers() throws InterruptedException { - ProducerRecord producerRecord = new ProducerRecord<>(testTopic, "test", "test"); - startKafkaConsumer(); - - this.kafkaProducer.send(producerRecord); - - Awaitility.await().atMost(1, TimeUnit.MINUTES).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { - ConsumerRecord consumerRecord = consumerRecords.poll(15, TimeUnit.SECONDS); - - BDDAssertions.then(consumerRecord).isNotNull(); - BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-TraceId")).isNotNull(); - BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-SpanId")).isNotNull(); - BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-Sampled")).isNotNull(); - }); - } - - private static String getHeaderValueOrNull(ConsumerRecord consumerRecord, String header) { - return Optional.ofNullable(consumerRecord).map(ConsumerRecord::headers) - .map(headers -> headers.lastHeader(header)).map(Header::value).map(String::new).orElse(null); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.kafka; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.header.Header; +import org.assertj.core.api.BDDAssertions; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.sleuth.brave.BraveTestTracing; +import org.springframework.cloud.sleuth.test.TestTracingAware; + +public class KafkaProducerTest extends org.springframework.cloud.sleuth.instrument.kafka.KafkaProducerTest { + + BraveTestTracing testTracing; + + @Override + public TestTracingAware tracerTest() { + if (this.testTracing == null) { + this.testTracing = new BraveTestTracing(); + } + return this.testTracing; + } + + @Test + public void should_inject_native_headers() throws InterruptedException { + ProducerRecord producerRecord = new ProducerRecord<>(testTopic, "test", "test"); + startKafkaConsumer(); + + this.kafkaProducer.send(producerRecord); + + Awaitility.await().atMost(1, TimeUnit.MINUTES).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { + ConsumerRecord consumerRecord = consumerRecords.poll(15, TimeUnit.SECONDS); + + BDDAssertions.then(consumerRecord).isNotNull(); + BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-TraceId")).isNotNull(); + BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-SpanId")).isNotNull(); + BDDAssertions.then(getHeaderValueOrNull(consumerRecord, "X-B3-Sampled")).isNotNull(); + }); + } + + private static String getHeaderValueOrNull(ConsumerRecord consumerRecord, String header) { + return Optional.ofNullable(consumerRecord).map(ConsumerRecord::headers) + .map(headers -> headers.lastHeader(header)).map(Header::value).map(String::new).orElse(null); + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java index b8a862262..5ebdeb20d 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java @@ -1,233 +1,237 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.brave.instrument.web; - -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicReference; - -import brave.Span; -import brave.Tracer; -import brave.handler.MutableSpan; -import brave.handler.SpanHandler; -import brave.sampler.Sampler; -import brave.test.TestSpanHandler; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.sleuth.SpanName; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.EnableAsync; - -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.assertj.core.api.BDDAssertions.then; - -@SpringBootTest(classes = { TraceAsyncIntegrationTests.TraceAsyncITestConfiguration.class }) -public class TraceAsyncIntegrationTests { - - @Autowired - ClassPerformingAsyncLogic classPerformingAsyncLogic; - - @Autowired - Tracer tracer; - - @Autowired - TestSpanHandler spans; - - @BeforeEach - public void cleanup() { - this.spans.clear(); - this.classPerformingAsyncLogic.clear(); - } - - @Test - public void should_set_span_on_an_async_annotated_method() { - whenAsyncProcessingTakesPlace(); - - thenANewAsyncSpanGetsCreated(); - } - - @Test - public void should_set_span_with_custom_method_on_an_async_annotated_method() { - whenAsyncProcessingTakesPlaceWithCustomSpanName(); - - thenAsyncSpanHasCustomName(); - } - - @Test - public void should_continue_a_span_on_an_async_annotated_method() { - Span span = givenASpanInCurrentThread(); - - try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { - whenAsyncProcessingTakesPlace(); - } - finally { - span.finish(); - } - - thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(span); - } - - @Test - public void should_continue_a_span_with_custom_method_on_an_async_annotated_method() { - Span span = givenASpanInCurrentThread(); - - try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { - whenAsyncProcessingTakesPlaceWithCustomSpanName(); - } - finally { - span.finish(); - } - - thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(span); - } - - private Span givenASpanInCurrentThread() { - return this.tracer.nextSpan().name("http:existing"); - } - - private void whenAsyncProcessingTakesPlace() { - this.classPerformingAsyncLogic.invokeAsynchronousLogic(); - } - - private void whenAsyncProcessingTakesPlaceWithCustomSpanName() { - this.classPerformingAsyncLogic.customNameInvokeAsynchronousLogic(); - } - - private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(final Span span) { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) - .isEqualTo(span.context().traceId()); - then(this.spans).hasSize(2); - // HTTP - then(this.spans.get(0).name()).isEqualTo("http:existing"); - // ASYNC - then(this.spans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "invokeAsynchronousLogic"); - }); - } - - private void thenANewAsyncSpanGetsCreated() { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(this.spans).hasSize(1); - MutableSpan storedSpan = this.spans.get(0); - then(storedSpan.name()).isEqualTo("invoke-asynchronous-logic"); - then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "invokeAsynchronousLogic"); - }); - } - - private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(final Span span) { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan()).isNotNull(); - then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) - .isEqualTo(span.context().traceId()); - then(this.spans).hasSize(2); - // HTTP - then(this.spans.get(0).name()).isEqualTo("http:existing"); - // ASYNC - then(this.spans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "customNameInvokeAsynchronousLogic"); - }); - } - - private void thenAsyncSpanHasCustomName() { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(this.spans).hasSize(1); - MutableSpan storedSpan = this.spans.get(0); - then(storedSpan.name()).isEqualTo("foo"); - then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "customNameInvokeAsynchronousLogic"); - }); - } - - @AfterEach - public void cleanTrace() { - this.spans.clear(); - } - - @EnableAutoConfiguration - @EnableAsync - @Configuration(proxyBeanMethods = false) - static class TraceAsyncITestConfiguration { - - @Bean - ClassPerformingAsyncLogic asyncClass(Tracer tracer) { - return new ClassPerformingAsyncLogic(tracer); - } - - @Bean - Sampler defaultSampler() { - return Sampler.ALWAYS_SAMPLE; - } - - @Bean - SpanHandler testSpanHandler() { - return new TestSpanHandler(); - } - - @Bean - Executor fooExecutor() { - return new SimpleAsyncTaskExecutor(); - } - - @Bean - Executor barExecutor() { - return new SimpleAsyncTaskExecutor(); - } - - } - - static class ClassPerformingAsyncLogic { - - private final Tracer tracer; - - AtomicReference span = new AtomicReference<>(); - - ClassPerformingAsyncLogic(Tracer tracer) { - this.tracer = tracer; - } - - @Async("fooExecutor") - public void invokeAsynchronousLogic() { - this.span.set(this.tracer.currentSpan()); - } - - @Async - @SpanName("foo") - public void customNameInvokeAsynchronousLogic() { - this.span.set(this.tracer.currentSpan()); - } - - public Span getSpan() { - return this.span.get(); - } - - public void clear() { - this.span.set(null); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.web; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import brave.Span; +import brave.Tracer; +import brave.handler.MutableSpan; +import brave.handler.SpanHandler; +import brave.sampler.Sampler; +import brave.test.TestSpanHandler; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.SpanName; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.BDDAssertions.then; + +@SpringBootTest(classes = { TraceAsyncIntegrationTests.TraceAsyncITestConfiguration.class }) +public class TraceAsyncIntegrationTests { + + @Autowired + ClassPerformingAsyncLogic classPerformingAsyncLogic; + + @Autowired + Tracer tracer; + + @Autowired + TestSpanHandler spans; + + @BeforeEach + public void cleanup() { + this.spans.clear(); + this.classPerformingAsyncLogic.clear(); + } + + @Test + public void should_set_span_on_an_async_annotated_method() { + whenAsyncProcessingTakesPlace(); + + thenANewAsyncSpanGetsCreated(); + } + + @Test + public void should_set_span_with_custom_method_on_an_async_annotated_method() { + whenAsyncProcessingTakesPlaceWithCustomSpanName(); + + thenAsyncSpanHasCustomName(); + } + + @Test + public void should_continue_a_span_on_an_async_annotated_method() { + Span span = givenASpanInCurrentThread(); + + try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { + whenAsyncProcessingTakesPlace(); + } + finally { + span.finish(); + } + + thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(span); + } + + @Test + public void should_continue_a_span_with_custom_method_on_an_async_annotated_method() { + Span span = givenASpanInCurrentThread(); + + try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { + whenAsyncProcessingTakesPlaceWithCustomSpanName(); + } + finally { + span.finish(); + } + + thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(span); + } + + private Span givenASpanInCurrentThread() { + return this.tracer.nextSpan().name("http:existing"); + } + + private void whenAsyncProcessingTakesPlace() { + this.classPerformingAsyncLogic.invokeAsynchronousLogic(); + } + + private void whenAsyncProcessingTakesPlaceWithCustomSpanName() { + this.classPerformingAsyncLogic.customNameInvokeAsynchronousLogic(); + } + + private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(final Span span) { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) + .isEqualTo(span.context().traceId()); + List webSpans = this.spans.spans().stream().filter(mutableSpan -> mutableSpan.traceId().equalsIgnoreCase(span.context().traceIdString())) + .collect(Collectors.toList()); + then(webSpans).hasSize(2); + // HTTP + then(webSpans.get(0).name()).isEqualTo("http:existing"); + // ASYNC + then(webSpans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "invokeAsynchronousLogic"); + }); + } + + private void thenANewAsyncSpanGetsCreated() { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(this.spans).hasSize(1); + MutableSpan storedSpan = this.spans.get(0); + then(storedSpan.name()).isEqualTo("invoke-asynchronous-logic"); + then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "invokeAsynchronousLogic"); + }); + } + + private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(final Span span) { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan()).isNotNull(); + then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) + .isEqualTo(span.context().traceId()); + then(this.spans).hasSize(2); + // HTTP + then(this.spans.get(0).name()).isEqualTo("http:existing"); + // ASYNC + then(this.spans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "customNameInvokeAsynchronousLogic"); + }); + } + + private void thenAsyncSpanHasCustomName() { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(this.spans).hasSize(1); + MutableSpan storedSpan = this.spans.get(0); + then(storedSpan.name()).isEqualTo("foo"); + then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "customNameInvokeAsynchronousLogic"); + }); + } + + @AfterEach + public void cleanTrace() { + this.spans.clear(); + } + + @EnableAutoConfiguration + @EnableAsync + @Configuration(proxyBeanMethods = false) + static class TraceAsyncITestConfiguration { + + @Bean + ClassPerformingAsyncLogic asyncClass(Tracer tracer) { + return new ClassPerformingAsyncLogic(tracer); + } + + @Bean + Sampler defaultSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + SpanHandler testSpanHandler() { + return new TestSpanHandler(); + } + + @Bean + Executor fooExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + @Bean + Executor barExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + } + + static class ClassPerformingAsyncLogic { + + private final Tracer tracer; + + AtomicReference span = new AtomicReference<>(); + + ClassPerformingAsyncLogic(Tracer tracer) { + this.tracer = tracer; + } + + @Async("fooExecutor") + public void invokeAsynchronousLogic() { + this.span.set(this.tracer.currentSpan()); + } + + @Async + @SpanName("foo") + public void customNameInvokeAsynchronousLogic() { + this.span.set(this.tracer.currentSpan()); + } + + public Span getSpan() { + return this.span.get(); + } + + public void clear() { + this.span.set(null); + } + + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/R2dbcIntegrationTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/R2dbcIntegrationTests.java index 7844ae8ba..7ad6008b8 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/R2dbcIntegrationTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/R2dbcIntegrationTests.java @@ -1,102 +1,102 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.r2dbc; - -import java.time.Duration; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import io.r2dbc.spi.ConnectionFactory; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.cloud.sleuth.exporter.FinishedSpan; -import org.springframework.cloud.sleuth.test.TestSpanHandler; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ClassPathResource; -import org.springframework.dao.DataAccessException; -import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer; -import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; - -import static org.assertj.core.api.BDDAssertions.then; - -@ContextConfiguration(classes = R2dbcIntegrationTests.TestConfig.class) -@TestPropertySource(properties = { "spring.application.name=MyApplication", "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" }) -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) -public abstract class R2dbcIntegrationTests { - - @Autowired - TestSpanHandler spans; - - @Test - public void should_pass_tracing_information_when_using_r2dbc() { - Set traceIds = this.spans.reportedSpans().stream().map(FinishedSpan::getTraceId) - .collect(Collectors.toSet()); - then(traceIds).as("There's one traceid").hasSize(1); - Set spanIds = this.spans.reportedSpans().stream().map(FinishedSpan::getSpanId) - .collect(Collectors.toSet()); - - // 2 transactions - 9 database interactions - then(spanIds).as("There are 11 spans").hasSize(11); - List spanNames = this.spans.reportedSpans().stream().map(FinishedSpan::getName) - .collect(Collectors.toList()); - List remoteServiceNames = this.spans.reportedSpans().stream().map(FinishedSpan::getRemoteServiceName) - .collect(Collectors.toList()); - then(spanNames.stream().filter("tx"::equalsIgnoreCase).collect(Collectors.toList())).hasSize(2); - then(remoteServiceNames.stream().filter("h2"::equalsIgnoreCase).collect(Collectors.toList())).hasSize(9); - } - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration - @ComponentScan - public static class TestConfig { - - private static final Logger log = LoggerFactory.getLogger(TestConfig.class); - - @Bean - public CommandLineRunner demo(ReactiveNewTransactionService reactiveNewTransactionService) { - return (args) -> { - try { - reactiveNewTransactionService.newTransaction().block(Duration.ofSeconds(50)); - } - catch (DataAccessException e) { - log.info("Expected to throw an exception so that we see if rollback works", e); - } - }; - } - - @Bean - ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) { - ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer(); - initializer.setConnectionFactory(connectionFactory); - initializer.setDatabasePopulator(new ResourceDatabasePopulator(new ClassPathResource("schema.sql"))); - return initializer; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.r2dbc; + +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import io.r2dbc.spi.ConnectionFactory; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.dao.DataAccessException; +import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer; +import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.BDDAssertions.then; + +@ContextConfiguration(classes = R2dbcIntegrationTests.TestConfig.class) +@TestPropertySource(properties = { "spring.application.name=MyApplication", "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" }) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) +public abstract class R2dbcIntegrationTests { + + @Autowired + TestSpanHandler spans; + + @Test + public void should_pass_tracing_information_when_using_r2dbc() { + Set traceIds = this.spans.reportedSpans().stream().map(FinishedSpan::getTraceId) + .collect(Collectors.toSet()); + then(traceIds).as("There's one traceid").hasSize(1); + Set spanIds = this.spans.reportedSpans().stream().map(FinishedSpan::getSpanId) + .collect(Collectors.toSet()); + + // 2 transactions - 9 database interactions + then(spanIds).as("There are 11 spans").hasSize(11); + List spanNames = this.spans.reportedSpans().stream().map(FinishedSpan::getName) + .collect(Collectors.toList()); + List remoteServiceNames = this.spans.reportedSpans().stream().map(FinishedSpan::getRemoteServiceName) + .collect(Collectors.toList()); + then(spanNames.stream().filter("tx"::equalsIgnoreCase).collect(Collectors.toList())).hasSize(2); + then(remoteServiceNames.stream().filter("h2"::equalsIgnoreCase).collect(Collectors.toList())).hasSize(9); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @ComponentScan + public static class TestConfig { + + private static final Logger log = LoggerFactory.getLogger(TestConfig.class); + + @Bean + public CommandLineRunner demo(ReactiveNewTransactionService reactiveNewTransactionService) { + return (args) -> { + try { + reactiveNewTransactionService.newTransaction().block(Duration.ofSeconds(50)); + } + catch (DataAccessException e) { + log.info("Expected to throw an exception so that we see if rollback works", e); + } + }; + } + + @Bean + ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) { + ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer(); + initializer.setConnectionFactory(connectionFactory); + initializer.setDatabasePopulator(new ResourceDatabasePopulator(new ClassPathResource("schema.sql"))); + return initializer; + } + + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/integration/sampled/WebClientTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/integration/sampled/WebClientTests.java index 618c83d0a..7af97c32b 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/integration/sampled/WebClientTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/web/client/integration/sampled/WebClientTests.java @@ -1,597 +1,597 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.web.client.integration.sampled; - -import java.time.Duration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import javax.servlet.http.HttpServletRequest; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.reactivestreams.Subscription; -import reactor.core.publisher.BaseSubscriber; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.boot.web.client.RestTemplateCustomizer; -import org.springframework.boot.web.server.LocalServerPort; -import org.springframework.boot.web.servlet.error.ErrorAttributes; -import org.springframework.cloud.client.DefaultServiceInstance; -import org.springframework.cloud.client.loadbalancer.LoadBalanced; -import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient; -import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; -import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers; -import org.springframework.cloud.openfeign.EnableFeignClients; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.exporter.FinishedSpan; -import org.springframework.cloud.sleuth.test.TestSpanHandler; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseEntity; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.TestPropertySource; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.reactive.function.client.UnknownHttpStatusCodeException; -import org.springframework.web.reactive.function.client.WebClient; - -import static org.assertj.core.api.Assertions.fail; -import static org.assertj.core.api.BDDAssertions.then; - -@ContextConfiguration(classes = WebClientTests.TestConfiguration.class) -@TestPropertySource(properties = { "spring.sleuth.web.servlet.enabled=false", "spring.application.name=fooservice", - "spring.sleuth.web.client.skip-pattern=/skip.*" }) -@DirtiesContext -public abstract class WebClientTests { - - private static final Log log = LogFactory.getLog(WebClientTests.class); - - @Autowired - TestFeignInterface testFeignInterface; - - @Autowired - @LoadBalanced - RestTemplate template; - - @Autowired - WebClient webClient; - - @Autowired - WebClient.Builder webClientBuilder; - - @Autowired - TestSpanHandler spans; - - @Autowired - Tracer tracer; - - @Autowired - TestErrorController testErrorController; - - @Autowired - RestTemplateBuilder restTemplateBuilder; - - @LocalServerPort - int port; - - @Autowired - FooController fooController; - - @Autowired - MyRestTemplateCustomizer customizer; - - @AfterEach - @BeforeEach - public void close() { - this.spans.clear(); - this.testErrorController.clear(); - this.fooController.clear(); - } - - @BeforeEach - public void setup() { - log.info("Starting test"); - } - - @ParameterizedTest - @MethodSource("parametersForShouldCreateANewSpanWithClientSideTagsWhenNoPreviousTracingWasPresent") - @SuppressWarnings("unchecked") - public void shouldCreateANewSpanWithClientSideTagsWhenNoPreviousTracingWasPresent(ResponseEntityProvider provider) { - ResponseEntity response = provider.get(this); - - Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> { - then(getHeader(response, "b3")).isNull(); - then(this.spans).isNotEmpty(); - Optional noTraceSpan = this.spans.reportedSpans().stream() - .filter(span -> span.getName().contains("GET") && !span.getTags().isEmpty() - && span.getTags().containsKey(pathKey())) - .findFirst(); - then(noTraceSpan.isPresent()).isTrue(); - then(noTraceSpan.get().getTags()).containsEntry(pathKey(), "/notrace").containsEntry("http.method", "GET"); - // TODO: matches cause there is an issue with Feign not providing the full URL - // at the interceptor level - then(noTraceSpan.get().getTags().get(pathKey())).matches(".*/notrace"); - }); - thenThereIsNoCurrentSpan(); - } - - protected String pathKey() { - return "http.path"; - } - - private void thenThereIsNoCurrentSpan() { - log.info("Current span [" + this.tracer.currentSpan() + "]"); - then(this.tracer.currentSpan()).isNull(); - } - - static Stream parametersForShouldCreateANewSpanWithClientSideTagsWhenNoPreviousTracingWasPresent() { - return Stream.of((ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), - (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), - (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), - (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), - (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), - (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), - (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), - (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), - (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", - String.class), - (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", - String.class), - (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", - String.class), - (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", - String.class), - (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", - String.class), - (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", - String.class), - (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", - String.class), - (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", - String.class)); - } - - @ParameterizedTest - @MethodSource("parametersForShouldAttachTraceIdWhenCallingAnotherService") - @SuppressWarnings("unchecked") - public void shouldAttachTraceIdWhenCallingAnotherService(ResponseEntityProvider provider) { - Span span = this.tracer.nextSpan().name("foo").start(); - - try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { - ResponseEntity response = provider.get(this); - - // https://github.com/spring-cloud/spring-cloud-sleuth/issues/327 - // we don't want to respond with any tracing data - then(getHeader(response, "b3")).isNull(); - } - finally { - span.end(); - } - - thenThereIsNoCurrentSpan(); - then(this.spans).isNotEmpty(); - } - - @Test - @SuppressWarnings("unchecked") - public void shouldAttachTraceIdWhenCallingAnotherServiceViaWebClient() { - Span span = this.tracer.nextSpan().name("foo").start(); - - try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { - this.webClient.get().uri("http://localhost:" + this.port + "/traceid").retrieve().bodyToMono(String.class) - .block(Duration.ofSeconds(5)); - } - finally { - span.end(); - } - thenThereIsNoCurrentSpan(); - then(this.spans.reportedSpans().stream().filter(r -> r.getKind() != null).map(r -> r.getKind().name()) - .collect(Collectors.toList())).isNotEmpty().contains("CLIENT"); - } - - @Test - @SuppressWarnings("unchecked") - public void shouldWorkWhenCustomStatusCodeIsReturned() { - Span span = this.tracer.nextSpan().name("foo").start(); - - try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { - this.webClient.get().uri("http://localhost:" + this.port + "/issue1462").retrieve().bodyToMono(String.class) - .block(Duration.ofSeconds(5)); - } - catch (UnknownHttpStatusCodeException ex) { - - } - finally { - span.end(); - } - - thenThereIsNoCurrentSpan(); - then(this.spans.reportedSpans().stream().filter(r -> r.getKind() != null).map(r -> r.getKind().name()) - .collect(Collectors.toList())).isNotEmpty().contains("CLIENT"); - } - - @Test - @SuppressWarnings("unchecked") - public void shouldUseUriTemplateInSpanName() { - Span span = this.tracer.nextSpan().name("foo").start(); - - try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { - this.webClientBuilder.baseUrl("http://localhost:" + this.port).build().get() - .uri("/prefix/{variable}/suffix", "value").retrieve().bodyToMono(String.class) - .block(Duration.ofSeconds(5)); - } - finally { - span.end(); - } - - thenThereIsNoCurrentSpan(); - then(this.spans.reportedSpans().stream().filter(r -> r.getKind() == Span.Kind.CLIENT).map(r -> r.getName()) - .collect(Collectors.toList())).isNotEmpty().contains(templatedName()); - } - - protected String templatedName() { - return "GET /prefix/{variable}/suffix"; - } - - /** - * Cancel before {@link Subscription#request(long)} means a network request was never - * sent - */ - @Test - @Disabled("flakey") - public void shouldNotTagOnCancel() { - this.webClient.get().uri("http://localhost:" + this.port + "/doNotSkip").retrieve().bodyToMono(String.class) - .subscribe(new BaseSubscriber() { - @Override - protected void hookOnSubscribe(Subscription subscription) { - cancel(); - } - }); - - then(this.spans).isEmpty(); - } - - @Test - public void shouldRespectSkipPattern() { - this.webClient.get().uri("http://localhost:" + this.port + "/skip").retrieve().bodyToMono(String.class) - .block(Duration.ofSeconds(5)); - then(this.spans).isEmpty(); - - this.webClient.get().uri("http://localhost:" + this.port + "/doNotSkip").retrieve().bodyToMono(String.class) - .block(Duration.ofSeconds(5)); - then(this.spans).isNotEmpty(); - } - - static Stream parametersForShouldAttachTraceIdWhenCallingAnotherService() { - return Stream.of((ResponseEntityProvider) (tests) -> tests.testFeignInterface.headers(), - (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/traceid", - String.class)); - } - - @ParameterizedTest - @MethodSource("parametersForShouldAttachTraceIdWhenUsingFeignClientWithoutResponseBody") - public void shouldAttachTraceIdWhenUsingFeignClientWithoutResponseBody(ResponseEntityProvider provider) { - Span span = this.tracer.nextSpan().name("foo").start(); - - try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { - provider.get(this); - } - finally { - span.end(); - } - - thenThereIsNoCurrentSpan(); - then(this.spans).isNotEmpty(); - } - - static Stream parametersForShouldAttachTraceIdWhenUsingFeignClientWithoutResponseBody() { - return Stream.of((ResponseEntityProvider) (tests) -> tests.testFeignInterface.noResponseBody(), - (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/noresponse", - String.class)); - } - - @Test - public void shouldCloseSpanWhenErrorControllerGetsCalled() { - try { - this.template.getForEntity("http://fooservice/nonExistent", String.class); - fail("An exception should be thrown"); - } - catch (HttpClientErrorException e) { - } - - thenThereIsNoCurrentSpan(); - Optional storedSpan = this.spans.reportedSpans().stream() - .filter(span -> "404".equals(span.getTags().get("http.status_code"))).findFirst(); - then(storedSpan.isPresent()).isTrue(); - this.spans.reportedSpans().stream().forEach(span -> { - int initialSize = span.getEvents().size(); - int distinctSize = span.getEvents().stream().map(Map.Entry::getValue).distinct() - .collect(Collectors.toList()).size(); - log.info("logs " + span.getEvents()); - then(initialSize).as("there are no duplicate log entries").isEqualTo(distinctSize); - }); - - then(this.spans.reportedSpans().stream().filter(r -> r.getKind() != null).map(r -> r.getKind().name()) - .collect(Collectors.toList())).isNotEmpty().contains("CLIENT"); - } - - @Test - public void shouldNotExecuteErrorControllerWhenUrlIsFound() { - this.template.getForEntity("http://fooservice/notrace", String.class); - - thenThereIsNoCurrentSpan(); - then(this.testErrorController.getSpan()).isNull(); - } - - @Test - public void should_wrap_rest_template_builders() { - Span span = this.tracer.nextSpan().name("foo").start(); - - try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { - RestTemplate template = this.restTemplateBuilder.build(); - - template.getForObject("http://localhost:" + this.port + "/traceid", String.class); - } - finally { - span.end(); - } - thenThereIsNoCurrentSpan(); - then(this.customizer.isExecuted()).isTrue(); - then(this.spans.reportedSpans().stream().filter(s -> s.getKind() != null).map(s -> s.getKind().name()) - .collect(Collectors.toList())).contains("CLIENT"); - } - - @Test - public void should_add_headers_eagerly() { - Span span = this.tracer.nextSpan().name("foo").start(); - - AtomicReference traceId = new AtomicReference<>(); - try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { - this.webClientBuilder.filter((request, exchange) -> { - traceId.set(request.headers().getFirst("b3")); - - return exchange.exchange(request); - }).build().get().uri("http://localhost:" + this.port + "/traceid").retrieve().bodyToMono(String.class) - .block(Duration.ofSeconds(5)); - } - finally { - span.end(); - } - then(traceId).doesNotHaveValue(null); - } - - private String getHeader(ResponseEntity response, String name) { - List headers = response.getHeaders().get(name); - return headers == null || headers.isEmpty() ? null : headers.get(0); - } - - @FeignClient("fooservice") - public interface TestFeignInterface { - - @RequestMapping(method = RequestMethod.GET, value = "/traceid") - ResponseEntity getTraceId(); - - @RequestMapping(method = RequestMethod.GET, value = "/notrace") - ResponseEntity getNoTrace(); - - @RequestMapping(method = RequestMethod.GET, value = "/") - ResponseEntity> headers(); - - @RequestMapping(method = RequestMethod.GET, value = "/noresponse") - ResponseEntity noResponseBody(); - - } - - @FunctionalInterface - interface ResponseEntityProvider { - - @SuppressWarnings("rawtypes") - ResponseEntity get(WebClientTests webClientTests); - - } - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration(exclude = JmxAutoConfiguration.class) - @EnableFeignClients - @LoadBalancerClient(value = "fooservice", configuration = SimpleLoadBalancerClientConfiguration.class) - public static class TestConfiguration { - - @Bean - FooController fooController() { - return new FooController(); - } - - @Bean - WebClientController webClientController() { - return new WebClientController(); - } - - @LoadBalanced - @Bean - public RestTemplate restTemplate() { - return new RestTemplate(); - } - - @Bean - TestErrorController testErrorController(ErrorAttributes errorAttributes, Tracer tracer) { - return new TestErrorController(errorAttributes, tracer); - } - - @Bean - WebClient webClient() { - return WebClient.builder().build(); - } - - @Bean - WebClient.Builder webClientBuilder() { - return WebClient.builder(); - } - - @Bean - RestTemplateCustomizer myRestTemplateCustomizer() { - return new MyRestTemplateCustomizer(); - } - - } - - static class MyRestTemplateCustomizer implements RestTemplateCustomizer { - - boolean executed; - - @Override - public void customize(RestTemplate restTemplate) { - this.executed = true; - } - - public boolean isExecuted() { - return this.executed; - } - - } - - public static class TestErrorController extends BasicErrorController { - - private final Tracer tracer; - - Span span; - - public TestErrorController(ErrorAttributes errorAttributes, Tracer tracer) { - super(errorAttributes, new ServerProperties().getError()); - this.tracer = tracer; - } - - @Override - public ResponseEntity> error(HttpServletRequest request) { - this.span = this.tracer.currentSpan(); - return super.error(request); - } - - public Span getSpan() { - return this.span; - } - - public void clear() { - this.span = null; - } - - } - - @RestController - public static class FooController { - - Span span; - - @RequestMapping(value = "/notrace", method = RequestMethod.GET) - public String notrace(@RequestHeader(name = "b3", required = false) String b3Single) { - then(b3Single).isNotNull(); - return "OK"; - } - - @RequestMapping(value = "/traceid", method = RequestMethod.GET) - public String traceId(@RequestHeader("b3") String b3Single) { - then(b3Single).isNotEmpty(); - return b3Single; - } - - @RequestMapping("/") - public Map home(@RequestHeader HttpHeaders headers) { - Map map = new HashMap<>(); - for (String key : headers.keySet()) { - map.put(key, headers.getFirst(key)); - } - return map; - } - - @RequestMapping("/noresponse") - public void noResponse(@RequestHeader("b3") String b3Single) { - then(b3Single).isNotEmpty(); - } - - public Span getSpan() { - return this.span; - } - - public void clear() { - this.span = null; - } - - } - - @RestController - public static class WebClientController { - - @RequestMapping(value = "/issue1462", method = RequestMethod.GET) - public ResponseEntity issue1462() { - return ResponseEntity.status(499).body("issue1462"); - } - - @RequestMapping(value = { "/skip", "/doNotSkip" }, method = RequestMethod.GET) - String skip() { - return "ok"; - } - - @RequestMapping(value = "/prefix/{variable}/suffix", method = RequestMethod.GET) - String pathVariable(@PathVariable("variable") String variable) { - return "variable = " + variable; - } - - } - - @Configuration(proxyBeanMethods = false) - public static class SimpleLoadBalancerClientConfiguration { - - @Value("${local.server.port}") - private int port = 0; - - @Bean - public ServiceInstanceListSupplier serviceInstanceListSupplier() { - return ServiceInstanceListSuppliers.from("fooservice", - new DefaultServiceInstance("fooservice" + "-1", "fooservice", "localhost", port, false)); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.web.client.integration.sampled; + +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.reactivestreams.Subscription; +import reactor.core.publisher.BaseSubscriber; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalanced; +import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.UnknownHttpStatusCodeException; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.BDDAssertions.then; + +@ContextConfiguration(classes = WebClientTests.TestConfiguration.class) +@TestPropertySource(properties = { "spring.sleuth.web.servlet.enabled=false", "spring.application.name=fooservice", + "spring.sleuth.web.client.skip-pattern=/skip.*" }) +@DirtiesContext +public abstract class WebClientTests { + + private static final Log log = LogFactory.getLog(WebClientTests.class); + + @Autowired + TestFeignInterface testFeignInterface; + + @Autowired + @LoadBalanced + RestTemplate template; + + @Autowired + WebClient webClient; + + @Autowired + WebClient.Builder webClientBuilder; + + @Autowired + TestSpanHandler spans; + + @Autowired + Tracer tracer; + + @Autowired + TestErrorController testErrorController; + + @Autowired + RestTemplateBuilder restTemplateBuilder; + + @LocalServerPort + int port; + + @Autowired + FooController fooController; + + @Autowired + MyRestTemplateCustomizer customizer; + + @AfterEach + @BeforeEach + public void close() { + this.spans.clear(); + this.testErrorController.clear(); + this.fooController.clear(); + } + + @BeforeEach + public void setup() { + log.info("Starting test"); + } + + @ParameterizedTest + @MethodSource("parametersForShouldCreateANewSpanWithClientSideTagsWhenNoPreviousTracingWasPresent") + @SuppressWarnings("unchecked") + public void shouldCreateANewSpanWithClientSideTagsWhenNoPreviousTracingWasPresent(ResponseEntityProvider provider) { + ResponseEntity response = provider.get(this); + + Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> { + then(getHeader(response, "b3")).isNull(); + then(this.spans).isNotEmpty(); + Optional noTraceSpan = this.spans.reportedSpans().stream() + .filter(span -> span.getName().contains("GET") && !span.getTags().isEmpty() + && span.getTags().containsKey(pathKey())) + .findFirst(); + then(noTraceSpan.isPresent()).isTrue(); + then(noTraceSpan.get().getTags()).containsEntry(pathKey(), "/notrace").containsEntry("http.method", "GET"); + // TODO: matches cause there is an issue with Feign not providing the full URL + // at the interceptor level + then(noTraceSpan.get().getTags().get(pathKey())).matches(".*/notrace"); + }); + thenThereIsNoCurrentSpan(); + } + + protected String pathKey() { + return "http.path"; + } + + private void thenThereIsNoCurrentSpan() { + log.info("Current span [" + this.tracer.currentSpan() + "]"); + then(this.tracer.currentSpan()).isNull(); + } + + static Stream parametersForShouldCreateANewSpanWithClientSideTagsWhenNoPreviousTracingWasPresent() { + return Stream.of((ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), + (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), + (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), + (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), + (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), + (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), + (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), + (ResponseEntityProvider) (tests) -> tests.testFeignInterface.getNoTrace(), + (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", + String.class), + (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", + String.class), + (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", + String.class), + (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", + String.class), + (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", + String.class), + (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", + String.class), + (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", + String.class), + (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/notrace", + String.class)); + } + + @ParameterizedTest + @MethodSource("parametersForShouldAttachTraceIdWhenCallingAnotherService") + @SuppressWarnings("unchecked") + public void shouldAttachTraceIdWhenCallingAnotherService(ResponseEntityProvider provider) { + Span span = this.tracer.nextSpan().name("foo").start(); + + try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { + ResponseEntity response = provider.get(this); + + // https://github.com/spring-cloud/spring-cloud-sleuth/issues/327 + // we don't want to respond with any tracing data + then(getHeader(response, "b3")).isNull(); + } + finally { + span.end(); + } + + thenThereIsNoCurrentSpan(); + then(this.spans).isNotEmpty(); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldAttachTraceIdWhenCallingAnotherServiceViaWebClient() { + Span span = this.tracer.nextSpan().name("foo").start(); + + try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { + this.webClient.get().uri("http://localhost:" + this.port + "/traceid").retrieve().bodyToMono(String.class) + .block(Duration.ofSeconds(5)); + } + finally { + span.end(); + } + thenThereIsNoCurrentSpan(); + then(this.spans.reportedSpans().stream().filter(r -> r.getKind() != null).map(r -> r.getKind().name()) + .collect(Collectors.toList())).isNotEmpty().contains("CLIENT"); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldWorkWhenCustomStatusCodeIsReturned() { + Span span = this.tracer.nextSpan().name("foo").start(); + + try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { + this.webClient.get().uri("http://localhost:" + this.port + "/issue1462").retrieve().bodyToMono(String.class) + .block(Duration.ofSeconds(5)); + } + catch (UnknownHttpStatusCodeException ex) { + + } + finally { + span.end(); + } + + thenThereIsNoCurrentSpan(); + then(this.spans.reportedSpans().stream().filter(r -> r.getKind() != null).map(r -> r.getKind().name()) + .collect(Collectors.toList())).isNotEmpty().contains("CLIENT"); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldUseUriTemplateInSpanName() { + Span span = this.tracer.nextSpan().name("foo").start(); + + try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { + this.webClientBuilder.baseUrl("http://localhost:" + this.port).build().get() + .uri("/prefix/{variable}/suffix", "value").retrieve().bodyToMono(String.class) + .block(Duration.ofSeconds(5)); + } + finally { + span.end(); + } + + thenThereIsNoCurrentSpan(); + then(this.spans.reportedSpans().stream().filter(r -> r.getKind() == Span.Kind.CLIENT).map(r -> r.getName()) + .collect(Collectors.toList())).isNotEmpty().contains(templatedName()); + } + + protected String templatedName() { + return "GET /prefix/{variable}/suffix"; + } + + /** + * Cancel before {@link Subscription#request(long)} means a network request was never + * sent + */ + @Test + @Disabled("flakey") + public void shouldNotTagOnCancel() { + this.webClient.get().uri("http://localhost:" + this.port + "/doNotSkip").retrieve().bodyToMono(String.class) + .subscribe(new BaseSubscriber() { + @Override + protected void hookOnSubscribe(Subscription subscription) { + cancel(); + } + }); + + then(this.spans).isEmpty(); + } + + @Test + public void shouldRespectSkipPattern() { + this.webClient.get().uri("http://localhost:" + this.port + "/skip").retrieve().bodyToMono(String.class) + .block(Duration.ofSeconds(5)); + then(this.spans).isEmpty(); + + this.webClient.get().uri("http://localhost:" + this.port + "/doNotSkip").retrieve().bodyToMono(String.class) + .block(Duration.ofSeconds(5)); + then(this.spans).isNotEmpty(); + } + + static Stream parametersForShouldAttachTraceIdWhenCallingAnotherService() { + return Stream.of((ResponseEntityProvider) (tests) -> tests.testFeignInterface.headers(), + (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/traceid", + String.class)); + } + + @ParameterizedTest + @MethodSource("parametersForShouldAttachTraceIdWhenUsingFeignClientWithoutResponseBody") + public void shouldAttachTraceIdWhenUsingFeignClientWithoutResponseBody(ResponseEntityProvider provider) { + Span span = this.tracer.nextSpan().name("foo").start(); + + try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { + provider.get(this); + } + finally { + span.end(); + } + + thenThereIsNoCurrentSpan(); + then(this.spans).isNotEmpty(); + } + + static Stream parametersForShouldAttachTraceIdWhenUsingFeignClientWithoutResponseBody() { + return Stream.of((ResponseEntityProvider) (tests) -> tests.testFeignInterface.noResponseBody(), + (ResponseEntityProvider) (tests) -> tests.template.getForEntity("http://fooservice/noresponse", + String.class)); + } + + @Test + public void shouldCloseSpanWhenErrorControllerGetsCalled() { + try { + this.template.getForEntity("http://fooservice/nonExistent", String.class); + fail("An exception should be thrown"); + } + catch (HttpClientErrorException e) { + } + + thenThereIsNoCurrentSpan(); + Optional storedSpan = this.spans.reportedSpans().stream() + .filter(span -> "404".equals(span.getTags().get("http.status_code"))).findFirst(); + then(storedSpan.isPresent()).isTrue(); + this.spans.reportedSpans().stream().forEach(span -> { + int initialSize = span.getEvents().size(); + int distinctSize = span.getEvents().stream().map(Map.Entry::getValue).distinct() + .collect(Collectors.toList()).size(); + log.info("logs " + span.getEvents()); + then(initialSize).as("there are no duplicate log entries").isEqualTo(distinctSize); + }); + + then(this.spans.reportedSpans().stream().filter(r -> r.getKind() != null).map(r -> r.getKind().name()) + .collect(Collectors.toList())).isNotEmpty().contains("CLIENT"); + } + + @Test + public void shouldNotExecuteErrorControllerWhenUrlIsFound() { + this.template.getForEntity("http://fooservice/notrace", String.class); + + thenThereIsNoCurrentSpan(); + then(this.testErrorController.getSpan()).isNull(); + } + + @Test + public void should_wrap_rest_template_builders() { + Span span = this.tracer.nextSpan().name("foo").start(); + + try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { + RestTemplate template = this.restTemplateBuilder.build(); + + template.getForObject("http://localhost:" + this.port + "/traceid", String.class); + } + finally { + span.end(); + } + thenThereIsNoCurrentSpan(); + then(this.customizer.isExecuted()).isTrue(); + then(this.spans.reportedSpans().stream().filter(s -> s.getKind() != null).map(s -> s.getKind().name()) + .collect(Collectors.toList())).contains("CLIENT"); + } + + @Test + public void should_add_headers_eagerly() { + Span span = this.tracer.nextSpan().name("foo").start(); + + AtomicReference traceId = new AtomicReference<>(); + try (Tracer.SpanInScope ws = this.tracer.withSpan(span)) { + this.webClientBuilder.filter((request, exchange) -> { + traceId.set(request.headers().getFirst("b3")); + + return exchange.exchange(request); + }).build().get().uri("http://localhost:" + this.port + "/traceid").retrieve().bodyToMono(String.class) + .block(Duration.ofSeconds(5)); + } + finally { + span.end(); + } + then(traceId).doesNotHaveValue(null); + } + + private String getHeader(ResponseEntity response, String name) { + List headers = response.getHeaders().get(name); + return headers == null || headers.isEmpty() ? null : headers.get(0); + } + + @FeignClient("fooservice") + public interface TestFeignInterface { + + @RequestMapping(method = RequestMethod.GET, value = "/traceid") + ResponseEntity getTraceId(); + + @RequestMapping(method = RequestMethod.GET, value = "/notrace") + ResponseEntity getNoTrace(); + + @RequestMapping(method = RequestMethod.GET, value = "/") + ResponseEntity> headers(); + + @RequestMapping(method = RequestMethod.GET, value = "/noresponse") + ResponseEntity noResponseBody(); + + } + + @FunctionalInterface + interface ResponseEntityProvider { + + @SuppressWarnings("rawtypes") + ResponseEntity get(WebClientTests webClientTests); + + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration(exclude = JmxAutoConfiguration.class) + @EnableFeignClients + @LoadBalancerClient(value = "fooservice", configuration = SimpleLoadBalancerClientConfiguration.class) + public static class TestConfiguration { + + @Bean + FooController fooController() { + return new FooController(); + } + + @Bean + WebClientController webClientController() { + return new WebClientController(); + } + + @LoadBalanced + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Bean + TestErrorController testErrorController(ErrorAttributes errorAttributes, Tracer tracer) { + return new TestErrorController(errorAttributes, tracer); + } + + @Bean + WebClient webClient() { + return WebClient.builder().build(); + } + + @Bean + WebClient.Builder webClientBuilder() { + return WebClient.builder(); + } + + @Bean + RestTemplateCustomizer myRestTemplateCustomizer() { + return new MyRestTemplateCustomizer(); + } + + } + + static class MyRestTemplateCustomizer implements RestTemplateCustomizer { + + boolean executed; + + @Override + public void customize(RestTemplate restTemplate) { + this.executed = true; + } + + public boolean isExecuted() { + return this.executed; + } + + } + + public static class TestErrorController extends BasicErrorController { + + private final Tracer tracer; + + Span span; + + public TestErrorController(ErrorAttributes errorAttributes, Tracer tracer) { + super(errorAttributes, new ServerProperties().getError()); + this.tracer = tracer; + } + + @Override + public ResponseEntity> error(HttpServletRequest request) { + this.span = this.tracer.currentSpan(); + return super.error(request); + } + + public Span getSpan() { + return this.span; + } + + public void clear() { + this.span = null; + } + + } + + @RestController + public static class FooController { + + Span span; + + @RequestMapping(value = "/notrace", method = RequestMethod.GET) + public String notrace(@RequestHeader(name = "b3", required = false) String b3Single) { + then(b3Single).isNotNull(); + return "OK"; + } + + @RequestMapping(value = "/traceid", method = RequestMethod.GET) + public String traceId(@RequestHeader("b3") String b3Single) { + then(b3Single).isNotEmpty(); + return b3Single; + } + + @RequestMapping("/") + public Map home(@RequestHeader HttpHeaders headers) { + Map map = new HashMap<>(); + for (String key : headers.keySet()) { + map.put(key, headers.getFirst(key)); + } + return map; + } + + @RequestMapping("/noresponse") + public void noResponse(@RequestHeader("b3") String b3Single) { + then(b3Single).isNotEmpty(); + } + + public Span getSpan() { + return this.span; + } + + public void clear() { + this.span = null; + } + + } + + @RestController + public static class WebClientController { + + @RequestMapping(value = "/issue1462", method = RequestMethod.GET) + public ResponseEntity issue1462() { + return ResponseEntity.status(499).body("issue1462"); + } + + @RequestMapping(value = { "/skip", "/doNotSkip" }, method = RequestMethod.GET) + String skip() { + return "ok"; + } + + @RequestMapping(value = "/prefix/{variable}/suffix", method = RequestMethod.GET) + String pathVariable(@PathVariable("variable") String variable) { + return "variable = " + variable; + } + + } + + @Configuration(proxyBeanMethods = false) + public static class SimpleLoadBalancerClientConfiguration { + + @Value("${local.server.port}") + private int port = 0; + + @Bean + public ServiceInstanceListSupplier serviceInstanceListSupplier() { + return ServiceInstanceListSuppliers.from("fooservice", + new DefaultServiceInstance("fooservice" + "-1", "fooservice", "localhost", port, false)); + } + + } + +} From 8991b4d3cd2eca75ab6b62d03d1096aa26f529e0 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 27 May 2021 13:41:44 +0200 Subject: [PATCH 39/59] Moved tests to common --- .../jdbc/DataSourceProxyConfiguration.java | 231 ++- .../instrument/jdbc/P6SpyConfiguration.java | 154 +- .../jdbc/P6SpyPropertiesSetter.java | 374 ++--- ...eDataSourceDecoratorBeanPostProcessor.java | 280 ++-- ...n.java => TraceJdbcAutoConfiguration.java} | 140 +- ...operties.java => TraceJdbcProperties.java} | 996 +++++------ .../main/resources/META-INF/spring.factories | 2 +- .../TraceNoOpAutoConfiguration.java | 192 ++- ...SourceDecoratorAutoConfigurationTests.java | 741 ++++---- .../jdbc/P6SpyConfigurationTests.java | 560 +++---- .../ProxyDataSourceConfigurationTests.java | 485 +++--- ...thP6SpyListenerAutoConfigurationTests.java | 133 +- ...aSourceListenerAutoConfigurationTests.java | 139 +- .../jdbc/TestSpanHandlerConfiguration.java | 38 - .../TraceListenerStrategySpanCustomizer.java | 86 +- .../sleuth/instrument/jdbc/TraceType.java | 78 +- tests/brave/pom.xml | 1 + .../pom.xml | 114 ++ .../jdbc/TracingJdbcEventListenerTests.java | 60 + .../TracingQueryExecutionListenerTests.java | 60 + .../src/test/resources/application.yml | 5 + .../instrument/web/view/Issue469Tests.java | 119 +- tests/common/pom.xml | 10 + .../jdbc/TracingJdbcEventListenerTests.java | 156 +- .../jdbc/TracingListenerStrategyTests.java | 1488 ++++++++--------- .../TracingQueryExecutionListenerTests.java | 77 +- 26 files changed, 3481 insertions(+), 3238 deletions(-) rename spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/{TraceDataSourceDecoratorAutoConfiguration.java => TraceJdbcAutoConfiguration.java} (91%) rename spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/{TraceDataSourceDecoratorProperties.java => TraceJdbcProperties.java} (80%) delete mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TestSpanHandlerConfiguration.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/pom.xml create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingJdbcEventListenerTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingQueryExecutionListenerTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/resources/application.yml rename {spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig => tests/common/src/main/java/org/springframework/cloud/sleuth}/instrument/jdbc/TracingJdbcEventListenerTests.java (81%) rename {spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig => tests/common/src/main/java/org/springframework/cloud/sleuth}/instrument/jdbc/TracingListenerStrategyTests.java (59%) rename {spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig => tests/common/src/main/java/org/springframework/cloud/sleuth}/instrument/jdbc/TracingQueryExecutionListenerTests.java (67%) diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java index 9775cb354..944e74fa7 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java @@ -1,119 +1,112 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.ArrayList; -import java.util.List; - -import net.ttddyy.dsproxy.listener.MethodExecutionListener; -import net.ttddyy.dsproxy.listener.QueryCountStrategy; -import net.ttddyy.dsproxy.listener.QueryExecutionListener; -import net.ttddyy.dsproxy.listener.SingleQueryCountHolder; -import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; -import net.ttddyy.dsproxy.proxy.ResultSetProxyLogicFactory; -import net.ttddyy.dsproxy.proxy.SimpleResultSetProxyLogicFactory; -import net.ttddyy.dsproxy.support.ProxyDataSource; -import net.ttddyy.dsproxy.transform.ParameterTransformer; -import net.ttddyy.dsproxy.transform.QueryTransformer; - -import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyBuilderCustomizer; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyDataSourceDecorator; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyProperties; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; -import org.springframework.context.annotation.Bean; - -/** - * Configuration for integration with datasource-proxy, allows to use define custom - * {@link QueryExecutionListener}, {@link ParameterTransformer} and - * {@link QueryTransformer}. - * - * @author Arthur Gavlyukovskiy - */ -@ConditionalOnClass(ProxyDataSource.class) -class DataSourceProxyConfiguration { - - @Bean - @ConditionalOnMissingBean - DataSourceProxyConnectionIdManagerProvider traceConnectionIdManagerProvider() { - return GlobalConnectionIdManager::new; - } - - @Bean - DataSourceProxyBuilderCustomizer proxyDataSourceBuilderConfigurer( - ObjectProvider queryCountStrategy, - ObjectProvider> listeners, - ObjectProvider> methodExecutionListeners, - ObjectProvider parameterTransformer, - ObjectProvider queryTransformer, - ObjectProvider resultSetProxyLogicFactory, - ObjectProvider dataSourceProxyConnectionIdManagerProvider, - TraceDataSourceDecoratorProperties dataSourceDecoratorProperties) { - return new DataSourceProxyBuilderCustomizer(queryCountStrategy.getIfAvailable(() -> null), - listeners.getIfAvailable(() -> null), methodExecutionListeners.getIfAvailable(() -> null), - parameterTransformer.getIfAvailable(() -> null), queryTransformer.getIfAvailable(() -> null), - resultSetProxyLogicFactory.getIfAvailable(() -> null), - dataSourceProxyConnectionIdManagerProvider.getIfAvailable(() -> null), - props(dataSourceDecoratorProperties)); - } - - private DataSourceProxyProperties props(TraceDataSourceDecoratorProperties dataSourceDecoratorProperties) { - TraceDataSourceDecoratorProperties.DataSourceProxyProperties originalProxy = dataSourceDecoratorProperties - .getDatasourceProxy(); - DataSourceProxyProperties props = new DataSourceProxyProperties(); - BeanUtils.copyProperties(originalProxy, props); - props.setLogging(DataSourceProxyProperties.DataSourceProxyLogging.valueOf(originalProxy.getLogging().name())); - return props; - } - - @Bean - DataSourceProxyDataSourceDecorator proxyDataSourceDecorator( - DataSourceProxyBuilderCustomizer dataSourceProxyBuilderCustomizer, - DataSourceNameResolver dataSourceNameResolver) { - return new DataSourceProxyDataSourceDecorator(dataSourceProxyBuilderCustomizer, dataSourceNameResolver); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(value = "spring.sleuth.jdbc.datasource-proxy.count-query", havingValue = "true") - QueryCountStrategy queryCountStrategy() { - return new SingleQueryCountHolder(); - } - - @Bean - TraceQueryExecutionListener traceQueryExecutionListener(Tracer tracer, - TraceDataSourceDecoratorProperties dataSourceDecoratorProperties, - ObjectProvider> customizers) { - return new TraceQueryExecutionListener(tracer, dataSourceDecoratorProperties.getIncludes(), - customizers.getIfAvailable(ArrayList::new)); - } - - @Bean - @ConditionalOnMissingBean - ResultSetProxyLogicFactory traceResultSetProxyLogicFactory() { - return new SimpleResultSetProxyLogicFactory(); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.ArrayList; +import java.util.List; + +import net.ttddyy.dsproxy.listener.MethodExecutionListener; +import net.ttddyy.dsproxy.listener.QueryCountStrategy; +import net.ttddyy.dsproxy.listener.QueryExecutionListener; +import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; +import net.ttddyy.dsproxy.proxy.ResultSetProxyLogicFactory; +import net.ttddyy.dsproxy.proxy.SimpleResultSetProxyLogicFactory; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import net.ttddyy.dsproxy.transform.ParameterTransformer; +import net.ttddyy.dsproxy.transform.QueryTransformer; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyBuilderCustomizer; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyDataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyProperties; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; +import org.springframework.context.annotation.Bean; + +/** + * Configuration for integration with datasource-proxy, allows to use define custom + * {@link QueryExecutionListener}, {@link ParameterTransformer} and + * {@link QueryTransformer}. + * + * @author Arthur Gavlyukovskiy + */ +@ConditionalOnClass(ProxyDataSource.class) +@ConditionalOnProperty(name = "spring.sleuth.jdbc.datasource-proxy.enabled", havingValue = "true", matchIfMissing = true) +class DataSourceProxyConfiguration { + + @Bean + @ConditionalOnMissingBean + DataSourceProxyConnectionIdManagerProvider traceConnectionIdManagerProvider() { + return GlobalConnectionIdManager::new; + } + + @Bean + DataSourceProxyBuilderCustomizer proxyDataSourceBuilderConfigurer( + ObjectProvider queryCountStrategy, + ObjectProvider> listeners, + ObjectProvider> methodExecutionListeners, + ObjectProvider parameterTransformer, + ObjectProvider queryTransformer, + ObjectProvider resultSetProxyLogicFactory, + ObjectProvider dataSourceProxyConnectionIdManagerProvider, + TraceJdbcProperties traceJdbcProperties) { + return new DataSourceProxyBuilderCustomizer(queryCountStrategy.getIfAvailable(() -> null), + listeners.getIfAvailable(() -> null), methodExecutionListeners.getIfAvailable(() -> null), + parameterTransformer.getIfAvailable(() -> null), queryTransformer.getIfAvailable(() -> null), + resultSetProxyLogicFactory.getIfAvailable(() -> null), + dataSourceProxyConnectionIdManagerProvider.getIfAvailable(() -> null), + props(traceJdbcProperties)); + } + + private DataSourceProxyProperties props(TraceJdbcProperties traceJdbcProperties) { + TraceJdbcProperties.DataSourceProxyProperties originalProxy = traceJdbcProperties + .getDatasourceProxy(); + DataSourceProxyProperties props = new DataSourceProxyProperties(); + BeanUtils.copyProperties(originalProxy, props); + props.setLogging(DataSourceProxyProperties.DataSourceProxyLogging.valueOf(originalProxy.getLogging().name())); + return props; + } + + @Bean + DataSourceProxyDataSourceDecorator proxyDataSourceDecorator( + DataSourceProxyBuilderCustomizer dataSourceProxyBuilderCustomizer, + DataSourceNameResolver dataSourceNameResolver) { + return new DataSourceProxyDataSourceDecorator(dataSourceProxyBuilderCustomizer, dataSourceNameResolver); + } + + @Bean + TraceQueryExecutionListener traceQueryExecutionListener(Tracer tracer, + TraceJdbcProperties dataSourceDecoratorProperties, + ObjectProvider> customizers) { + return new TraceQueryExecutionListener(tracer, dataSourceDecoratorProperties.getIncludes(), + customizers.getIfAvailable(ArrayList::new)); + } + + @Bean + @ConditionalOnMissingBean + ResultSetProxyLogicFactory traceResultSetProxyLogicFactory() { + return new SimpleResultSetProxyLogicFactory(); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfiguration.java index 1597e77d4..b38bd2d85 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfiguration.java @@ -1,76 +1,78 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.ArrayList; -import java.util.List; - -import com.p6spy.engine.event.JdbcEventListener; -import com.p6spy.engine.spy.DefaultJdbcEventListenerFactory; -import com.p6spy.engine.spy.JdbcEventListenerFactory; -import com.p6spy.engine.spy.P6DataSource; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; -import org.springframework.cloud.sleuth.instrument.jdbc.P6SpyContextJdbcEventListenerFactory; -import org.springframework.cloud.sleuth.instrument.jdbc.P6SpyDataSourceDecorator; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; - -/** - * Configuration for integration with p6spy, allows to define custom - * {@link JdbcEventListener}. - * - * @author Arthur Gavlyukovskiy - */ -@ConditionalOnClass(P6DataSource.class) -class P6SpyConfiguration { - - @Bean - static P6SpyPropertiesSetter p6SpyPropertiesSetter(ConfigurableApplicationContext context) { - return new P6SpyPropertiesSetter(context); - } - - @Bean - @ConditionalOnMissingBean - JdbcEventListenerFactory traceJdbcEventListenerFactory(ObjectProvider> listeners) { - JdbcEventListenerFactory jdbcEventListenerFactory = new DefaultJdbcEventListenerFactory(); - List listenerList = listeners.getIfAvailable(() -> null); - return listenerList != null ? new P6SpyContextJdbcEventListenerFactory(jdbcEventListenerFactory, listenerList) - : jdbcEventListenerFactory; - } - - @Bean - P6SpyDataSourceDecorator p6SpyDataSourceDecorator(JdbcEventListenerFactory jdbcEventListenerFactory) { - return new P6SpyDataSourceDecorator(jdbcEventListenerFactory); - } - - @Bean - TraceJdbcEventListener tracingJdbcEventListener(Tracer tracer, DataSourceNameResolver dataSourceNameResolver, - TraceDataSourceDecoratorProperties dataSourceDecoratorProperties, - ObjectProvider> customizers) { - return new TraceJdbcEventListener(tracer, dataSourceNameResolver, dataSourceDecoratorProperties.getIncludes(), - dataSourceDecoratorProperties.getP6spy().getTracing().isIncludeParameterValues(), - customizers.getIfAvailable(ArrayList::new)); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.ArrayList; +import java.util.List; + +import com.p6spy.engine.event.JdbcEventListener; +import com.p6spy.engine.spy.DefaultJdbcEventListenerFactory; +import com.p6spy.engine.spy.JdbcEventListenerFactory; +import com.p6spy.engine.spy.P6DataSource; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; +import org.springframework.cloud.sleuth.instrument.jdbc.P6SpyContextJdbcEventListenerFactory; +import org.springframework.cloud.sleuth.instrument.jdbc.P6SpyDataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; + +/** + * Configuration for integration with p6spy, allows to define custom + * {@link JdbcEventListener}. + * + * @author Arthur Gavlyukovskiy + */ +@ConditionalOnClass(P6DataSource.class) +@ConditionalOnProperty(name = "spring.sleuth.jdbc.p6spy.enabled", havingValue = "true", matchIfMissing = true) +class P6SpyConfiguration { + + @Bean + static P6SpyPropertiesSetter p6SpyPropertiesSetter(ConfigurableApplicationContext context) { + return new P6SpyPropertiesSetter(context); + } + + @Bean + @ConditionalOnMissingBean + JdbcEventListenerFactory traceJdbcEventListenerFactory(ObjectProvider> listeners) { + JdbcEventListenerFactory jdbcEventListenerFactory = new DefaultJdbcEventListenerFactory(); + List listenerList = listeners.getIfAvailable(() -> null); + return listenerList != null ? new P6SpyContextJdbcEventListenerFactory(jdbcEventListenerFactory, listenerList) + : jdbcEventListenerFactory; + } + + @Bean + P6SpyDataSourceDecorator p6SpyDataSourceDecorator(JdbcEventListenerFactory jdbcEventListenerFactory) { + return new P6SpyDataSourceDecorator(jdbcEventListenerFactory); + } + + @Bean + TraceJdbcEventListener tracingJdbcEventListener(Tracer tracer, DataSourceNameResolver dataSourceNameResolver, + TraceJdbcProperties traceJdbcProperties, + ObjectProvider> customizers) { + return new TraceJdbcEventListener(tracer, dataSourceNameResolver, traceJdbcProperties.getIncludes(), + traceJdbcProperties.getP6spy().getTracing().isIncludeParameterValues(), + customizers.getIfAvailable(ArrayList::new)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java index c0227ac10..f12826584 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java @@ -1,187 +1,187 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.io.Closeable; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import com.p6spy.engine.logging.P6LogFactory; -import com.p6spy.engine.spy.P6ModuleManager; -import com.p6spy.engine.spy.P6SpyFactory; -import com.p6spy.engine.spy.option.EnvironmentVariables; -import com.p6spy.engine.spy.option.P6OptionsSource; -import com.p6spy.engine.spy.option.SpyDotProperties; -import com.p6spy.engine.spy.option.SystemProperties; -import org.slf4j.Logger; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.env.ConfigurableEnvironment; - -import static org.slf4j.LoggerFactory.getLogger; - -/** - * Sets p6spy properties to / from system properties. - * - * @author Arthur Gavlyukovskiy - * @since 3.1.0 - */ -class P6SpyPropertiesSetter implements BeanDefinitionRegistryPostProcessor, Closeable { - - private static final Logger log = getLogger(P6SpyPropertiesSetter.class); - - private final ConfigurableApplicationContext context; - - private final Map initialP6SpyOptions; - - P6SpyPropertiesSetter(ConfigurableApplicationContext context) { - this.context = context; - this.initialP6SpyOptions = findDefinedOptions(); - } - - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - ConfigurableEnvironment environment = this.context.getEnvironment(); - String customModuleList = initialP6SpyOptions.get("modulelist"); - boolean isEnableLogging = environment.getProperty("spring.sleuth.jdbc.p6spy.enable-logging", Boolean.class, - true); - if (customModuleList != null) { - log.info("P6Spy modulelist is overridden, some p6spy configuration features will not be applied"); - } - else { - List moduleList = new ArrayList<>(); - // default factory, holds P6Spy configuration - moduleList.add(P6SpyFactory.class.getName()); - if (isEnableLogging) { - moduleList.add(P6LogFactory.class.getName()); - } - System.setProperty("p6spy.config.modulelist", String.join(",", moduleList)); - } - if (!initialP6SpyOptions.containsKey("logMessageFormat")) { - String logFormat = logFormat(environment); - boolean isMultiline = multiLine(environment); - if (logFormat != null) { - System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); - System.setProperty("p6spy.config.customLogMessageFormat", logFormat); - } - else if (isMultiline) { - System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.MultiLineFormat"); - } - } - if (isEnableLogging && !initialP6SpyOptions.containsKey("appender")) { - TraceDataSourceDecoratorProperties.P6SpyProperties.P6SpyLogging logging = TraceDataSourceDecoratorProperties.P6SpyProperties.P6SpyLogging - .valueOf(environment - .getProperty("spring.sleuth.jdbc.p6spy.logging", String.class, - TraceDataSourceDecoratorProperties.P6SpyProperties.P6SpyLogging.SLF4J.toString()) - .toUpperCase()); - switch (logging) { - case SYSOUT: - System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.StdoutLogger"); - break; - case SLF4J: - System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.Slf4JLogger"); - break; - case FILE: - System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.FileLogger"); - break; - case CUSTOM: - String customAppender = environment.getProperty("spring.sleuth.jdbc.p6spy.custom-appender-class", - String.class, ""); - System.setProperty("p6spy.config.appender", customAppender); - break; - } - } - if (!initialP6SpyOptions.containsKey("logfile")) { - String logFile = environment.getProperty("spring.sleuth.jdbc.p6spy.log-file", String.class, "spy.log"); - System.setProperty("p6spy.config.logfile", logFile); - } - String pattern = environment.getProperty("spring.sleuth.jdbc.p6spy.log-filter.pattern", String.class); - if (pattern != null) { - System.setProperty("p6spy.config.filter", "true"); - System.setProperty("p6spy.config.sqlexpression", pattern); - } - // If factories were loaded before this method is initialized changing properties - // will not be applied - // Changes done in this method could not override anything user specified, - // therefore it is safe to call reload - P6ModuleManager.getInstance().reload(); - } - - private Boolean multiLine(ConfigurableEnvironment environment) { - return environment.getProperty("spring.sleuth.jdbc.p6spy.multiline", Boolean.class, true); - } - - private String logFormat(ConfigurableEnvironment environment) { - return environment.getProperty("spring.sleuth.jdbc.p6spy.log-format", String.class); - } - - @Override - public void close() throws IOException { - if (!initialP6SpyOptions.containsKey("modulelist")) { - System.clearProperty("p6spy.config.modulelist"); - } - if (!initialP6SpyOptions.containsKey("logMessageFormat")) { - ConfigurableEnvironment environment = this.context.getEnvironment(); - String logFormat = logFormat(environment); - boolean isMultiline = multiLine(environment); - if (logFormat != null) { - System.clearProperty("p6spy.config.logMessageFormat"); - System.clearProperty("p6spy.config.customLogMessageFormat"); - } - else if (isMultiline) { - System.clearProperty("p6spy.config.logMessageFormat"); - } - } - if (!initialP6SpyOptions.containsKey("appender")) { - System.clearProperty("p6spy.config.appender"); - } - if (!initialP6SpyOptions.containsKey("logfile")) { - System.clearProperty("p6spy.config.logfile"); - } - P6ModuleManager.getInstance().reload(); - } - - private Map findDefinedOptions() { - SpyDotProperties spyDotProperties = null; - try { - spyDotProperties = new SpyDotProperties(); - } - catch (IOException ignored) { - } - return Stream.of(spyDotProperties, new EnvironmentVariables(), new SystemProperties()).filter(Objects::nonNull) - .map(P6OptionsSource::getOptions).filter(Objects::nonNull) - .flatMap(options -> options.entrySet().stream()) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, - // always using value from the first P6OptionsSource - (value1, value2) -> value1)); - } - - @Override - public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.p6spy.engine.logging.P6LogFactory; +import com.p6spy.engine.spy.P6ModuleManager; +import com.p6spy.engine.spy.P6SpyFactory; +import com.p6spy.engine.spy.option.EnvironmentVariables; +import com.p6spy.engine.spy.option.P6OptionsSource; +import com.p6spy.engine.spy.option.SpyDotProperties; +import com.p6spy.engine.spy.option.SystemProperties; +import org.slf4j.Logger; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +import static org.slf4j.LoggerFactory.getLogger; + +/** + * Sets p6spy properties to / from system properties. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +class P6SpyPropertiesSetter implements BeanDefinitionRegistryPostProcessor, Closeable { + + private static final Logger log = getLogger(P6SpyPropertiesSetter.class); + + private final ConfigurableApplicationContext context; + + private final Map initialP6SpyOptions; + + P6SpyPropertiesSetter(ConfigurableApplicationContext context) { + this.context = context; + this.initialP6SpyOptions = findDefinedOptions(); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + ConfigurableEnvironment environment = this.context.getEnvironment(); + String customModuleList = initialP6SpyOptions.get("modulelist"); + boolean isEnableLogging = environment.getProperty("spring.sleuth.jdbc.p6spy.enable-logging", Boolean.class, + true); + if (customModuleList != null) { + log.info("P6Spy modulelist is overridden, some p6spy configuration features will not be applied"); + } + else { + List moduleList = new ArrayList<>(); + // default factory, holds P6Spy configuration + moduleList.add(P6SpyFactory.class.getName()); + if (isEnableLogging) { + moduleList.add(P6LogFactory.class.getName()); + } + System.setProperty("p6spy.config.modulelist", String.join(",", moduleList)); + } + if (!initialP6SpyOptions.containsKey("logMessageFormat")) { + String logFormat = logFormat(environment); + boolean isMultiline = multiLine(environment); + if (logFormat != null) { + System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); + System.setProperty("p6spy.config.customLogMessageFormat", logFormat); + } + else if (isMultiline) { + System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.MultiLineFormat"); + } + } + if (isEnableLogging && !initialP6SpyOptions.containsKey("appender")) { + TraceJdbcProperties.P6SpyProperties.P6SpyLogging logging = TraceJdbcProperties.P6SpyProperties.P6SpyLogging + .valueOf(environment + .getProperty("spring.sleuth.jdbc.p6spy.logging", String.class, + TraceJdbcProperties.P6SpyProperties.P6SpyLogging.SLF4J.toString()) + .toUpperCase()); + switch (logging) { + case SYSOUT: + System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.StdoutLogger"); + break; + case SLF4J: + System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.Slf4JLogger"); + break; + case FILE: + System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.FileLogger"); + break; + case CUSTOM: + String customAppender = environment.getProperty("spring.sleuth.jdbc.p6spy.custom-appender-class", + String.class, ""); + System.setProperty("p6spy.config.appender", customAppender); + break; + } + } + if (!initialP6SpyOptions.containsKey("logfile")) { + String logFile = environment.getProperty("spring.sleuth.jdbc.p6spy.log-file", String.class, "spy.log"); + System.setProperty("p6spy.config.logfile", logFile); + } + String pattern = environment.getProperty("spring.sleuth.jdbc.p6spy.log-filter.pattern", String.class); + if (pattern != null) { + System.setProperty("p6spy.config.filter", "true"); + System.setProperty("p6spy.config.sqlexpression", pattern); + } + // If factories were loaded before this method is initialized changing properties + // will not be applied + // Changes done in this method could not override anything user specified, + // therefore it is safe to call reload + P6ModuleManager.getInstance().reload(); + } + + private Boolean multiLine(ConfigurableEnvironment environment) { + return environment.getProperty("spring.sleuth.jdbc.p6spy.multiline", Boolean.class, true); + } + + private String logFormat(ConfigurableEnvironment environment) { + return environment.getProperty("spring.sleuth.jdbc.p6spy.log-format", String.class); + } + + @Override + public void close() throws IOException { + if (!initialP6SpyOptions.containsKey("modulelist")) { + System.clearProperty("p6spy.config.modulelist"); + } + if (!initialP6SpyOptions.containsKey("logMessageFormat")) { + ConfigurableEnvironment environment = this.context.getEnvironment(); + String logFormat = logFormat(environment); + boolean isMultiline = multiLine(environment); + if (logFormat != null) { + System.clearProperty("p6spy.config.logMessageFormat"); + System.clearProperty("p6spy.config.customLogMessageFormat"); + } + else if (isMultiline) { + System.clearProperty("p6spy.config.logMessageFormat"); + } + } + if (!initialP6SpyOptions.containsKey("appender")) { + System.clearProperty("p6spy.config.appender"); + } + if (!initialP6SpyOptions.containsKey("logfile")) { + System.clearProperty("p6spy.config.logfile"); + } + P6ModuleManager.getInstance().reload(); + } + + private Map findDefinedOptions() { + SpyDotProperties spyDotProperties = null; + try { + spyDotProperties = new SpyDotProperties(); + } + catch (IOException ignored) { + } + return Stream.of(spyDotProperties, new EnvironmentVariables(), new SystemProperties()).filter(Objects::nonNull) + .map(P6OptionsSource::getOptions).filter(Objects::nonNull) + .flatMap(options -> options.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, + // always using value from the first P6OptionsSource + (value1, value2) -> value1)); + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorBeanPostProcessor.java index 6fde67b16..3c1a78142 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorBeanPostProcessor.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorBeanPostProcessor.java @@ -1,140 +1,140 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.stream.Collectors; - -import javax.sql.DataSource; - -import com.zaxxer.hikari.HikariDataSource; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.aop.scope.ScopedProxyUtils; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.util.ClassUtils; - -/** - * {@link BeanPostProcessor} that wraps all data source beans in {@link DataSource} - * proxies specified in property 'spring.datasource.type'. - * - * @author Arthur Gavlyukovskiy - * @since 3.1.0 - */ -public class TraceDataSourceDecoratorBeanPostProcessor implements BeanPostProcessor, Ordered, ApplicationContextAware { - - private static final Log log = LogFactory.getLog(TraceDataSourceDecoratorBeanPostProcessor.class); - - private final static boolean HIKARI_AVAILABLE = ClassUtils.isPresent("com.zaxxer.hikari.HikariDataSource", - DataSourceNameResolver.class.getClassLoader()); - - private ApplicationContext applicationContext; - - private DataSourceNameResolver dataSourceNameResolver; - - private final Collection excludedBeans; - - public TraceDataSourceDecoratorBeanPostProcessor(Collection excludedBeans) { - this.excludedBeans = excludedBeans; - } - - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - return bean; - } - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof DataSource && !ScopedProxyUtils.isScopedTarget(beanName) - && !this.excludedBeans.contains(beanName)) { - // TODO: This might be a problem - Map decorators = this.applicationContext - .getBeansOfType(DataSourceDecorator.class).entrySet().stream() - .sorted(Entry.comparingByValue(AnnotationAwareOrderComparator.INSTANCE)) - .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (v1, v2) -> v2, LinkedHashMap::new)); - return decorate((DataSource) bean, getDataSourceName(bean, beanName), decorators); - } - else { - return bean; - } - } - - private String getDataSourceName(Object bean, String beanName) { - if (HIKARI_AVAILABLE && bean instanceof HikariDataSource) { - HikariDataSource hikariDataSource = (HikariDataSource) bean; - if (hikariDataSource.getPoolName() != null && !hikariDataSource.getPoolName().startsWith("HikariPool-")) { - return hikariDataSource.getPoolName(); - } - } - return beanName; - } - - private DataSource decorate(DataSource dataSource, String name, Map decorators) { - getDataSourceNameResolver().addDataSource(name, dataSource); - DataSource decoratedDataSource = dataSource; - for (Entry decoratorEntry : decorators.entrySet()) { - String decoratorBeanName = decoratorEntry.getKey(); - DataSourceDecorator decorator = decoratorEntry.getValue(); - DataSource dataSourceBeforeDecorating = decoratedDataSource; - decoratedDataSource = Objects.requireNonNull(decorator.decorate(name, decoratedDataSource), - "DataSourceDecorator (" + decoratorBeanName + ", " + decorator + ") should not return null"); - if (dataSourceBeforeDecorating != decoratedDataSource) { - getDataSourceNameResolver().addDataSource(name, decoratedDataSource); - } - } - if (dataSource != decoratedDataSource) { - if (log.isDebugEnabled()) { - log.debug("The decorated data source [" + decoratedDataSource + "] will replace the original one [" - + dataSource + "]"); - } - decoratedDataSource = new DataSourceWrapper(dataSource, decoratedDataSource); - getDataSourceNameResolver().addDataSource(name, decoratedDataSource); - } - return decoratedDataSource; - } - - private DataSourceNameResolver getDataSourceNameResolver() { - if (this.dataSourceNameResolver == null) { - this.dataSourceNameResolver = this.applicationContext.getBean(DataSourceNameResolver.class); - } - return this.dataSourceNameResolver; - } - - @Override - public int getOrder() { - return Ordered.LOWEST_PRECEDENCE - 10; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariDataSource; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.util.ClassUtils; + +/** + * {@link BeanPostProcessor} that wraps all data source beans in {@link DataSource} + * proxies specified in property 'spring.datasource.type'. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +public class TraceDataSourceDecoratorBeanPostProcessor implements BeanPostProcessor, Ordered, ApplicationContextAware { + + private static final Log log = LogFactory.getLog(TraceDataSourceDecoratorBeanPostProcessor.class); + + private final static boolean HIKARI_AVAILABLE = ClassUtils.isPresent("com.zaxxer.hikari.HikariDataSource", + DataSourceNameResolver.class.getClassLoader()); + + private ApplicationContext applicationContext; + + private DataSourceNameResolver dataSourceNameResolver; + + private final Collection excludedBeans; + + public TraceDataSourceDecoratorBeanPostProcessor(Collection excludedBeans) { + this.excludedBeans = excludedBeans; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof DataSource && !ScopedProxyUtils.isScopedTarget(beanName) + && !this.excludedBeans.contains(beanName)) { + // TODO: This might be a problem - it can lead to eager bean init + Map decorators = this.applicationContext + .getBeansOfType(DataSourceDecorator.class).entrySet().stream() + .sorted(Entry.comparingByValue(AnnotationAwareOrderComparator.INSTANCE)) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (v1, v2) -> v2, LinkedHashMap::new)); + return decorate((DataSource) bean, getDataSourceName(bean, beanName), decorators); + } + else { + return bean; + } + } + + private String getDataSourceName(Object bean, String beanName) { + if (HIKARI_AVAILABLE && bean instanceof HikariDataSource) { + HikariDataSource hikariDataSource = (HikariDataSource) bean; + if (hikariDataSource.getPoolName() != null && !hikariDataSource.getPoolName().startsWith("HikariPool-")) { + return hikariDataSource.getPoolName(); + } + } + return beanName; + } + + private DataSource decorate(DataSource dataSource, String name, Map decorators) { + getDataSourceNameResolver().addDataSource(name, dataSource); + DataSource decoratedDataSource = dataSource; + for (Entry decoratorEntry : decorators.entrySet()) { + String decoratorBeanName = decoratorEntry.getKey(); + DataSourceDecorator decorator = decoratorEntry.getValue(); + DataSource dataSourceBeforeDecorating = decoratedDataSource; + decoratedDataSource = Objects.requireNonNull(decorator.decorate(name, decoratedDataSource), + "DataSourceDecorator (" + decoratorBeanName + ", " + decorator + ") should not return null"); + if (dataSourceBeforeDecorating != decoratedDataSource) { + getDataSourceNameResolver().addDataSource(name, decoratedDataSource); + } + } + if (dataSource != decoratedDataSource) { + if (log.isDebugEnabled()) { + log.debug("The decorated data source [" + decoratedDataSource + "] will replace the original one [" + + dataSource + "]"); + } + decoratedDataSource = new DataSourceWrapper(dataSource, decoratedDataSource); + getDataSourceNameResolver().addDataSource(name, decoratedDataSource); + } + return decoratedDataSource; + } + + private DataSourceNameResolver getDataSourceNameResolver() { + if (this.dataSourceNameResolver == null) { + this.dataSourceNameResolver = this.applicationContext.getBean(DataSourceNameResolver.class); + } + return this.dataSourceNameResolver; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcAutoConfiguration.java similarity index 91% rename from spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorAutoConfiguration.java rename to spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcAutoConfiguration.java index 0c0c46f9e..c8c385ba9 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcAutoConfiguration.java @@ -1,70 +1,70 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import javax.sql.DataSource; - -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceHikariListenerStrategySpanCustomizer; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for proxying DataSource. - * - * @author Arthur Gavlyukovskiy - */ -@Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(TraceDataSourceDecoratorProperties.class) -@ConditionalOnProperty(name = "spring.sleuth.jdbc.enabled", havingValue = "true", matchIfMissing = true) -@ConditionalOnBean({ DataSource.class, Tracer.class }) -@AutoConfigureAfter(DataSourceAutoConfiguration.class) -@Import({ P6SpyConfiguration.class, DataSourceProxyConfiguration.class }) -public class TraceDataSourceDecoratorAutoConfiguration { - - @Bean - @ConditionalOnBean(DataSourceDecorator.class) - static TraceDataSourceDecoratorBeanPostProcessor traceDataSourceDecoratorBeanPostProcessor( - TraceDataSourceDecoratorProperties dataSourceDecoratorProperties) { - return new TraceDataSourceDecoratorBeanPostProcessor(dataSourceDecoratorProperties.getExcludedBeans()); - } - - @Bean - @ConditionalOnMissingBean - DataSourceNameResolver traceDataSourceNameResolver() { - return new DataSourceNameResolver(); - } - - @Bean - @ConditionalOnClass(name = "com.zaxxer.hikari.HikariDataSource") - TraceListenerStrategySpanCustomizer hikariTraceListenerStrategySpanCustomizer() { - return new TraceHikariListenerStrategySpanCustomizer(); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import javax.sql.DataSource; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceHikariListenerStrategySpanCustomizer; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for proxying DataSource. + * + * @author Arthur Gavlyukovskiy + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(TraceJdbcProperties.class) +@ConditionalOnProperty(name = "spring.sleuth.jdbc.enabled", havingValue = "true", matchIfMissing = true) +@ConditionalOnBean({ DataSource.class, Tracer.class }) +@AutoConfigureAfter(DataSourceAutoConfiguration.class) +@Import({ P6SpyConfiguration.class, DataSourceProxyConfiguration.class }) +public class TraceJdbcAutoConfiguration { + + @Bean + @ConditionalOnBean(DataSourceDecorator.class) + static TraceDataSourceDecoratorBeanPostProcessor traceDataSourceDecoratorBeanPostProcessor( + TraceJdbcProperties dataSourceDecoratorProperties) { + return new TraceDataSourceDecoratorBeanPostProcessor(dataSourceDecoratorProperties.getExcludedDataSourceBeanNames()); + } + + @Bean + @ConditionalOnMissingBean + DataSourceNameResolver traceDataSourceNameResolver() { + return new DataSourceNameResolver(); + } + + @Bean + @ConditionalOnClass(name = "com.zaxxer.hikari.HikariDataSource") + TraceListenerStrategySpanCustomizer hikariTraceListenerStrategySpanCustomizer() { + return new TraceHikariListenerStrategySpanCustomizer(); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorProperties.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java similarity index 80% rename from spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorProperties.java rename to spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java index 9e71c9013..bd16574da 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorProperties.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java @@ -1,485 +1,511 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.regex.Pattern; - -import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceType; - -/** - * Properties for configuring proxy providers. - * - * @author Arthur Gavlyukovskiy - * @since 3.1.0 - */ -@ConfigurationProperties(prefix = "spring.sleuth.jdbc") -public class TraceDataSourceDecoratorProperties { - - /** - * Enables data source decorating. - */ - private boolean enabled = true; - - /** - * Beans that won't be decorated. - */ - private Collection excludedBeans = Collections.emptyList(); - - /** - * Which types of tracing we would like to include. - */ - private List includes = Arrays.asList(TraceType.CONNECTION, TraceType.QUERY, TraceType.FETCH); - - private DataSourceProxyProperties datasourceProxy = new DataSourceProxyProperties(); - - private P6SpyProperties p6spy = new P6SpyProperties(); - - public boolean isEnabled() { - return enabled; - } - - public Collection getExcludedBeans() { - return excludedBeans; - } - - public List getIncludes() { - return includes; - } - - public void setIncludes(List includes) { - this.includes = includes; - } - - public DataSourceProxyProperties getDatasourceProxy() { - return datasourceProxy; - } - - public P6SpyProperties getP6spy() { - return p6spy; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public void setExcludedBeans(Collection excludedBeans) { - this.excludedBeans = excludedBeans; - } - - public void setDatasourceProxy(DataSourceProxyProperties datasourceProxy) { - this.datasourceProxy = datasourceProxy; - } - - public void setP6spy(P6SpyProperties p6spy) { - this.p6spy = p6spy; - } - - /** - * Properties for datasource-proxy. - */ - public static class DataSourceProxyProperties { - - /** - * Logging to use for logging queries. - */ - private DataSourceProxyLogging logging = DataSourceProxyLogging.SLF4J; - - /** - * Query configuration. - */ - private Query query = new Query(); - - /** - * Slow query configuration. - */ - private SlowQuery slowQuery = new SlowQuery(); - - /** - * Use multiline output for logging query. - * - * @see ProxyDataSourceBuilder#multiline() - */ - private boolean multiline = true; - - /** - * Use json output for logging query. - * - * @see ProxyDataSourceBuilder#asJson() - */ - private boolean jsonFormat = false; - - public DataSourceProxyLogging getLogging() { - return logging; - } - - public void setLogging(DataSourceProxyLogging logging) { - this.logging = logging; - } - - public Query getQuery() { - return query; - } - - public void setQuery(Query query) { - this.query = query; - } - - public SlowQuery getSlowQuery() { - return slowQuery; - } - - public void setSlowQuery(SlowQuery slowQuery) { - this.slowQuery = slowQuery; - } - - public boolean isMultiline() { - return multiline; - } - - public void setMultiline(boolean multiline) { - this.multiline = multiline; - } - - public boolean isJsonFormat() { - return jsonFormat; - } - - public void setJsonFormat(boolean jsonFormat) { - this.jsonFormat = jsonFormat; - } - - /** - * Properties to configure query logging listener. - */ - public static class Query { - - /** - * Enable logging all queries to the log. - */ - private boolean enableLogging = true; - - /** - * Name of query logger. - */ - private String loggerName; - - /** - * Severity of query logger. - */ - private String logLevel = "DEBUG"; - - public boolean isEnableLogging() { - return enableLogging; - } - - public void setEnableLogging(boolean enableLogging) { - this.enableLogging = enableLogging; - } - - public String getLoggerName() { - return loggerName; - } - - public void setLoggerName(String loggerName) { - this.loggerName = loggerName; - } - - public String getLogLevel() { - return logLevel; - } - - public void setLogLevel(String logLevel) { - this.logLevel = logLevel; - } - - } - - /** - * Properties to configure slow query logging listener. - */ - public static class SlowQuery { - - /** - * Enable logging slow queries to the log. - */ - private boolean enableLogging = true; - - /** - * Name of slow query logger. - */ - private String loggerName; - - /** - * Severity of slow query logger. - */ - private String logLevel = "WARN"; - - /** - * Number of seconds to consider query as slow. - */ - private long threshold = 300; - - boolean isEnableLogging() { - return enableLogging; - } - - public void setEnableLogging(boolean enableLogging) { - this.enableLogging = enableLogging; - } - - public String getLoggerName() { - return loggerName; - } - - public void setLoggerName(String loggerName) { - this.loggerName = loggerName; - } - - public String getLogLevel() { - return logLevel; - } - - public void setLogLevel(String logLevel) { - this.logLevel = logLevel; - } - - public long getThreshold() { - return threshold; - } - - public void setThreshold(long threshold) { - this.threshold = threshold; - } - - } - - /** - * Query logging listener is the most used listener that logs executing query with - * actual parameters to. You can pick one of the following proxy logging - * mechanisms. - */ - public enum DataSourceProxyLogging { - - /** - * Log using System.out. - */ - SYSOUT, - - /** - * Log using SLF4J. - */ - SLF4J, - - /** - * Log using Commons. - */ - COMMONS, - - /** - * Log using Java Util Logging. - */ - JUL - - } - - } - - /** - * Properties for configuring p6spy. - */ - public static class P6SpyProperties { - - /** - * Enables logging JDBC events. - */ - private boolean enableLogging = true; - - /** - * Enables multiline output. - */ - private boolean multiline = true; - - /** - * Logging to use for logging queries. - */ - private P6SpyLogging logging = P6SpyLogging.SLF4J; - - /** - * Name of log file to use (only with logging=file). - */ - private String logFile = "spy.log"; - - /** - * Custom log format. - */ - private String logFormat; - - /** - * Tracing related properties. - */ - private P6SpyTracing tracing = new P6SpyTracing(); - - /** - * Class file to use (only with logging=custom). The class must implement - * {@link com.p6spy.engine.spy.appender.FormattedLogger}. - */ - private String customAppenderClass; - - /** - * Log filtering related properties. - */ - private P6SpyLogFilter logFilter = new P6SpyLogFilter(); - - public boolean isEnableLogging() { - return enableLogging; - } - - public void setEnableLogging(boolean enableLogging) { - this.enableLogging = enableLogging; - } - - public boolean isMultiline() { - return multiline; - } - - public void setMultiline(boolean multiline) { - this.multiline = multiline; - } - - public P6SpyLogging getLogging() { - return logging; - } - - public void setLogging(P6SpyLogging logging) { - this.logging = logging; - } - - public String getLogFile() { - return logFile; - } - - public void setLogFile(String logFile) { - this.logFile = logFile; - } - - public String getLogFormat() { - return logFormat; - } - - public void setLogFormat(String logFormat) { - this.logFormat = logFormat; - } - - public P6SpyTracing getTracing() { - return tracing; - } - - public void setTracing(P6SpyTracing tracing) { - this.tracing = tracing; - } - - public String getCustomAppenderClass() { - return customAppenderClass; - } - - public void setCustomAppenderClass(String customAppenderClass) { - this.customAppenderClass = customAppenderClass; - } - - public P6SpyLogFilter getLogFilter() { - return logFilter; - } - - public void setLogFilter(P6SpyLogFilter logFilter) { - this.logFilter = logFilter; - } - - /** - * P6Spy logging options. - */ - public enum P6SpyLogging { - - /** - * Log using System.out. - */ - SYSOUT, - - /** - * Log using SLF4J. - */ - SLF4J, - - /** - * Log to file. - */ - FILE, - - /** - * Custom logging. - */ - CUSTOM - - } - - public static class P6SpyTracing { - - /** - * Report the effective sql string (with '?' replaced with real values) to - * tracing systems. - *

- * NOTE this setting does not affect the logging message. - */ - private boolean includeParameterValues = true; - - public boolean isIncludeParameterValues() { - return includeParameterValues; - } - - public void setIncludeParameterValues(boolean includeParameterValues) { - this.includeParameterValues = includeParameterValues; - } - - } - - public static class P6SpyLogFilter { - - /** - * Use regex pattern to filter log messages. Only matched messages will be - * logged. - */ - private Pattern pattern; - - public Pattern getPattern() { - return pattern; - } - - public void setPattern(Pattern pattern) { - this.pattern = pattern; - } - - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceType; + +/** + * Properties for JDBC instrumentation. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +@ConfigurationProperties(prefix = "spring.sleuth.jdbc") +public class TraceJdbcProperties { + + /** + * Enables JDBC instrumentation. + */ + private boolean enabled = true; + + /** + * List of DataSource bean names that will not be decorated. + */ + private Collection excludedDataSourceBeanNames = Collections.emptyList(); + + /** + * Which types of tracing we would like to include. + */ + private List includes = Arrays.asList(TraceType.CONNECTION, TraceType.QUERY, TraceType.FETCH); + + private DataSourceProxyProperties datasourceProxy = new DataSourceProxyProperties(); + + private P6SpyProperties p6spy = new P6SpyProperties(); + + public boolean isEnabled() { + return this.enabled; + } + + public Collection getExcludedDataSourceBeanNames() { + return this.excludedDataSourceBeanNames; + } + + public List getIncludes() { + return this.includes; + } + + public void setIncludes(List includes) { + this.includes = includes; + } + + public DataSourceProxyProperties getDatasourceProxy() { + return datasourceProxy; + } + + public P6SpyProperties getP6spy() { + return this.p6spy; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setExcludedDataSourceBeanNames(Collection excludedDataSourceBeanNames) { + this.excludedDataSourceBeanNames = excludedDataSourceBeanNames; + } + + public void setDatasourceProxy(DataSourceProxyProperties datasourceProxy) { + this.datasourceProxy = datasourceProxy; + } + + public void setP6spy(P6SpyProperties p6spy) { + this.p6spy = p6spy; + } + + /** + * Properties for datasource-proxy. + */ + public static class DataSourceProxyProperties { + + /** + * Should the datasource-proxy tracing be enabled? + */ + private boolean enabled = true; + + /** + * Logging to use for logging queries. + */ + private DataSourceProxyLogging logging = DataSourceProxyLogging.SLF4J; + + /** + * Query configuration. + */ + private Query query = new Query(); + + /** + * Slow query configuration. + */ + private SlowQuery slowQuery = new SlowQuery(); + + /** + * Use multiline output for logging query. + * + * @see ProxyDataSourceBuilder#multiline() + */ + private boolean multiline = true; + + /** + * Use json output for logging query. + * + * @see ProxyDataSourceBuilder#asJson() + */ + private boolean jsonFormat = false; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public DataSourceProxyLogging getLogging() { + return this.logging; + } + + public void setLogging(DataSourceProxyLogging logging) { + this.logging = logging; + } + + public Query getQuery() { + return this.query; + } + + public void setQuery(Query query) { + this.query = query; + } + + public SlowQuery getSlowQuery() { + return this.slowQuery; + } + + public void setSlowQuery(SlowQuery slowQuery) { + this.slowQuery = slowQuery; + } + + public boolean isMultiline() { + return this.multiline; + } + + public void setMultiline(boolean multiline) { + this.multiline = multiline; + } + + public boolean isJsonFormat() { + return this.jsonFormat; + } + + public void setJsonFormat(boolean jsonFormat) { + this.jsonFormat = jsonFormat; + } + + /** + * Properties to configure query logging listener. + */ + public static class Query { + + /** + * Enable logging all queries to the log. + */ + private boolean enableLogging = true; + + /** + * Name of query logger. + */ + private String loggerName; + + /** + * Severity of query logger. + */ + private String logLevel = "DEBUG"; + + public boolean isEnableLogging() { + return this.enableLogging; + } + + public void setEnableLogging(boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public String getLoggerName() { + return this.loggerName; + } + + public void setLoggerName(String loggerName) { + this.loggerName = loggerName; + } + + public String getLogLevel() { + return this.logLevel; + } + + public void setLogLevel(String logLevel) { + this.logLevel = logLevel; + } + + } + + /** + * Properties to configure slow query logging listener. + */ + public static class SlowQuery { + + /** + * Enable logging slow queries to the log. + */ + private boolean enableLogging = true; + + /** + * Name of slow query logger. + */ + private String loggerName; + + /** + * Severity of slow query logger. + */ + private String logLevel = "WARN"; + + /** + * Number of seconds to consider query as slow. + */ + private long threshold = 300; + + boolean isEnableLogging() { + return enableLogging; + } + + public void setEnableLogging(boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public String getLoggerName() { + return loggerName; + } + + public void setLoggerName(String loggerName) { + this.loggerName = loggerName; + } + + public String getLogLevel() { + return logLevel; + } + + public void setLogLevel(String logLevel) { + this.logLevel = logLevel; + } + + public long getThreshold() { + return threshold; + } + + public void setThreshold(long threshold) { + this.threshold = threshold; + } + + } + + /** + * Query logging listener is the most used listener that logs executing query with + * actual parameters to. You can pick one of the following proxy logging + * mechanisms. + */ + public enum DataSourceProxyLogging { + + /** + * Log using System.out. + */ + SYSOUT, + + /** + * Log using SLF4J. + */ + SLF4J, + + /** + * Log using Commons. + */ + COMMONS, + + /** + * Log using Java Util Logging. + */ + JUL + + } + + } + + /** + * Properties for configuring p6spy. + */ + public static class P6SpyProperties { + + /** + * Should the p6spy tracing be enabled? + */ + private boolean enabled = true; + + /** + * Enables logging JDBC events. + */ + private boolean enableLogging = true; + + /** + * Enables multiline output. + */ + private boolean multiline = true; + + /** + * Logging to use for logging queries. + */ + private P6SpyLogging logging = P6SpyLogging.SLF4J; + + /** + * Name of log file to use (only with logging=file). + */ + private String logFile = "spy.log"; + + /** + * Custom log format. + */ + private String logFormat; + + /** + * Tracing related properties. + */ + private P6SpyTracing tracing = new P6SpyTracing(); + + /** + * Class file to use (only with logging=custom). The class must implement + * {@link com.p6spy.engine.spy.appender.FormattedLogger}. + */ + private String customAppenderClass; + + /** + * Log filtering related properties. + */ + private P6SpyLogFilter logFilter = new P6SpyLogFilter(); + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnableLogging() { + return this.enableLogging; + } + + public void setEnableLogging(boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public boolean isMultiline() { + return this.multiline; + } + + public void setMultiline(boolean multiline) { + this.multiline = multiline; + } + + public P6SpyLogging getLogging() { + return this.logging; + } + + public void setLogging(P6SpyLogging logging) { + this.logging = logging; + } + + public String getLogFile() { + return this.logFile; + } + + public void setLogFile(String logFile) { + this.logFile = logFile; + } + + public String getLogFormat() { + return this.logFormat; + } + + public void setLogFormat(String logFormat) { + this.logFormat = logFormat; + } + + public P6SpyTracing getTracing() { + return this.tracing; + } + + public void setTracing(P6SpyTracing tracing) { + this.tracing = tracing; + } + + public String getCustomAppenderClass() { + return this.customAppenderClass; + } + + public void setCustomAppenderClass(String customAppenderClass) { + this.customAppenderClass = customAppenderClass; + } + + public P6SpyLogFilter getLogFilter() { + return this.logFilter; + } + + public void setLogFilter(P6SpyLogFilter logFilter) { + this.logFilter = logFilter; + } + + /** + * P6Spy logging options. + */ + public enum P6SpyLogging { + + /** + * Log using System.out. + */ + SYSOUT, + + /** + * Log using SLF4J. + */ + SLF4J, + + /** + * Log to file. + */ + FILE, + + /** + * Custom logging. + */ + CUSTOM + + } + + public static class P6SpyTracing { + + /** + * Report the effective sql string (with '?' replaced with real values) to + * tracing systems. + *

+ * NOTE this setting does not affect the logging message. + */ + private boolean includeParameterValues = true; + + public boolean isIncludeParameterValues() { + return this.includeParameterValues; + } + + public void setIncludeParameterValues(boolean includeParameterValues) { + this.includeParameterValues = includeParameterValues; + } + + } + + public static class P6SpyLogFilter { + + /** + * Use regex pattern to filter log messages. Only matched messages will be + * logged. + */ + private Pattern pattern; + + public Pattern getPattern() { + return this.pattern; + } + + public void setPattern(Pattern pattern) { + this.pattern = pattern; + } + + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index becaaeab8..16b2fd99f 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -9,7 +9,7 @@ org.springframework.cloud.sleuth.autoconfig.instrument.batch.TraceBatchAutoConfi org.springframework.cloud.sleuth.autoconfig.instrument.config.TraceSpringCloudConfigAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.circuitbreaker.TraceCircuitBreakerAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.deployer.TraceDeployerAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.jdbc.TraceDataSourceDecoratorAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.jdbc.TraceJdbcAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.rxjava.TraceRxJavaAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.quartz.TraceQuartzAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.task.TraceTaskAutoConfiguration,\ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/TraceNoOpAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/TraceNoOpAutoConfiguration.java index 0757e34d1..491a6ea42 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/TraceNoOpAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/TraceNoOpAutoConfiguration.java @@ -1,88 +1,104 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig; - -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.SpanCustomizer; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.autoconfig.instrument.web.ConditionalOnSleuthWeb; -import org.springframework.cloud.sleuth.autoconfig.instrument.web.client.ConditionalnOnSleuthWebClient; -import org.springframework.cloud.sleuth.http.HttpClientHandler; -import org.springframework.cloud.sleuth.http.HttpServerHandler; -import org.springframework.cloud.sleuth.propagation.Propagator; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -/** - * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration - * Auto-configuration} to enable tracing via Spring Cloud Sleuth. - * - * @author Spencer Gibb - * @author Marcin Grzejszczak - * @author Tim Ysewyn - * @since 2.0.0 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnProperty("spring.sleuth.noop.enabled") -@AutoConfigureBefore(BraveAutoConfiguration.class) -@Import(TraceConfiguration.class) -public class TraceNoOpAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - Tracer defaultTracer() { - return new NoOpTracer(); - } - - @Bean - Propagator defaultPropagator() { - return new NoOpPropagator(); - } - - @Bean - CurrentTraceContext defaultCurrentTraceContext() { - return new NoOpCurrentTraceContext(); - } - - @Bean - SpanCustomizer defaultSpanCustomizer() { - return new NoOpSpanCustomizer(); - } - - @Configuration(proxyBeanMethods = false) - static class TraceHttpConfiguration { - - @Bean - @ConditionalnOnSleuthWebClient - HttpClientHandler defaultHttpClientHandler() { - return new NoOpHttpClientHandler(); - } - - @Bean - @ConditionalOnSleuthWeb - HttpServerHandler defaultHttpServerHandler() { - return new NoOpHttpServerHandler(); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.SpanCustomizer; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.autoconfig.instrument.web.ConditionalOnSleuthWeb; +import org.springframework.cloud.sleuth.autoconfig.instrument.web.client.ConditionalnOnSleuthWebClient; +import org.springframework.cloud.sleuth.http.HttpClientHandler; +import org.springframework.cloud.sleuth.http.HttpServerHandler; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} to enable tracing via Spring Cloud Sleuth. + * + * @author Spencer Gibb + * @author Marcin Grzejszczak + * @author Tim Ysewyn + * @since 2.0.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnSleuth +@ConditionalOnProperty("spring.sleuth.noop.enabled") +@AutoConfigureBefore(BraveAutoConfiguration.class) +@Import(TraceConfiguration.class) +public class TraceNoOpAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + Tracer defaultTracer() { + return new NoOpTracer(); + } + + @Bean + Propagator defaultPropagator() { + return new NoOpPropagator(); + } + + @Bean + CurrentTraceContext defaultCurrentTraceContext() { + return new NoOpCurrentTraceContext(); + } + + @Bean + SpanCustomizer defaultSpanCustomizer() { + return new NoOpSpanCustomizer(); + } + + @Configuration(proxyBeanMethods = false) + static class TraceHttpConfiguration { + + @Bean + @ConditionalnOnSleuthWebClient + HttpClientHandler defaultHttpClientHandler() { + return new NoOpHttpClientHandler(); + } + + @Bean + @ConditionalOnSleuthWeb + HttpServerHandler defaultHttpServerHandler() { + return new NoOpHttpServerHandler(); + } + + } +} + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ConditionalOnProperty(value = "spring.sleuth.enabled", matchIfMissing = true) +@interface ConditionalOnSleuth { + +} + + diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java index b8c38ee16..f2a08a139 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java @@ -1,370 +1,371 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.io.PrintWriter; -import java.sql.Connection; -import java.util.concurrent.ThreadLocalRandom; -import java.util.logging.Logger; - -import javax.sql.DataSource; - -import com.p6spy.engine.spy.P6DataSource; -import com.zaxxer.hikari.HikariDataSource; -import net.ttddyy.dsproxy.support.ProxyDataSource; -import org.apache.commons.dbcp2.BasicDataSource; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.Scope; -import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -class DataSourceDecoratorAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, - TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()); - - @Test - void testDecoratingInDefaultOrder() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); - - DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; - assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(P6DataSource.class); - P6DataSource p6DataSource = (P6DataSource) DataSourceWrapper.getDecoratedDataSource(); - - DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); - assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; - }); - } - - @Test - void testNoDecoratingForExcludeBeans() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.excluded-beans:dataSource"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - - assertThat(dataSource).isInstanceOf(HikariDataSource.class); - }); - } - - @Test - void testDecoratingWhenDefaultProxyProviderNotAvailable() { - ApplicationContextRunner contextRunner = this.contextRunner; - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - - assertThat(((DataSourceWrapper) dataSource).getOriginalDataSource()).isInstanceOf(HikariDataSource.class); - - DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; - assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(P6DataSource.class); - P6DataSource p6DataSource = (P6DataSource) DataSourceWrapper.getDecoratedDataSource(); - - DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); - assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; - - DataSource dsProxyWrappedDataSource = (DataSource) ReflectionTestUtils.getField(proxyDataSource, - "dataSource"); - assertThat(dsProxyWrappedDataSource).isEqualTo(DataSourceWrapper.getOriginalDataSource()); - }); - } - - @Test - void testDecoratedHikariSpecificPropertiesIsSet() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.datasource.type:" + HikariDataSource.class.getName(), - "spring.datasource.hikari.catalog:test_catalog"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isNotNull(); - assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); - DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); - assertThat(realDataSource).isInstanceOf(HikariDataSource.class); - assertThat(((HikariDataSource) realDataSource).getCatalog()).isEqualTo("test_catalog"); - }); - } - - @Test - void testCustomDataSourceIsDecorated() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(TestDataSourceConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); - DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); - assertThat(realDataSource).isInstanceOf(BasicDataSource.class); - }); - } - - @Test - void testScopedDataSourceIsNotDecorated() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(TestScopedDataSourceConfiguration.class); - - contextRunner.run(context -> { - assertThat(context).getBeanNames(DataSource.class).containsOnly("dataSource", "scopedTarget.dataSource"); - assertThat(context).getBean("dataSource").isInstanceOf(DataSourceWrapper.class); - assertThat(context).getBean("scopedTarget.dataSource").isNotInstanceOf(DataSourceWrapper.class); - }); - } - - @Test - void testCustomDataSourceDecoratorApplied() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(TestDataSourceDecoratorConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isNotNull(); - - DataSource customDataSource = ((DataSourceWrapper) dataSource).getDecoratedDataSource(); - assertThat(customDataSource).isInstanceOf(CustomDataSourceProxy.class); - - DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); - assertThat(realDataSource).isInstanceOf(HikariDataSource.class); - - assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); - - DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; - - assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(CustomDataSourceProxy.class); - CustomDataSourceProxy customDataSourceProxy = (CustomDataSourceProxy) DataSourceWrapper - .getDecoratedDataSource(); - - assertThat(customDataSourceProxy.delegate).isInstanceOf(P6DataSource.class); - P6DataSource p6DataSource = (P6DataSource) customDataSourceProxy.delegate; - - DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); - assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; - }); - } - - @Test - void testDecoratingCanBeDisabled() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.enabled:false"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isInstanceOf(HikariDataSource.class); - }); - } - - @Test - void testDecoratingCanBeDisabledForSpecificBeans() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.excluded-beans:secondDataSource") - .withUserConfiguration(TestMultiDataSourceConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean("dataSource", DataSource.class); - assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); - - DataSource secondDataSource = context.getBean("secondDataSource", DataSource.class); - assertThat(secondDataSource).isInstanceOf(BasicDataSource.class); - }); - } - - @Test - void testDecoratingChainBuiltCorrectly() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - - DataSourceWrapper dataSource1 = context.getBean(DataSourceWrapper.class); - assertThat(dataSource1).isNotNull(); - - DataSource p6DataSource = dataSource1.getDecoratedDataSource(); - assertThat(p6DataSource).isNotNull(); - assertThat(p6DataSource).isInstanceOf(P6DataSource.class); - - DataSource proxyDataSource = (DataSource) new DirectFieldAccessor(p6DataSource) - .getPropertyValue("realDataSource"); - assertThat(proxyDataSource).isNotNull(); - assertThat(proxyDataSource).isInstanceOf(ProxyDataSource.class); - }); - } - - @Test - void testDecorateDynamicallyRegisteredBeans() { - ApplicationContextRunner contextRunner = this.contextRunner.withInitializer(context -> { - GenericApplicationContext gac = (GenericApplicationContext) context; - gac.registerBean("ds1", DataSource.class, () -> new HikariDataSource()); - gac.registerBean("ds2", DataSource.class, () -> new HikariDataSource()); - }); - - contextRunner.run(context -> { - DataSource dataSource1 = context.getBean("ds1", DataSource.class); - assertThat(dataSource1).isNotNull(); - assertThat(dataSource1).isInstanceOf(DataSourceWrapper.class); - - DataSource dataSource2 = context.getBean("ds2", DataSource.class); - assertThat(dataSource2).isNotNull(); - assertThat(dataSource2).isInstanceOf(DataSourceWrapper.class); - }); - } - - @Configuration - static class TestDataSourceConfiguration { - - @Bean - public DataSource dataSource() { - BasicDataSource pool = new BasicDataSource(); - pool.setDriverClassName("org.hsqldb.jdbcDriver"); - pool.setUrl("jdbc:hsqldb:target/overridedb"); - pool.setUsername("sa"); - return pool; - } - - } - - @Configuration - static class TestDataSourceDecoratorConfiguration { - - @Bean - public DataSourceDecorator customDataSourceDecorator() { - return (beanName, dataSource) -> new CustomDataSourceProxy(dataSource); - } - - } - - @Configuration - static class TestMultiDataSourceConfiguration { - - @Bean - @Primary - public DataSource dataSource() { - BasicDataSource pool = new BasicDataSource(); - pool.setDriverClassName("org.hsqldb.jdbcDriver"); - pool.setUrl("jdbc:hsqldb:target/db"); - pool.setUsername("sa"); - return pool; - } - - @Bean - public DataSource secondDataSource() { - BasicDataSource pool = new BasicDataSource(); - pool.setDriverClassName("org.hsqldb.jdbcDriver"); - pool.setUrl("jdbc:hsqldb:target/db2"); - pool.setUsername("sa"); - return pool; - } - - } - - @Configuration - static class TestScopedDataSourceConfiguration { - - @Bean - @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) - public DataSource dataSource() { - BasicDataSource pool = new BasicDataSource(); - pool.setDriverClassName("org.hsqldb.jdbcDriver"); - pool.setUrl("jdbc:hsqldb:target/overridedb"); - pool.setUsername("sa"); - return pool; - } - - } - - /** - * Custom proxy data source for tests. - * - * @author Arthur Gavlyukovskiy - */ - static class CustomDataSourceProxy implements DataSource { - - private final DataSource delegate; - - CustomDataSourceProxy(DataSource delegate) { - this.delegate = delegate; - } - - @Override - public Connection getConnection() { - return null; - } - - @Override - public Connection getConnection(String username, String password) { - return null; - } - - @Override - public T unwrap(Class iface) { - return null; - } - - @Override - public boolean isWrapperFor(Class iface) { - return false; - } - - @Override - public PrintWriter getLogWriter() { - return null; - } - - @Override - public void setLogWriter(PrintWriter out) { - - } - - @Override - public void setLoginTimeout(int seconds) { - - } - - @Override - public int getLoginTimeout() { - return 0; - } - - @Override - public Logger getParentLogger() { - return null; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.util.concurrent.ThreadLocalRandom; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +import com.p6spy.engine.spy.P6DataSource; +import com.zaxxer.hikari.HikariDataSource; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import org.apache.commons.dbcp2.BasicDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class DataSourceDecoratorAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceJdbcAutoConfiguration.class, TraceNoOpAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.sleuth.noop.enabled=true", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()); + + @Test + void testDecoratingInDefaultOrder() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + + DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; + assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(P6DataSource.class); + P6DataSource p6DataSource = (P6DataSource) DataSourceWrapper.getDecoratedDataSource(); + + DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); + assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; + }); + } + + @Test + void testNoDecoratingForExcludeBeans() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.excluded-data-source-bean-names:dataSource"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + }); + } + + @Test + void testDecoratingWhenDefaultProxyProviderNotAvailable() { + ApplicationContextRunner contextRunner = this.contextRunner; + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + assertThat(((DataSourceWrapper) dataSource).getOriginalDataSource()).isInstanceOf(HikariDataSource.class); + + DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; + assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(P6DataSource.class); + P6DataSource p6DataSource = (P6DataSource) DataSourceWrapper.getDecoratedDataSource(); + + DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); + assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; + + DataSource dsProxyWrappedDataSource = (DataSource) ReflectionTestUtils.getField(proxyDataSource, + "dataSource"); + assertThat(dsProxyWrappedDataSource).isEqualTo(DataSourceWrapper.getOriginalDataSource()); + }); + } + + @Test + void testDecoratedHikariSpecificPropertiesIsSet() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.datasource.type:" + HikariDataSource.class.getName(), + "spring.datasource.hikari.catalog:test_catalog"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isNotNull(); + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); + assertThat(realDataSource).isInstanceOf(HikariDataSource.class); + assertThat(((HikariDataSource) realDataSource).getCatalog()).isEqualTo("test_catalog"); + }); + } + + @Test + void testCustomDataSourceIsDecorated() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(TestDataSourceConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); + assertThat(realDataSource).isInstanceOf(BasicDataSource.class); + }); + } + + @Test + void testScopedDataSourceIsNotDecorated() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(TestScopedDataSourceConfiguration.class); + + contextRunner.run(context -> { + assertThat(context).getBeanNames(DataSource.class).containsOnly("dataSource", "scopedTarget.dataSource"); + assertThat(context).getBean("dataSource").isInstanceOf(DataSourceWrapper.class); + assertThat(context).getBean("scopedTarget.dataSource").isNotInstanceOf(DataSourceWrapper.class); + }); + } + + @Test + void testCustomDataSourceDecoratorApplied() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(TestDataSourceDecoratorConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isNotNull(); + + DataSource customDataSource = ((DataSourceWrapper) dataSource).getDecoratedDataSource(); + assertThat(customDataSource).isInstanceOf(CustomDataSourceProxy.class); + + DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); + assertThat(realDataSource).isInstanceOf(HikariDataSource.class); + + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + + DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; + + assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(CustomDataSourceProxy.class); + CustomDataSourceProxy customDataSourceProxy = (CustomDataSourceProxy) DataSourceWrapper + .getDecoratedDataSource(); + + assertThat(customDataSourceProxy.delegate).isInstanceOf(P6DataSource.class); + P6DataSource p6DataSource = (P6DataSource) customDataSourceProxy.delegate; + + DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); + assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; + }); + } + + @Test + void testDecoratingCanBeDisabled() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.enabled:false"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + }); + } + + @Test + void testDecoratingCanBeDisabledForSpecificBeans() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.excluded-data-source-bean-names:secondDataSource") + .withUserConfiguration(TestMultiDataSourceConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean("dataSource", DataSource.class); + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + + DataSource secondDataSource = context.getBean("secondDataSource", DataSource.class); + assertThat(secondDataSource).isInstanceOf(BasicDataSource.class); + }); + } + + @Test + void testDecoratingChainBuiltCorrectly() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + DataSourceWrapper dataSource1 = context.getBean(DataSourceWrapper.class); + assertThat(dataSource1).isNotNull(); + + DataSource p6DataSource = dataSource1.getDecoratedDataSource(); + assertThat(p6DataSource).isNotNull(); + assertThat(p6DataSource).isInstanceOf(P6DataSource.class); + + DataSource proxyDataSource = (DataSource) new DirectFieldAccessor(p6DataSource) + .getPropertyValue("realDataSource"); + assertThat(proxyDataSource).isNotNull(); + assertThat(proxyDataSource).isInstanceOf(ProxyDataSource.class); + }); + } + + @Test + void testDecorateDynamicallyRegisteredBeans() { + ApplicationContextRunner contextRunner = this.contextRunner.withInitializer(context -> { + GenericApplicationContext gac = (GenericApplicationContext) context; + gac.registerBean("ds1", DataSource.class, () -> new HikariDataSource()); + gac.registerBean("ds2", DataSource.class, () -> new HikariDataSource()); + }); + + contextRunner.run(context -> { + DataSource dataSource1 = context.getBean("ds1", DataSource.class); + assertThat(dataSource1).isNotNull(); + assertThat(dataSource1).isInstanceOf(DataSourceWrapper.class); + + DataSource dataSource2 = context.getBean("ds2", DataSource.class); + assertThat(dataSource2).isNotNull(); + assertThat(dataSource2).isInstanceOf(DataSourceWrapper.class); + }); + } + + @Configuration + static class TestDataSourceConfiguration { + + @Bean + public DataSource dataSource() { + BasicDataSource pool = new BasicDataSource(); + pool.setDriverClassName("org.hsqldb.jdbcDriver"); + pool.setUrl("jdbc:hsqldb:target/overridedb"); + pool.setUsername("sa"); + return pool; + } + + } + + @Configuration + static class TestDataSourceDecoratorConfiguration { + + @Bean + public DataSourceDecorator customDataSourceDecorator() { + return (beanName, dataSource) -> new CustomDataSourceProxy(dataSource); + } + + } + + @Configuration + static class TestMultiDataSourceConfiguration { + + @Bean + @Primary + public DataSource dataSource() { + BasicDataSource pool = new BasicDataSource(); + pool.setDriverClassName("org.hsqldb.jdbcDriver"); + pool.setUrl("jdbc:hsqldb:target/db"); + pool.setUsername("sa"); + return pool; + } + + @Bean + public DataSource secondDataSource() { + BasicDataSource pool = new BasicDataSource(); + pool.setDriverClassName("org.hsqldb.jdbcDriver"); + pool.setUrl("jdbc:hsqldb:target/db2"); + pool.setUsername("sa"); + return pool; + } + + } + + @Configuration + static class TestScopedDataSourceConfiguration { + + @Bean + @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) + public DataSource dataSource() { + BasicDataSource pool = new BasicDataSource(); + pool.setDriverClassName("org.hsqldb.jdbcDriver"); + pool.setUrl("jdbc:hsqldb:target/overridedb"); + pool.setUsername("sa"); + return pool; + } + + } + + /** + * Custom proxy data source for tests. + * + * @author Arthur Gavlyukovskiy + */ + static class CustomDataSourceProxy implements DataSource { + + private final DataSource delegate; + + CustomDataSourceProxy(DataSource delegate) { + this.delegate = delegate; + } + + @Override + public Connection getConnection() { + return null; + } + + @Override + public Connection getConnection(String username, String password) { + return null; + } + + @Override + public T unwrap(Class iface) { + return null; + } + + @Override + public boolean isWrapperFor(Class iface) { + return false; + } + + @Override + public PrintWriter getLogWriter() { + return null; + } + + @Override + public void setLogWriter(PrintWriter out) { + + } + + @Override + public void setLoginTimeout(int seconds) { + + } + + @Override + public int getLoginTimeout() { + return 0; + } + + @Override + public Logger getParentLogger() { + return null; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java index cb45faada..0573e862d 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java @@ -1,280 +1,280 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import com.p6spy.engine.common.ConnectionInformation; -import com.p6spy.engine.common.P6LogQuery; -import com.p6spy.engine.event.CompoundJdbcEventListener; -import com.p6spy.engine.event.JdbcEventListener; -import com.p6spy.engine.logging.Category; -import com.p6spy.engine.logging.LoggingEventListener; -import com.p6spy.engine.spy.JdbcEventListenerFactory; -import com.p6spy.engine.spy.P6DataSource; -import com.p6spy.engine.spy.appender.CustomLineFormat; -import com.p6spy.engine.spy.appender.FormattedLogger; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -class P6SpyConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, - TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) - .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); - - @BeforeEach - @AfterEach - void resetLogAccumulator() { - LogAccumulator.reset(); - } - - @Test - void testCustomListeners() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomListenerConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - GetCountingListener getCountingListener = context.getBean(GetCountingListener.class); - ClosingCountingListener closingCountingListener = context.getBean(ClosingCountingListener.class); - P6DataSource p6DataSource = (P6DataSource) ((DataSourceWrapper) dataSource).getDecoratedDataSource(); - assertThat(p6DataSource).extracting("jdbcEventListenerFactory").isEqualTo(jdbcEventListenerFactory); - - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).contains(getCountingListener, closingCountingListener); - assertThat(getCountingListener.connectionCount).isEqualTo(0); - - Connection connection1 = p6DataSource.getConnection(); - - assertThat(getCountingListener.connectionCount).isEqualTo(1); - assertThat(closingCountingListener.connectionCount).isEqualTo(0); - - Connection connection2 = p6DataSource.getConnection(); - - assertThat(getCountingListener.connectionCount).isEqualTo(2); - - // order matters! - connection2.close(); - - assertThat(closingCountingListener.connectionCount).isEqualTo(1); - - // order matters! - connection1.close(); - - assertThat(closingCountingListener.connectionCount).isEqualTo(2); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testDoesNotRegisterLoggingListenerIfDisabled() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.p6spy.enable-logging:false"); - - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).extracting("class") - .doesNotContain(LoggingEventListener.class); - }); - } - - @Test - void testCanSetCustomLoggingFormat() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.p6spy.log-format:test %{connectionId}"); - - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); - assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") - .isEqualTo(CustomLineFormat.class); - }); - } - - @Test - void testMultilineShouldNotOverrideCustomProperties() { - System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); - System.setProperty("p6spy.config.excludecategories", "debug"); - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.p6spy.multiline:true"); - - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); - assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") - .isEqualTo(CustomLineFormat.class); - }); - } - - @Test - void testUseCustomLogger() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.sleuth.jdbc.p6spy.logging:custom", - "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName()); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - dataSource.getConnection().close(); - - assertThat(P6LogQuery.getLogger()).isInstanceOf(LogAccumulator.class); - }); - } - - @Test - void testLogFilterPattern() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.sleuth.jdbc.p6spy.logging:custom", - "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), - "spring.sleuth.jdbc.p6spy.log-filter.pattern:.*table1.*"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - try (Connection connection = dataSource.getConnection(); - PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); - PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { - ps1.execute(); - ps2.execute(); - } - - assertThat(LogAccumulator.MESSAGES).hasSize(1); - assertThat(LogAccumulator.MESSAGES).allMatch(message -> message.contains("table1")); - }); - } - - @Test - void testLogFilterPatternMatchAll() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.sleuth.jdbc.p6spy.logging:custom", - "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), - "spring.sleuth.jdbc.p6spy.log-filter.pattern:.*"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - try (Connection connection = dataSource.getConnection(); - PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); - PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { - ps1.execute(); - ps2.execute(); - } - - assertThat(LogAccumulator.MESSAGES).hasSize(2); - }); - } - - @Configuration - static class CustomListenerConfiguration { - - @Bean - public GetCountingListener wrappingCountingListener() { - return new GetCountingListener(); - } - - @Bean - public ClosingCountingListener closingCountingListener() { - return new ClosingCountingListener(); - } - - } - - static class GetCountingListener extends JdbcEventListener { - - int connectionCount = 0; - - @Override - public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) { - connectionCount++; - } - - } - - static class ClosingCountingListener extends JdbcEventListener { - - int connectionCount = 0; - - @Override - public void onAfterConnectionClose(ConnectionInformation connectionInformation, SQLException e) { - connectionCount++; - } - - } - - public static class LogAccumulator extends FormattedLogger { - - static final List MESSAGES = new ArrayList<>(); - static final List EXCEPTIONS = new ArrayList<>(); - - public static void reset() { - MESSAGES.clear(); - EXCEPTIONS.clear(); - } - - @Override - public void logException(Exception e) { - EXCEPTIONS.add(e); - } - - @Override - public void logText(String text) { - MESSAGES.add(text); - } - - @Override - public boolean isCategoryEnabled(Category category) { - return true; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import com.p6spy.engine.common.ConnectionInformation; +import com.p6spy.engine.common.P6LogQuery; +import com.p6spy.engine.event.CompoundJdbcEventListener; +import com.p6spy.engine.event.JdbcEventListener; +import com.p6spy.engine.logging.Category; +import com.p6spy.engine.logging.LoggingEventListener; +import com.p6spy.engine.spy.JdbcEventListenerFactory; +import com.p6spy.engine.spy.P6DataSource; +import com.p6spy.engine.spy.appender.CustomLineFormat; +import com.p6spy.engine.spy.appender.FormattedLogger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +class P6SpyConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceJdbcAutoConfiguration.class, TraceNoOpAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.sleuth.noop.enabled=true", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); + + @BeforeEach + @AfterEach + void resetLogAccumulator() { + LogAccumulator.reset(); + } + + @Test + void testCustomListeners() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomListenerConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + GetCountingListener getCountingListener = context.getBean(GetCountingListener.class); + ClosingCountingListener closingCountingListener = context.getBean(ClosingCountingListener.class); + P6DataSource p6DataSource = (P6DataSource) ((DataSourceWrapper) dataSource).getDecoratedDataSource(); + assertThat(p6DataSource).extracting("jdbcEventListenerFactory").isEqualTo(jdbcEventListenerFactory); + + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).contains(getCountingListener, closingCountingListener); + assertThat(getCountingListener.connectionCount).isEqualTo(0); + + Connection connection1 = p6DataSource.getConnection(); + + assertThat(getCountingListener.connectionCount).isEqualTo(1); + assertThat(closingCountingListener.connectionCount).isEqualTo(0); + + Connection connection2 = p6DataSource.getConnection(); + + assertThat(getCountingListener.connectionCount).isEqualTo(2); + + // order matters! + connection2.close(); + + assertThat(closingCountingListener.connectionCount).isEqualTo(1); + + // order matters! + connection1.close(); + + assertThat(closingCountingListener.connectionCount).isEqualTo(2); + }); + } + + @Test + void testDoesNotRegisterLoggingListenerIfDisabled() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.p6spy.enable-logging=false"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class") + .doesNotContain(LoggingEventListener.class); + }); + } + + @Test + void testCanSetCustomLoggingFormat() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.p6spy.log-format:test %{connectionId}"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); + assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") + .isEqualTo(CustomLineFormat.class); + }); + } + + @Test + void testMultilineShouldNotOverrideCustomProperties() { + System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); + System.setProperty("p6spy.config.excludecategories", "debug"); + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.p6spy.multiline:true"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); + assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") + .isEqualTo(CustomLineFormat.class); + }); + } + + @Test + void testUseCustomLogger() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.p6spy.logging=custom", + "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName()); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + dataSource.getConnection().close(); + + assertThat(P6LogQuery.getLogger()).isInstanceOf(LogAccumulator.class); + }); + } + + @Test + void testLogFilterPattern() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.p6spy.logging=custom", + "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), + "spring.sleuth.jdbc.p6spy.log-filter.pattern:.*table1.*"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); + PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { + ps1.execute(); + ps2.execute(); + } + + assertThat(LogAccumulator.MESSAGES).hasSize(1); + assertThat(LogAccumulator.MESSAGES).allMatch(message -> message.contains("table1")); + }); + } + + @Test + void testLogFilterPatternMatchAll() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.p6spy.logging=custom", + "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), + "spring.sleuth.jdbc.p6spy.log-filter.pattern:.*"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); + PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { + ps1.execute(); + ps2.execute(); + } + + assertThat(LogAccumulator.MESSAGES).hasSize(2); + }); + } + + @Configuration + static class CustomListenerConfiguration { + + @Bean + public GetCountingListener wrappingCountingListener() { + return new GetCountingListener(); + } + + @Bean + public ClosingCountingListener closingCountingListener() { + return new ClosingCountingListener(); + } + + } + + static class GetCountingListener extends JdbcEventListener { + + int connectionCount = 0; + + @Override + public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) { + connectionCount++; + } + + } + + static class ClosingCountingListener extends JdbcEventListener { + + int connectionCount = 0; + + @Override + public void onAfterConnectionClose(ConnectionInformation connectionInformation, SQLException e) { + connectionCount++; + } + + } + + public static class LogAccumulator extends FormattedLogger { + + static final List MESSAGES = new ArrayList<>(); + static final List EXCEPTIONS = new ArrayList<>(); + + public static void reset() { + MESSAGES.clear(); + EXCEPTIONS.clear(); + } + + @Override + public void logException(Exception e) { + EXCEPTIONS.add(e); + } + + @Override + public void logText(String text) { + MESSAGES.add(text); + } + + @Override + public boolean isCategoryEnabled(Category category) { + return true; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java index ca3528afb..6b10c2d95 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java @@ -1,246 +1,239 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import net.ttddyy.dsproxy.ExecutionInfo; -import net.ttddyy.dsproxy.QueryInfo; -import net.ttddyy.dsproxy.listener.ChainListener; -import net.ttddyy.dsproxy.listener.QueryExecutionListener; -import net.ttddyy.dsproxy.listener.logging.CommonsQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.CommonsSlowQueryListener; -import net.ttddyy.dsproxy.listener.logging.JULQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.JULSlowQueryListener; -import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.SLF4JSlowQueryListener; -import net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.SystemOutSlowQueryListener; -import net.ttddyy.dsproxy.proxy.DefaultConnectionIdManager; -import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; -import net.ttddyy.dsproxy.support.ProxyDataSource; -import net.ttddyy.dsproxy.transform.ParameterTransformer; -import net.ttddyy.dsproxy.transform.QueryTransformer; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; - -import static org.assertj.core.api.Assertions.assertThat; - -class ProxyDataSourceConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, - TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) - .withClassLoader(new FilteredClassLoader("com.p6spy")); - - @Test - void testRegisterLogAndSlowQueryLogByDefaultToSlf4j() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogByUsingSlf4j() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.datasource-proxy.logging:slf4j"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogUsingSystemOut() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.datasource-proxy.logging:sysout"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutQueryLoggingListener.class); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogUsingJUL() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.datasourceProxy.logging:jul"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(JULSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(JULQueryLoggingListener.class); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogUsingApacheCommons() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.datasourceProxy.logging:commons"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(CommonsSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(CommonsQueryLoggingListener.class); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testCustomParameterAndQueryTransformer() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomDataSourceProxyConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ParameterTransformer parameterTransformer = context.getBean(ParameterTransformer.class); - QueryTransformer queryTransformer = context.getBean(QueryTransformer.class); - assertThat(proxyDataSource.getProxyConfig().getParameterTransformer()).isSameAs(parameterTransformer); - assertThat(proxyDataSource.getProxyConfig().getQueryTransformer()).isSameAs(queryTransformer); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testCustomListeners() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomListenerConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - QueryExecutionListener queryExecutionListener = context.getBean(QueryExecutionListener.class); - - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).contains(queryExecutionListener); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testGlobalConnectionIdManagerByDefault() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - - assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(GlobalConnectionIdManager.class); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testCustomConnectionIdManager() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomDataSourceProxyConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - - assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(DefaultConnectionIdManager.class); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Configuration - static class CustomDataSourceProxyConfiguration { - - @Bean - public ParameterTransformer parameterTransformer() { - return (replacer, transformInfo) -> { - }; - } - - @Bean - public QueryTransformer queryTransformer() { - return (transformInfo) -> "TestQuery"; - } - - @Bean - public DataSourceProxyConnectionIdManagerProvider connectionIdManagerProvider() { - return DefaultConnectionIdManager::new; - } - - } - - @Configuration - static class CustomListenerConfiguration { - - @Bean - @Primary - public QueryExecutionListener queryExecutionListener() { - return new QueryExecutionListener() { - @Override - public void beforeQuery(ExecutionInfo execInfo, List queryInfoList) { - System.out.println("beforeQuery"); - } - - @Override - public void afterQuery(ExecutionInfo execInfo, List queryInfoList) { - System.out.println("afterQuery"); - } - }; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import net.ttddyy.dsproxy.ExecutionInfo; +import net.ttddyy.dsproxy.QueryInfo; +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.listener.QueryExecutionListener; +import net.ttddyy.dsproxy.listener.logging.CommonsQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.CommonsSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.JULQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.JULSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.SystemOutSlowQueryListener; +import net.ttddyy.dsproxy.proxy.DefaultConnectionIdManager; +import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import net.ttddyy.dsproxy.transform.ParameterTransformer; +import net.ttddyy.dsproxy.transform.QueryTransformer; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import static org.assertj.core.api.Assertions.assertThat; + +class ProxyDataSourceConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceJdbcAutoConfiguration.class, TraceNoOpAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.sleuth.noop.enabled=true", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("com.p6spy")); + + @Test + void testRegisterLogAndSlowQueryLogByDefaultToSlf4j() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogByUsingSlf4j() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.datasource-proxy.logging=slf4j"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingSystemOut() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.datasource-proxy.logging=sysout"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingJUL() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.datasourceProxy.logging=jul"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(JULSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(JULQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingApacheCommons() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.datasourceProxy.logging=commons"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(CommonsSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(CommonsQueryLoggingListener.class); + }); + } + + @Test + void testCustomParameterAndQueryTransformer() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomDataSourceProxyConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ParameterTransformer parameterTransformer = context.getBean(ParameterTransformer.class); + QueryTransformer queryTransformer = context.getBean(QueryTransformer.class); + assertThat(proxyDataSource.getProxyConfig().getParameterTransformer()).isSameAs(parameterTransformer); + assertThat(proxyDataSource.getProxyConfig().getQueryTransformer()).isSameAs(queryTransformer); + }); + } + + @Test + void testCustomListeners() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomListenerConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + QueryExecutionListener queryExecutionListener = context.getBean(QueryExecutionListener.class); + + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).contains(queryExecutionListener); + }); + } + + @Test + void testGlobalConnectionIdManagerByDefault() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + + assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(GlobalConnectionIdManager.class); + }); + } + + @Test + void testCustomConnectionIdManager() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomDataSourceProxyConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + + assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(DefaultConnectionIdManager.class); + }); + } + + @Configuration + static class CustomDataSourceProxyConfiguration { + + @Bean + public ParameterTransformer parameterTransformer() { + return (replacer, transformInfo) -> { + }; + } + + @Bean + public QueryTransformer queryTransformer() { + return (transformInfo) -> "TestQuery"; + } + + @Bean + public DataSourceProxyConnectionIdManagerProvider connectionIdManagerProvider() { + return DefaultConnectionIdManager::new; + } + + } + + @Configuration + static class CustomListenerConfiguration { + + @Bean + @Primary + public QueryExecutionListener queryExecutionListener() { + return new QueryExecutionListener() { + @Override + public void beforeQuery(ExecutionInfo execInfo, List queryInfoList) { + System.out.println("beforeQuery"); + } + + @Override + public void afterQuery(ExecutionInfo execInfo, List queryInfoList) { + System.out.println("afterQuery"); + } + }; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java index 5a9af8e3f..0d1153ef0 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java @@ -1,67 +1,66 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.concurrent.ThreadLocalRandom; - -import com.p6spy.engine.event.CompoundJdbcEventListener; -import com.p6spy.engine.spy.JdbcEventListenerFactory; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; - -import static org.assertj.core.api.Assertions.assertThat; - -class SleuthP6SpyListenerAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, - TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) - .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); - - @Test - void testAddsP6SpyListener() { - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - assertThat(jdbcEventListener.getEventListeners()).extracting("class") - .contains(TraceJdbcEventListener.class); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testDoesNotAddP6SpyListenerIfNoTracer() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues("spring.sleuth.enabled:false"); - - contextRunner.run(context -> { - assertThat(context).doesNotHaveBean(JdbcEventListenerFactory.class); - }); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.concurrent.ThreadLocalRandom; + +import com.p6spy.engine.event.CompoundJdbcEventListener; +import com.p6spy.engine.spy.JdbcEventListenerFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; + +import static org.assertj.core.api.Assertions.assertThat; + +class SleuthP6SpyListenerAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceJdbcAutoConfiguration.class, TraceNoOpAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.sleuth.noop.enabled=true", + "spring.datasource.url=jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); + + @Test + void testAddsP6SpyListener() { + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + assertThat(jdbcEventListener.getEventListeners()).extracting("class") + .contains(TraceJdbcEventListener.class); + }); + } + + @Test + void testDoesNotAddP6SpyListenerIfNoTracer() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues("spring.sleuth.enabled=false"); + + contextRunner.run(context -> { + assertThat(context).doesNotHaveBean(JdbcEventListenerFactory.class); + }); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java index f359bd951..9380ce316 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java @@ -1,70 +1,69 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import net.ttddyy.dsproxy.listener.ChainListener; -import net.ttddyy.dsproxy.support.ProxyDataSource; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; - -import static org.assertj.core.api.Assertions.assertThat; - -class SleuthProxyDataSourceListenerAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, - TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) - .withClassLoader(new FilteredClassLoader("com.p6spy")); - - @Test - void testAddsDatasourceProxyListener() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(TraceQueryExecutionListener.class); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testDoesNotAddDatasourceProxyListenerIfNoTracer() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues("spring.sleuth.enabled:false"); - - contextRunner.run(context -> { - assertThat(context).doesNotHaveBean(DataSourceWrapper.class); - }); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; + +import static org.assertj.core.api.Assertions.assertThat; + +class SleuthProxyDataSourceListenerAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceJdbcAutoConfiguration.class, TraceNoOpAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.sleuth.noop.enabled=true", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("com.p6spy")); + + @Test + void testAddsDatasourceProxyListener() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(TraceQueryExecutionListener.class); + }); + } + + @Test + void testDoesNotAddDatasourceProxyListenerIfNoTracer() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues("spring.sleuth.enabled:false"); + + contextRunner.run(context -> { + assertThat(context).doesNotHaveBean(DataSourceWrapper.class); + }); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TestSpanHandlerConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TestSpanHandlerConfiguration.java deleted file mode 100644 index 78deb853e..000000000 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TestSpanHandlerConfiguration.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import brave.sampler.Sampler; -import brave.test.TestSpanHandler; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -class TestSpanHandlerConfiguration { - - @Bean - public TestSpanHandler spanHandler() { - return new TestSpanHandler(); - } - - @Bean - public Sampler sampler() { - return Sampler.ALWAYS_SAMPLE; - } - -} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java index 52fe0ae6b..3fd40e9d5 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java @@ -1,43 +1,43 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.jdbc; - -import javax.sql.CommonDataSource; - -import org.springframework.cloud.sleuth.Span; - -/** - * Customizer for {@link TraceListenerStrategy} client span. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -public interface TraceListenerStrategySpanCustomizer { - - /** - * Customizes the client database span. - * @param spanBuilder span builder - */ - void customizeConnectionSpan(T dataSource, Span.Builder spanBuilder); - - /** - * @param dataSource data source for which we're building the span - * @return {@code true} when this customizer can be applied - */ - boolean isApplicable(CommonDataSource dataSource); - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import javax.sql.CommonDataSource; + +import org.springframework.cloud.sleuth.Span; + +/** + * Customizer for {@link TraceListenerStrategy} client span. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface TraceListenerStrategySpanCustomizer { + + /** + * Customizes the client database span. + * @param spanBuilder span builder + */ + void customizeConnectionSpan(T dataSource, Span.Builder spanBuilder); + + /** + * @param dataSource data source for which we're building the span + * @return {@code true} when this customizer can be applied + */ + boolean isApplicable(CommonDataSource dataSource); + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceType.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceType.java index 2b74bec40..2348ae3fc 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceType.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceType.java @@ -1,39 +1,39 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.jdbc; - -/** - * Trace types. - */ -public enum TraceType { - - /** - * Connection. - */ - CONNECTION, - - /** - * Query. - */ - QUERY, - - /** - * Fetch. - */ - FETCH - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +/** + * Trace types. + */ +public enum TraceType { + + /** + * Related to JDBC connections. + */ + CONNECTION, + + /** + * Related to query executions. + */ + QUERY, + + /** + * Related to ResultSets. + */ + FETCH + +} diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index 110bfbfc2..e6e878707 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -45,6 +45,7 @@ spring-cloud-sleuth-instrumentation-feign-tests spring-cloud-sleuth-instrumentation-gateway-tests spring-cloud-sleuth-instrumentation-grpc-tests + spring-cloud-sleuth-instrumentation-jdbc-tests spring-cloud-sleuth-instrumentation-kafka-tests spring-cloud-sleuth-instrumentation-lettuce-tests spring-cloud-sleuth-instrumentation-messaging-tests diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/pom.xml new file mode 100644 index 000000000..af4f29188 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/pom.xml @@ -0,0 +1,114 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-jdbc-tests + jar + Spring Cloud Sleuth Brave Jdbc Instrumentation Tests + Spring Cloud Sleuth Brave Jdbc Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + + + + org.springframework.cloud + spring-cloud-sleuth-tests-common + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + runtime + + + org.apache.commons + commons-dbcp2 + runtime + + + org.apache.tomcat + tomcat-jdbc + runtime + + + com.zaxxer + HikariCP + runtime + + + p6spy + p6spy + runtime + + + net.ttddyy + datasource-proxy + runtime + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-test + + + io.zipkin.brave + brave-tests + + + org.awaitility + awaitility + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingJdbcEventListenerTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingJdbcEventListenerTests.java new file mode 100644 index 000000000..96b6c08aa --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingJdbcEventListenerTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.jdbc; + +import brave.sampler.Sampler; + +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +public class TracingJdbcEventListenerTests + extends org.springframework.cloud.sleuth.instrument.jdbc.TracingJdbcEventListenerTests { + + @Override + protected Class autoConfiguration() { + return BraveAutoConfiguration.class; + } + + @Override + protected Class testConfiguration() { + return Config.class; + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + TestSpanHandler testSpanHandlerSupplier(brave.test.TestSpanHandler testSpanHandler) { + return new BraveTestSpanHandler(testSpanHandler); + } + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + brave.test.TestSpanHandler braveTestSpanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingQueryExecutionListenerTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingQueryExecutionListenerTests.java new file mode 100644 index 000000000..c2a442054 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingQueryExecutionListenerTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.jdbc; + +import brave.sampler.Sampler; + +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +public class TracingQueryExecutionListenerTests + extends org.springframework.cloud.sleuth.instrument.jdbc.TracingQueryExecutionListenerTests { + + @Override + protected Class autoConfiguration() { + return BraveAutoConfiguration.class; + } + + @Override + protected Class testConfiguration() { + return Config.class; + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + TestSpanHandler testSpanHandlerSupplier(brave.test.TestSpanHandler testSpanHandler) { + return new BraveTestSpanHandler(testSpanHandler); + } + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + brave.test.TestSpanHandler braveTestSpanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/resources/application.yml b/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/resources/application.yml new file mode 100644 index 000000000..caf171df7 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/resources/application.yml @@ -0,0 +1,5 @@ +logging.level.org.springframework.cloud: DEBUG +logging.level.com.netflix.discovery.InstanceInfoReplicator: ERROR +logging.level.org.springframework.cloud.sleuth.brave.instrument.web.client.feign: TRACE + +spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration, org.springframework.cloud.gateway.config.GatewayAutoConfiguration, org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration, org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration \ No newline at end of file diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/view/Issue469Tests.java b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/view/Issue469Tests.java index 873bfba21..9f12664e7 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/view/Issue469Tests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/view/Issue469Tests.java @@ -1,59 +1,60 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.brave.instrument.web.view; - -import brave.test.TestSpanHandler; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.core.env.Environment; -import org.springframework.test.context.TestPropertySource; -import org.springframework.web.client.RestTemplate; - -import static org.assertj.core.api.BDDAssertions.then; - -@SpringBootTest(classes = Issue469.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@TestPropertySource(properties = { "spring.mvc.view.prefix=/WEB-INF/jsp/", "spring.mvc.view.suffix=.jsp" }) -public class Issue469Tests { - - @Autowired - TestSpanHandler spans; - - @Autowired - Environment environment; - - RestTemplate restTemplate = new RestTemplate(); - - @Test - public void should_not_result_in_tracing_exceptions_when_using_view_controllers() { - try { - this.restTemplate.getForObject("http://localhost:" + port() + "/welcome", String.class); - } - catch (Exception e) { - // JSPs are not rendered - then(e).hasMessageContaining("404"); - } - - then(this.spans).isNotEmpty(); - } - - private int port() { - return this.environment.getProperty("local.server.port", Integer.class); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.web.view; + +import brave.test.TestSpanHandler; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.test.context.TestPropertySource; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.BDDAssertions.then; + +@SpringBootTest(classes = Issue469.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = { "spring.mvc.view.prefix=/WEB-INF/jsp/", "spring.mvc.view.suffix=.jsp" }) +public class Issue469Tests { + + @Autowired + TestSpanHandler spans; + + @Autowired + Environment environment; + + RestTemplate restTemplate = new RestTemplate(); + + @Test + public void should_not_result_in_tracing_exceptions_when_using_view_controllers() { + try { + this.restTemplate.getForObject("http://localhost:" + port() + "/welcome", String.class); + } + catch (Exception e) { + // JSPs are not rendered + then(e).hasMessageContaining("404"); + } + + Awaitility.await().untilAsserted(() -> then(this.spans).isNotEmpty()); + } + + private int port() { + return this.environment.getProperty("local.server.port", Integer.class); + } + +} diff --git a/tests/common/pom.xml b/tests/common/pom.xml index f23cd0d2d..46fe1655d 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -189,6 +189,16 @@ spring-boot-starter-rsocket true + + p6spy + p6spy + true + + + net.ttddyy + datasource-proxy + true + org.springframework.cloud spring-cloud-sleuth-zipkin diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingJdbcEventListenerTests.java similarity index 81% rename from spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java rename to tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingJdbcEventListenerTests.java index ba2ad8b26..ac0cf1b03 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingJdbcEventListenerTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingJdbcEventListenerTests.java @@ -1,77 +1,79 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.sql.Connection; -import java.sql.PreparedStatement; - -import javax.sql.DataSource; - -import brave.handler.MutableSpan; -import brave.test.TestSpanHandler; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; - -import static org.assertj.core.api.Assertions.assertThat; - -class TracingJdbcEventListenerTests extends TracingListenerStrategyTests { - - private static final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, - TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url=jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") - .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); - - protected TracingJdbcEventListenerTests() { - super(contextRunner); - } - - @Test - void testShouldUsePlaceholderInSqlTagOfSpansForPreparedStatementIfIncludeParameterValuesIsSetToFalse() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.p6spy.tracing.include-parameter-values=false") - .run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - PreparedStatement preparedStatement = connection - .prepareStatement("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = ? WHERE 0 = ?"); - preparedStatement.setString(1, ""); - preparedStatement.setInt(2, 1); - preparedStatement.executeUpdate(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan connectionSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("update"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, - "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = ? WHERE 0 = ?"); - assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); - }); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +import javax.sql.DataSource; + +import brave.handler.MutableSpan; +import brave.test.TestSpanHandler; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.instrument.jdbc.TraceJdbcAutoConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +public abstract class TracingJdbcEventListenerTests extends TracingListenerStrategyTests { + + protected final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceJdbcAutoConfiguration.class, autoConfiguration(), + PropertyPlaceholderAutoConfiguration.class)) + .withUserConfiguration(testConfiguration()) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url=jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") + .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); + + @Override + ApplicationContextRunner parentContextRunner() { + return this.contextRunner; + } + + @Test + void testShouldUsePlaceholderInSqlTagOfSpansForPreparedStatementIfIncludeParameterValuesIsSetToFalse() { + contextRunner.withPropertyValues("spring.sleuth.jdbc.p6spy.tracing.include-parameter-values=false") + .run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + PreparedStatement preparedStatement = connection + .prepareStatement("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = ? WHERE 0 = ?"); + preparedStatement.setString(1, ""); + preparedStatement.setInt(2, 1); + preparedStatement.executeUpdate(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan connectionSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("update"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, + "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = ? WHERE 0 = ?"); + assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); + }); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingListenerStrategyTests.java similarity index 59% rename from spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java rename to tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingListenerStrategyTests.java index 9ff9efa2b..de201d45d 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingListenerStrategyTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingListenerStrategyTests.java @@ -1,745 +1,743 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import javax.sql.DataSource; - -import brave.handler.MutableSpan; -import brave.test.TestSpanHandler; -import com.zaxxer.hikari.HikariDataSource; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.test.context.assertj.AssertableApplicationContext; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; -import org.springframework.context.annotation.Bean; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -abstract class TracingListenerStrategyTests { - - public static final String SPAN_SQL_QUERY_TAG_NAME = "jdbc.query"; - - public static final String SPAN_ROW_COUNT_TAG_NAME = "jdbc.row-count"; - - protected final ApplicationContextRunner contextRunner; - - protected TracingListenerStrategyTests(ApplicationContextRunner contextRunner) { - this.contextRunner = contextRunner; - } - - @Test - void testShouldAddSpanForConnection() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - connection.commit(); - connection.rollback(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(1); - MutableSpan connectionSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(connectionSpan.remoteServiceName()).isEqualTo("TESTDB-BAZ"); - assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.commit"); - assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.rollback"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldAddSpanForConnectionWithFixedRemoteServiceName() { - contextRunner.withPropertyValues("spring.datasource.url:jdbc:h2:mem:testdb-baz?sleuthServiceName=aaaabbbb") - .run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - connection.commit(); - connection.rollback(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(1); - MutableSpan connectionSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(connectionSpan.remoteServiceName()).isEqualTo("aaaabbbb"); - assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.commit"); - assertThat(connectionSpan.annotations()).extracting("value").contains("jdbc.rollback"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldAddSpanForPreparedStatementExecute() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - connection.prepareStatement("SELECT NOW()").execute(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan connectionSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(statementSpan.remoteServiceName()).isEqualTo("TESTDB-BAZ"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldAddSpanForPreparedStatementExecuteUpdate() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - connection.prepareStatement("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1") - .executeUpdate(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan connectionSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("update"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, - "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); - assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldAddSpanForStatementExecuteUpdate() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - connection.createStatement() - .executeUpdate("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan connectionSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("update"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, - "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); - assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldAddSpanForPreparedStatementExecuteQueryIncludingTimeToCloseResultSet() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - ResultSet resultSet = connection.prepareStatement("SELECT NOW() UNION ALL select NOW()").executeQuery(); - resultSet.next(); - resultSet.next(); - resultSet.close(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(3); - MutableSpan connectionSpan = spanReporter.spans().get(2); - MutableSpan resultSetSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, - "SELECT NOW() UNION ALL select NOW()"); - assertThat(resultSetSpan.name()).isEqualTo("result-set"); - assertThat(resultSetSpan.remoteServiceName()).isEqualTo("TESTDB-BAZ"); - if (isP6Spy(context)) { - assertThat(resultSetSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "2"); - } - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldAddSpanForStatementAndResultSet() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - ResultSet resultSet = connection.createStatement().executeQuery("SELECT NOW()"); - resultSet.next(); - Thread.sleep(200L); - resultSet.close(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(3); - MutableSpan connectionSpan = spanReporter.spans().get(2); - MutableSpan resultSetSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(resultSetSpan.name()).isEqualTo("result-set"); - if (isP6Spy(context)) { - assertThat(resultSetSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "1"); - } - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenStatementIsClosedWihoutResultSet() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.next(); - statement.close(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(3); - MutableSpan connectionSpan = spanReporter.spans().get(2); - MutableSpan resultSetSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(resultSetSpan.name()).isEqualTo("result-set"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenConnectionIsClosedWihoutResultSet() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.next(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(3); - MutableSpan connectionSpan = spanReporter.spans().get(2); - MutableSpan resultSetSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(resultSetSpan.name()).isEqualTo("result-set"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenResultSetNextWasNotCalled() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(3); - MutableSpan connectionSpan = spanReporter.spans().get(2); - MutableSpan resultSetSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(resultSetSpan.name()).isEqualTo("result-set"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenResourceIsAlreadyClosed() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.next(); - resultSet.close(); - resultSet.close(); - statement.close(); - statement.close(); - connection.close(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(3); - MutableSpan connectionSpan = spanReporter.spans().get(2); - MutableSpan resultSetSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(resultSetSpan.name()).isEqualTo("result-set"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenResourceIsAlreadyClosed2() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - assertThatThrownBy(() -> { - connection.close(); - connection.prepareStatement("SELECT NOW()"); - }).isInstanceOf(SQLException.class); - - assertThat(spanReporter.spans()).hasSize(1); - MutableSpan connectionSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenResourceIsAlreadyClosed3() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - assertThatThrownBy(() -> { - statement.close(); - statement.executeQuery("SELECT NOW()"); - }).isInstanceOf(SQLException.class); - connection.close(); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenResourceIsAlreadyClosed4() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - assertThatThrownBy(() -> { - resultSet.close(); - resultSet.next(); - }).isInstanceOf(SQLException.class); - statement.close(); - connection.close(); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailToCloseSpanForTwoConsecutiveConnections() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection1 = dataSource.getConnection(); - Connection connection2 = dataSource.getConnection(); - connection2.close(); - connection1.close(); - - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan connection1Span = spanReporter.spans().get(0); - MutableSpan connection2Span = spanReporter.spans().get(1); - assertThat(connection1Span.name()).isEqualTo("connection"); - assertThat(connection2Span.name()).isEqualTo("connection"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenClosedInReversedOrder() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.next(); - connection.close(); - statement.close(); - resultSet.close(); - - assertThat(spanReporter.spans()).hasSize(3); - MutableSpan connectionSpan = spanReporter.spans().get(2); - MutableSpan resultSetSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(resultSetSpan.name()).isEqualTo("result-set"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - @SuppressWarnings("unchecked") - void testShouldNotCauseMemoryLeakOnTomcatPool() { - contextRunner.withPropertyValues("spring.datasource.type:org.apache.tomcat.jdbc.pool.DataSource") - .run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - Object listener = isP6Spy(context) ? context.getBean(TraceJdbcEventListener.class) - : context.getBean(TraceQueryExecutionListener.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(listener).extracting("strategy").extracting("openConnections") - .isInstanceOfSatisfying(Map.class, map -> assertThat(map).isEmpty()); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - private boolean isP6Spy(AssertableApplicationContext context) { - if (context.getBeansOfType(TraceJdbcEventListener.class).size() == 1) { - return true; - } - else if (context.getBeansOfType(TraceQueryExecutionListener.class).size() == 1) { - return false; - } - else { - throw new IllegalStateException("Expected exactly 1 tracing listener bean in the context."); - } - } - - @Test - void testSingleConnectionAcrossMultipleThreads() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - IntStream.range(0, 5).mapToObj(i -> CompletableFuture.runAsync(() -> { - try { - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.next(); - statement.close(); - resultSet.close(); - } - catch (SQLException e) { - throw new IllegalStateException(e); - } - })).collect(Collectors.toList()).forEach(CompletableFuture::join); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(1 + 2 * 5); - assertThat(spanReporter.spans()).extracting("name").contains("select", "result-set", "connection"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldIncludeOnlyConnectionTraces() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.includes: connection").run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(1); - MutableSpan connectionSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldIncludeOnlyQueryTraces() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.includes: query").run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldIncludeOnlyFetchTraces() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.includes: fetch").run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(1); - MutableSpan resultSetSpan = spanReporter.spans().get(0); - assertThat(resultSetSpan.name()).isEqualTo("result-set"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldIncludeOnlyConnectionAndQueryTraces() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.includes: connection, query").run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan connectionSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldIncludeOnlyConnectionAndFetchTraces() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.includes: connection, fetch").run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan connectionSpan = spanReporter.spans().get(1); - MutableSpan resultSetSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(resultSetSpan.name()).isEqualTo("result-set"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldIncludeOnlyQueryAndFetchTraces() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.includes: query, fetch").run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan resultSetSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(resultSetSpan.name()).isEqualTo("result-set"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotOverrideExceptionWhenConnectionWasClosedBeforeExecutingQuery() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - PreparedStatement statement = connection.prepareStatement("SELECT NOW()"); - connection.close(); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - - assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class); - - assertThat(spanReporter.spans()).hasSize(1); - MutableSpan connectionSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotOverrideExceptionWhenStatementWasClosedBeforeExecutingQuery() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - PreparedStatement statement = connection.prepareStatement("SELECT NOW()"); - statement.close(); - assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan connectionSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotOverrideExceptionWhenResultSetWasClosedBeforeNext() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.close(); - assertThatThrownBy(resultSet::next).isInstanceOf(SQLException.class); - statement.close(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(3); - MutableSpan connectionSpan = spanReporter.spans().get(2); - MutableSpan resultSetSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("select"); - assertThat(resultSetSpan.name()).isEqualTo("result-set"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenClosingConnectionFromDifferentDataSource() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(MultiDataSourceConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource1 = context.getBean("test1", DataSource.class); - DataSource dataSource2 = context.getBean("test2", DataSource.class); - - dataSource1.getConnection().close(); - dataSource2.getConnection().close(); - - CompletableFuture future = CompletableFuture.runAsync(() -> { - try { - Connection connection1 = dataSource1.getConnection(); - PreparedStatement statement = connection1.prepareStatement("SELECT NOW()"); - ResultSet resultSet = statement.executeQuery(); - Thread.sleep(200); - resultSet.close(); - statement.close(); - connection1.close(); - } - catch (SQLException | InterruptedException e) { - throw new IllegalStateException(e); - } - }); - Thread.sleep(100); - Connection connection2 = dataSource2.getConnection(); - Thread.sleep(300); - connection2.close(); - - future.join(); - }); - } - - private static class MultiDataSourceConfiguration { - - @Bean - public HikariDataSource test1() { - HikariDataSource dataSource = new HikariDataSource(); - dataSource.setJdbcUrl("jdbc:h2:mem:testdb-1-foo"); - dataSource.setPoolName("test1"); - return dataSource; - } - - @Bean - public HikariDataSource test2() { - HikariDataSource dataSource = new HikariDataSource(); - dataSource.setJdbcUrl("jdbc:h2:mem:testdb-2-bar"); - dataSource.setPoolName("test2"); - return dataSource; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +abstract class TracingListenerStrategyTests { + + public static final String SPAN_SQL_QUERY_TAG_NAME = "jdbc.query"; + + public static final String SPAN_ROW_COUNT_TAG_NAME = "jdbc.row-count"; + + abstract ApplicationContextRunner parentContextRunner(); + + protected abstract Class autoConfiguration(); + + protected abstract Class testConfiguration(); + + @Test + void testShouldAddSpanForConnection() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.commit(); + connection.rollback(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(connectionSpan.getRemoteServiceName()).isEqualTo("TESTDB-BAZ"); + assertThat(connectionSpan.getEvents()).extracting("value").contains("jdbc.commit"); + assertThat(connectionSpan.getEvents()).extracting("value").contains("jdbc.rollback"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldAddSpanForConnectionWithFixedRemoteServiceName() { + parentContextRunner().withPropertyValues("spring.datasource.url:jdbc:h2:mem:testdb-baz?sleuthServiceName=aaaabbbb") + .run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.commit(); + connection.rollback(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(connectionSpan.getRemoteServiceName()).isEqualTo("aaaabbbb"); + assertThat(connectionSpan.getEvents()).extracting("value").contains("jdbc.commit"); + assertThat(connectionSpan.getEvents()).extracting("value").contains("jdbc.rollback"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldAddSpanForPreparedStatementExecute() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.prepareStatement("SELECT NOW()").execute(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(statementSpan.getRemoteServiceName()).isEqualTo("TESTDB-BAZ"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldAddSpanForPreparedStatementExecuteUpdate() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.prepareStatement("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1") + .executeUpdate(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("update"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, + "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldAddSpanForStatementExecuteUpdate() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.createStatement() + .executeUpdate("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("update"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, + "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldAddSpanForPreparedStatementExecuteQueryIncludingTimeToCloseResultSet() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + ResultSet resultSet = connection.prepareStatement("SELECT NOW() UNION ALL select NOW()").executeQuery(); + resultSet.next(); + resultSet.next(); + resultSet.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, + "SELECT NOW() UNION ALL select NOW()"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(resultSetSpan.getRemoteServiceName()).isEqualTo("TESTDB-BAZ"); + if (isP6Spy(context)) { + assertThat(resultSetSpan.getTags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "2"); + } + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldAddSpanForStatementAndResultSet() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + ResultSet resultSet = connection.createStatement().executeQuery("SELECT NOW()"); + resultSet.next(); + Thread.sleep(200L); + resultSet.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + if (isP6Spy(context)) { + assertThat(resultSetSpan.getTags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "1"); + } + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenStatementIsClosedWihoutResultSet() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenConnectionIsClosedWihoutResultSet() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenResultSetNextWasNotCalled() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenResourceIsAlreadyClosed() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + resultSet.close(); + resultSet.close(); + statement.close(); + statement.close(); + connection.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenResourceIsAlreadyClosed2() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + assertThatThrownBy(() -> { + connection.close(); + connection.prepareStatement("SELECT NOW()"); + }).isInstanceOf(SQLException.class); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenResourceIsAlreadyClosed3() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + assertThatThrownBy(() -> { + statement.close(); + statement.executeQuery("SELECT NOW()"); + }).isInstanceOf(SQLException.class); + connection.close(); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenResourceIsAlreadyClosed4() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + assertThatThrownBy(() -> { + resultSet.close(); + resultSet.next(); + }).isInstanceOf(SQLException.class); + statement.close(); + connection.close(); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailToCloseSpanForTwoConsecutiveConnections() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection1 = dataSource.getConnection(); + Connection connection2 = dataSource.getConnection(); + connection2.close(); + connection1.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connection1Span = spanReporter.reportedSpans().get(0); + FinishedSpan connection2Span = spanReporter.reportedSpans().get(1); + assertThat(connection1Span.getName()).isEqualTo("connection"); + assertThat(connection2Span.getName()).isEqualTo("connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenClosedInReversedOrder() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + connection.close(); + statement.close(); + resultSet.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + @SuppressWarnings("unchecked") + void testShouldNotCauseMemoryLeakOnTomcatPool() { + parentContextRunner().withPropertyValues("spring.datasource.type:org.apache.tomcat.jdbc.pool.DataSource") + .run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + Object listener = isP6Spy(context) ? context.getBean(TraceJdbcEventListener.class) + : context.getBean(TraceQueryExecutionListener.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(listener).extracting("strategy").extracting("openConnections") + .isInstanceOfSatisfying(Map.class, map -> assertThat(map).isEmpty()); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + private boolean isP6Spy(AssertableApplicationContext context) { + if (context.getBeansOfType(TraceJdbcEventListener.class).size() == 1) { + return true; + } + else if (context.getBeansOfType(TraceQueryExecutionListener.class).size() == 1) { + return false; + } + else { + throw new IllegalStateException("Expected exactly 1 tracing listener bean in the context."); + } + } + + @Test + void testSingleConnectionAcrossMultipleThreads() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + IntStream.range(0, 5).mapToObj(i -> CompletableFuture.runAsync(() -> { + try { + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + statement.close(); + resultSet.close(); + } + catch (SQLException e) { + throw new IllegalStateException(e); + } + })).collect(Collectors.toList()).forEach(CompletableFuture::join); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(1 + 2 * 5); + assertThat(spanReporter.reportedSpans()).extracting("name").contains("select", "result-set", "connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldIncludeOnlyConnectionTraces() { + parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: connection").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldIncludeOnlyQueryTraces() { + parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: query").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldIncludeOnlyFetchTraces() { + parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: fetch").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(0); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldIncludeOnlyConnectionAndQueryTraces() { + parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: connection, query").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldIncludeOnlyConnectionAndFetchTraces() { + parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: connection, fetch").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldIncludeOnlyQueryAndFetchTraces() { + parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: query, fetch").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotOverrideExceptionWhenConnectionWasClosedBeforeExecutingQuery() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT NOW()"); + connection.close(); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + + assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotOverrideExceptionWhenStatementWasClosedBeforeExecutingQuery() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT NOW()"); + statement.close(); + assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotOverrideExceptionWhenResultSetWasClosedBeforeNext() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.close(); + assertThatThrownBy(resultSet::next).isInstanceOf(SQLException.class); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenClosingConnectionFromDifferentDataSource() { + ApplicationContextRunner contextRunner = parentContextRunner() + .withUserConfiguration(MultiDataSourceConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource1 = context.getBean("test1", DataSource.class); + DataSource dataSource2 = context.getBean("test2", DataSource.class); + + dataSource1.getConnection().close(); + dataSource2.getConnection().close(); + + CompletableFuture future = CompletableFuture.runAsync(() -> { + try { + Connection connection1 = dataSource1.getConnection(); + PreparedStatement statement = connection1.prepareStatement("SELECT NOW()"); + ResultSet resultSet = statement.executeQuery(); + Thread.sleep(200); + resultSet.close(); + statement.close(); + connection1.close(); + } + catch (SQLException | InterruptedException e) { + throw new IllegalStateException(e); + } + }); + Thread.sleep(100); + Connection connection2 = dataSource2.getConnection(); + Thread.sleep(300); + connection2.close(); + + future.join(); + }); + } + + private static class MultiDataSourceConfiguration { + + @Bean + public HikariDataSource test1() { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setJdbcUrl("jdbc:h2:mem:testdb-1-foo"); + dataSource.setPoolName("test1"); + return dataSource; + } + + @Bean + public HikariDataSource test2() { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setJdbcUrl("jdbc:h2:mem:testdb-2-bar"); + dataSource.setPoolName("test2"); + return dataSource; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingQueryExecutionListenerTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingQueryExecutionListenerTests.java similarity index 67% rename from spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingQueryExecutionListenerTests.java rename to tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingQueryExecutionListenerTests.java index 89e7c601a..b54480927 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TracingQueryExecutionListenerTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingQueryExecutionListenerTests.java @@ -1,38 +1,39 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; - -class TracingQueryExecutionListenerTests extends TracingListenerStrategyTests { - - TracingQueryExecutionListenerTests() { - super(new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceDataSourceDecoratorAutoConfiguration.class, BraveAutoConfiguration.class, - TestSpanHandlerConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") - .withClassLoader(new FilteredClassLoader("com.p6spy"))); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.instrument.jdbc.TraceJdbcAutoConfiguration; + +public abstract class TracingQueryExecutionListenerTests extends TracingListenerStrategyTests { + + @Override + ApplicationContextRunner parentContextRunner() { + return new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceJdbcAutoConfiguration.class, autoConfiguration(), PropertyPlaceholderAutoConfiguration.class)) + .withUserConfiguration(testConfiguration()) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") + .withClassLoader(new FilteredClassLoader("com.p6spy")); + } + +} From 685fb727b5798facd23d486ba9e111ff2c26eee1 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 27 May 2021 14:05:39 +0200 Subject: [PATCH 40/59] Polish --- .../TraceAsyncDefaultAutoConfiguration.java | 306 ++-- .../jdbc/DataSourceProxyConfiguration.java | 223 ++- .../instrument/jdbc/P6SpyConfiguration.java | 156 +- .../jdbc/P6SpyPropertiesSetter.java | 375 ++--- ...eDataSourceDecoratorBeanPostProcessor.java | 280 ++-- .../jdbc/TraceJdbcAutoConfiguration.java | 141 +- .../instrument/jdbc/TraceJdbcProperties.java | 1022 +++++------ .../TraceNoOpAutoConfiguration.java | 207 ++- ...SourceDecoratorAutoConfigurationTests.java | 741 ++++---- .../jdbc/P6SpyConfigurationTests.java | 558 +++---- .../ProxyDataSourceConfigurationTests.java | 477 +++--- ...thP6SpyListenerAutoConfigurationTests.java | 131 +- ...aSourceListenerAutoConfigurationTests.java | 137 +- .../TraceListenerStrategySpanCustomizer.java | 86 +- .../sleuth/instrument/jdbc/TraceType.java | 78 +- .../jdbc/TracingJdbcEventListenerTests.java | 120 +- .../TracingQueryExecutionListenerTests.java | 120 +- .../web/TraceAsyncIntegrationTests.java | 475 +++--- .../instrument/web/view/Issue469Tests.java | 120 +- .../jdbc/TracingJdbcEventListenerTests.java | 157 +- .../jdbc/TracingListenerStrategyTests.java | 1487 +++++++++-------- .../TracingQueryExecutionListenerTests.java | 79 +- 22 files changed, 3736 insertions(+), 3740 deletions(-) diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfiguration.java index 72feab40a..18fb2fbdf 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfiguration.java @@ -1,153 +1,153 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.async; - -import java.util.concurrent.Executor; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.aop.interceptor.AsyncExecutionAspectSupport; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.beans.factory.NoUniqueBeanDefinitionException; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.sleuth.SpanNamer; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.async.LazyTraceExecutor; -import org.springframework.cloud.sleuth.instrument.async.TraceAsyncAspect; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Role; -import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.core.task.TaskExecutor; -import org.springframework.scheduling.annotation.AsyncConfigurer; -import org.springframework.scheduling.annotation.AsyncConfigurerSupport; - -/** - * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration - * Auto-configuration} enabling async related processing. - * - * @author Dave Syer - * @author Marcin Grzejszczak - * @since 1.0.0 - * @see LazyTraceExecutor - * @see TraceAsyncAspect - */ -@Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(SleuthAsyncProperties.class) -@ConditionalOnProperty(value = "spring.sleuth.async.enabled", matchIfMissing = true) -@ConditionalOnBean(Tracer.class) -@AutoConfigureAfter(BraveAutoConfiguration.class) -public class TraceAsyncDefaultAutoConfiguration { - - @Bean - @ConditionalOnProperty(value = "spring.sleuth.scheduled.enabled", matchIfMissing = true) - static ExecutorBeanPostProcessor executorBeanPostProcessor(BeanFactory beanFactory) { - return new ExecutorBeanPostProcessor(beanFactory); - } - - @Bean - TraceAsyncAspect traceAsyncAspect(Tracer tracer, SpanNamer spanNamer) { - return new TraceAsyncAspect(tracer, spanNamer); - } - - /** - * Wrapper for the async executor. - */ - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(AsyncConfigurer.class) - @ConditionalOnMissingBean(AsyncConfigurer.class) - @ConditionalOnProperty(value = "spring.sleuth.async.configurer.enabled", matchIfMissing = true) - @Role(BeanDefinition.ROLE_INFRASTRUCTURE) - static class DefaultAsyncConfigurerSupport extends AsyncConfigurerSupport { - - private static final Log log = LogFactory.getLog(DefaultAsyncConfigurerSupport.class); - - @Autowired - private BeanFactory beanFactory; - - @Override - public Executor getAsyncExecutor() { - Executor delegate = getDefaultExecutor(); - return new LazyTraceExecutor(this.beanFactory, delegate); - } - - /** - * Retrieve or build a default executor for this advice instance. An executor - * returned from here will be cached for further use. - *

- * The default implementation searches for a unique {@link TaskExecutor} bean in - * the context, or for an {@link Executor} bean named "taskExecutor" otherwise. If - * neither of the two is resolvable, this implementation will return {@code null}. - * @return the default executor, or {@code null} if none available - * @see AsyncExecutionAspectSupport#getDefaultExecutor(org.springframework.beans.factory.BeanFactory) - */ - private Executor getDefaultExecutor() { - try { - // Search for TaskExecutor bean... not plain Executor since that would - // match with ScheduledExecutorService as well, which is unusable for - // our purposes here. TaskExecutor is more clearly designed for it. - return this.beanFactory.getBean(TaskExecutor.class); - } - catch (NoUniqueBeanDefinitionException ex) { - if (log.isDebugEnabled()) { - log.debug("Could not find unique TaskExecutor bean", ex); - } - try { - return this.beanFactory.getBean(AsyncExecutionAspectSupport.DEFAULT_TASK_EXECUTOR_BEAN_NAME, - Executor.class); - } - catch (NoSuchBeanDefinitionException ex2) { - if (log.isInfoEnabled()) { - log.info("More than one TaskExecutor bean found within the context, and none is named " - + "'taskExecutor'. Mark one of them as primary or name it 'taskExecutor' (possibly " - + "as an alias) in order to use it for async processing: " + ex.getBeanNamesFound()); - } - } - } - catch (NoSuchBeanDefinitionException ex) { - log.debug("Could not find default TaskExecutor bean", ex); - try { - return this.beanFactory.getBean(AsyncExecutionAspectSupport.DEFAULT_TASK_EXECUTOR_BEAN_NAME, - Executor.class); - } - catch (NoSuchBeanDefinitionException ex2) { - log.info("No task executor bean found for async processing: " - + "no bean of type TaskExecutor and no bean named 'taskExecutor' either"); - } - // Giving up -> either using local default executor or none at all... - } - // backward compatibility - if (log.isInfoEnabled()) { - log.info( - "For backward compatibility, will fallback to the default, SimpleAsyncTaskExecutor implementation"); - } - return new SimpleAsyncTaskExecutor(); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.async; + +import java.util.concurrent.Executor; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.interceptor.AsyncExecutionAspectSupport; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.sleuth.SpanNamer; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.async.LazyTraceExecutor; +import org.springframework.cloud.sleuth.instrument.async.TraceAsyncAspect; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.AsyncConfigurerSupport; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} enabling async related processing. + * + * @author Dave Syer + * @author Marcin Grzejszczak + * @since 1.0.0 + * @see LazyTraceExecutor + * @see TraceAsyncAspect + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(SleuthAsyncProperties.class) +@ConditionalOnProperty(value = "spring.sleuth.async.enabled", matchIfMissing = true) +@ConditionalOnBean(Tracer.class) +@AutoConfigureAfter(BraveAutoConfiguration.class) +public class TraceAsyncDefaultAutoConfiguration { + + @Bean + @ConditionalOnProperty(value = "spring.sleuth.scheduled.enabled", matchIfMissing = true) + static ExecutorBeanPostProcessor executorBeanPostProcessor(BeanFactory beanFactory) { + return new ExecutorBeanPostProcessor(beanFactory); + } + + @Bean + TraceAsyncAspect traceAsyncAspect(Tracer tracer, SpanNamer spanNamer) { + return new TraceAsyncAspect(tracer, spanNamer); + } + + /** + * Wrapper for the async executor. + */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(AsyncConfigurer.class) + @ConditionalOnMissingBean(AsyncConfigurer.class) + @ConditionalOnProperty(value = "spring.sleuth.async.configurer.enabled", matchIfMissing = true) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static class DefaultAsyncConfigurerSupport extends AsyncConfigurerSupport { + + private static final Log log = LogFactory.getLog(DefaultAsyncConfigurerSupport.class); + + @Autowired + private BeanFactory beanFactory; + + @Override + public Executor getAsyncExecutor() { + Executor delegate = getDefaultExecutor(); + return new LazyTraceExecutor(this.beanFactory, delegate); + } + + /** + * Retrieve or build a default executor for this advice instance. An executor + * returned from here will be cached for further use. + *

+ * The default implementation searches for a unique {@link TaskExecutor} bean in + * the context, or for an {@link Executor} bean named "taskExecutor" otherwise. If + * neither of the two is resolvable, this implementation will return {@code null}. + * @return the default executor, or {@code null} if none available + * @see AsyncExecutionAspectSupport#getDefaultExecutor(org.springframework.beans.factory.BeanFactory) + */ + private Executor getDefaultExecutor() { + try { + // Search for TaskExecutor bean... not plain Executor since that would + // match with ScheduledExecutorService as well, which is unusable for + // our purposes here. TaskExecutor is more clearly designed for it. + return this.beanFactory.getBean(TaskExecutor.class); + } + catch (NoUniqueBeanDefinitionException ex) { + if (log.isDebugEnabled()) { + log.debug("Could not find unique TaskExecutor bean", ex); + } + try { + return this.beanFactory.getBean(AsyncExecutionAspectSupport.DEFAULT_TASK_EXECUTOR_BEAN_NAME, + Executor.class); + } + catch (NoSuchBeanDefinitionException ex2) { + if (log.isInfoEnabled()) { + log.info("More than one TaskExecutor bean found within the context, and none is named " + + "'taskExecutor'. Mark one of them as primary or name it 'taskExecutor' (possibly " + + "as an alias) in order to use it for async processing: " + ex.getBeanNamesFound()); + } + } + } + catch (NoSuchBeanDefinitionException ex) { + log.debug("Could not find default TaskExecutor bean", ex); + try { + return this.beanFactory.getBean(AsyncExecutionAspectSupport.DEFAULT_TASK_EXECUTOR_BEAN_NAME, + Executor.class); + } + catch (NoSuchBeanDefinitionException ex2) { + log.info("No task executor bean found for async processing: " + + "no bean of type TaskExecutor and no bean named 'taskExecutor' either"); + } + // Giving up -> either using local default executor or none at all... + } + // backward compatibility + if (log.isInfoEnabled()) { + log.info( + "For backward compatibility, will fallback to the default, SimpleAsyncTaskExecutor implementation"); + } + return new SimpleAsyncTaskExecutor(); + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java index 944e74fa7..876efc38c 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java @@ -1,112 +1,111 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.ArrayList; -import java.util.List; - -import net.ttddyy.dsproxy.listener.MethodExecutionListener; -import net.ttddyy.dsproxy.listener.QueryCountStrategy; -import net.ttddyy.dsproxy.listener.QueryExecutionListener; -import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; -import net.ttddyy.dsproxy.proxy.ResultSetProxyLogicFactory; -import net.ttddyy.dsproxy.proxy.SimpleResultSetProxyLogicFactory; -import net.ttddyy.dsproxy.support.ProxyDataSource; -import net.ttddyy.dsproxy.transform.ParameterTransformer; -import net.ttddyy.dsproxy.transform.QueryTransformer; - -import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyBuilderCustomizer; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyDataSourceDecorator; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyProperties; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; -import org.springframework.context.annotation.Bean; - -/** - * Configuration for integration with datasource-proxy, allows to use define custom - * {@link QueryExecutionListener}, {@link ParameterTransformer} and - * {@link QueryTransformer}. - * - * @author Arthur Gavlyukovskiy - */ -@ConditionalOnClass(ProxyDataSource.class) -@ConditionalOnProperty(name = "spring.sleuth.jdbc.datasource-proxy.enabled", havingValue = "true", matchIfMissing = true) -class DataSourceProxyConfiguration { - - @Bean - @ConditionalOnMissingBean - DataSourceProxyConnectionIdManagerProvider traceConnectionIdManagerProvider() { - return GlobalConnectionIdManager::new; - } - - @Bean - DataSourceProxyBuilderCustomizer proxyDataSourceBuilderConfigurer( - ObjectProvider queryCountStrategy, - ObjectProvider> listeners, - ObjectProvider> methodExecutionListeners, - ObjectProvider parameterTransformer, - ObjectProvider queryTransformer, - ObjectProvider resultSetProxyLogicFactory, - ObjectProvider dataSourceProxyConnectionIdManagerProvider, - TraceJdbcProperties traceJdbcProperties) { - return new DataSourceProxyBuilderCustomizer(queryCountStrategy.getIfAvailable(() -> null), - listeners.getIfAvailable(() -> null), methodExecutionListeners.getIfAvailable(() -> null), - parameterTransformer.getIfAvailable(() -> null), queryTransformer.getIfAvailable(() -> null), - resultSetProxyLogicFactory.getIfAvailable(() -> null), - dataSourceProxyConnectionIdManagerProvider.getIfAvailable(() -> null), - props(traceJdbcProperties)); - } - - private DataSourceProxyProperties props(TraceJdbcProperties traceJdbcProperties) { - TraceJdbcProperties.DataSourceProxyProperties originalProxy = traceJdbcProperties - .getDatasourceProxy(); - DataSourceProxyProperties props = new DataSourceProxyProperties(); - BeanUtils.copyProperties(originalProxy, props); - props.setLogging(DataSourceProxyProperties.DataSourceProxyLogging.valueOf(originalProxy.getLogging().name())); - return props; - } - - @Bean - DataSourceProxyDataSourceDecorator proxyDataSourceDecorator( - DataSourceProxyBuilderCustomizer dataSourceProxyBuilderCustomizer, - DataSourceNameResolver dataSourceNameResolver) { - return new DataSourceProxyDataSourceDecorator(dataSourceProxyBuilderCustomizer, dataSourceNameResolver); - } - - @Bean - TraceQueryExecutionListener traceQueryExecutionListener(Tracer tracer, - TraceJdbcProperties dataSourceDecoratorProperties, - ObjectProvider> customizers) { - return new TraceQueryExecutionListener(tracer, dataSourceDecoratorProperties.getIncludes(), - customizers.getIfAvailable(ArrayList::new)); - } - - @Bean - @ConditionalOnMissingBean - ResultSetProxyLogicFactory traceResultSetProxyLogicFactory() { - return new SimpleResultSetProxyLogicFactory(); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.ArrayList; +import java.util.List; + +import net.ttddyy.dsproxy.listener.MethodExecutionListener; +import net.ttddyy.dsproxy.listener.QueryCountStrategy; +import net.ttddyy.dsproxy.listener.QueryExecutionListener; +import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; +import net.ttddyy.dsproxy.proxy.ResultSetProxyLogicFactory; +import net.ttddyy.dsproxy.proxy.SimpleResultSetProxyLogicFactory; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import net.ttddyy.dsproxy.transform.ParameterTransformer; +import net.ttddyy.dsproxy.transform.QueryTransformer; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyBuilderCustomizer; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyDataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyProperties; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; +import org.springframework.context.annotation.Bean; + +/** + * Configuration for integration with datasource-proxy, allows to use define custom + * {@link QueryExecutionListener}, {@link ParameterTransformer} and + * {@link QueryTransformer}. + * + * @author Arthur Gavlyukovskiy + */ +@ConditionalOnClass(ProxyDataSource.class) +@ConditionalOnProperty(name = "spring.sleuth.jdbc.datasource-proxy.enabled", havingValue = "true", + matchIfMissing = true) +class DataSourceProxyConfiguration { + + @Bean + @ConditionalOnMissingBean + DataSourceProxyConnectionIdManagerProvider traceConnectionIdManagerProvider() { + return GlobalConnectionIdManager::new; + } + + @Bean + DataSourceProxyBuilderCustomizer proxyDataSourceBuilderConfigurer( + ObjectProvider queryCountStrategy, + ObjectProvider> listeners, + ObjectProvider> methodExecutionListeners, + ObjectProvider parameterTransformer, + ObjectProvider queryTransformer, + ObjectProvider resultSetProxyLogicFactory, + ObjectProvider dataSourceProxyConnectionIdManagerProvider, + TraceJdbcProperties traceJdbcProperties) { + return new DataSourceProxyBuilderCustomizer(queryCountStrategy.getIfAvailable(() -> null), + listeners.getIfAvailable(() -> null), methodExecutionListeners.getIfAvailable(() -> null), + parameterTransformer.getIfAvailable(() -> null), queryTransformer.getIfAvailable(() -> null), + resultSetProxyLogicFactory.getIfAvailable(() -> null), + dataSourceProxyConnectionIdManagerProvider.getIfAvailable(() -> null), props(traceJdbcProperties)); + } + + private DataSourceProxyProperties props(TraceJdbcProperties traceJdbcProperties) { + TraceJdbcProperties.DataSourceProxyProperties originalProxy = traceJdbcProperties.getDatasourceProxy(); + DataSourceProxyProperties props = new DataSourceProxyProperties(); + BeanUtils.copyProperties(originalProxy, props); + props.setLogging(DataSourceProxyProperties.DataSourceProxyLogging.valueOf(originalProxy.getLogging().name())); + return props; + } + + @Bean + DataSourceProxyDataSourceDecorator proxyDataSourceDecorator( + DataSourceProxyBuilderCustomizer dataSourceProxyBuilderCustomizer, + DataSourceNameResolver dataSourceNameResolver) { + return new DataSourceProxyDataSourceDecorator(dataSourceProxyBuilderCustomizer, dataSourceNameResolver); + } + + @Bean + TraceQueryExecutionListener traceQueryExecutionListener(Tracer tracer, + TraceJdbcProperties dataSourceDecoratorProperties, + ObjectProvider> customizers) { + return new TraceQueryExecutionListener(tracer, dataSourceDecoratorProperties.getIncludes(), + customizers.getIfAvailable(ArrayList::new)); + } + + @Bean + @ConditionalOnMissingBean + ResultSetProxyLogicFactory traceResultSetProxyLogicFactory() { + return new SimpleResultSetProxyLogicFactory(); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfiguration.java index b38bd2d85..68f05063e 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfiguration.java @@ -1,78 +1,78 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.ArrayList; -import java.util.List; - -import com.p6spy.engine.event.JdbcEventListener; -import com.p6spy.engine.spy.DefaultJdbcEventListenerFactory; -import com.p6spy.engine.spy.JdbcEventListenerFactory; -import com.p6spy.engine.spy.P6DataSource; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; -import org.springframework.cloud.sleuth.instrument.jdbc.P6SpyContextJdbcEventListenerFactory; -import org.springframework.cloud.sleuth.instrument.jdbc.P6SpyDataSourceDecorator; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; - -/** - * Configuration for integration with p6spy, allows to define custom - * {@link JdbcEventListener}. - * - * @author Arthur Gavlyukovskiy - */ -@ConditionalOnClass(P6DataSource.class) -@ConditionalOnProperty(name = "spring.sleuth.jdbc.p6spy.enabled", havingValue = "true", matchIfMissing = true) -class P6SpyConfiguration { - - @Bean - static P6SpyPropertiesSetter p6SpyPropertiesSetter(ConfigurableApplicationContext context) { - return new P6SpyPropertiesSetter(context); - } - - @Bean - @ConditionalOnMissingBean - JdbcEventListenerFactory traceJdbcEventListenerFactory(ObjectProvider> listeners) { - JdbcEventListenerFactory jdbcEventListenerFactory = new DefaultJdbcEventListenerFactory(); - List listenerList = listeners.getIfAvailable(() -> null); - return listenerList != null ? new P6SpyContextJdbcEventListenerFactory(jdbcEventListenerFactory, listenerList) - : jdbcEventListenerFactory; - } - - @Bean - P6SpyDataSourceDecorator p6SpyDataSourceDecorator(JdbcEventListenerFactory jdbcEventListenerFactory) { - return new P6SpyDataSourceDecorator(jdbcEventListenerFactory); - } - - @Bean - TraceJdbcEventListener tracingJdbcEventListener(Tracer tracer, DataSourceNameResolver dataSourceNameResolver, - TraceJdbcProperties traceJdbcProperties, - ObjectProvider> customizers) { - return new TraceJdbcEventListener(tracer, dataSourceNameResolver, traceJdbcProperties.getIncludes(), - traceJdbcProperties.getP6spy().getTracing().isIncludeParameterValues(), - customizers.getIfAvailable(ArrayList::new)); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.ArrayList; +import java.util.List; + +import com.p6spy.engine.event.JdbcEventListener; +import com.p6spy.engine.spy.DefaultJdbcEventListenerFactory; +import com.p6spy.engine.spy.JdbcEventListenerFactory; +import com.p6spy.engine.spy.P6DataSource; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; +import org.springframework.cloud.sleuth.instrument.jdbc.P6SpyContextJdbcEventListenerFactory; +import org.springframework.cloud.sleuth.instrument.jdbc.P6SpyDataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; + +/** + * Configuration for integration with p6spy, allows to define custom + * {@link JdbcEventListener}. + * + * @author Arthur Gavlyukovskiy + */ +@ConditionalOnClass(P6DataSource.class) +@ConditionalOnProperty(name = "spring.sleuth.jdbc.p6spy.enabled", havingValue = "true", matchIfMissing = true) +class P6SpyConfiguration { + + @Bean + static P6SpyPropertiesSetter p6SpyPropertiesSetter(ConfigurableApplicationContext context) { + return new P6SpyPropertiesSetter(context); + } + + @Bean + @ConditionalOnMissingBean + JdbcEventListenerFactory traceJdbcEventListenerFactory(ObjectProvider> listeners) { + JdbcEventListenerFactory jdbcEventListenerFactory = new DefaultJdbcEventListenerFactory(); + List listenerList = listeners.getIfAvailable(() -> null); + return listenerList != null ? new P6SpyContextJdbcEventListenerFactory(jdbcEventListenerFactory, listenerList) + : jdbcEventListenerFactory; + } + + @Bean + P6SpyDataSourceDecorator p6SpyDataSourceDecorator(JdbcEventListenerFactory jdbcEventListenerFactory) { + return new P6SpyDataSourceDecorator(jdbcEventListenerFactory); + } + + @Bean + TraceJdbcEventListener tracingJdbcEventListener(Tracer tracer, DataSourceNameResolver dataSourceNameResolver, + TraceJdbcProperties traceJdbcProperties, + ObjectProvider> customizers) { + return new TraceJdbcEventListener(tracer, dataSourceNameResolver, traceJdbcProperties.getIncludes(), + traceJdbcProperties.getP6spy().getTracing().isIncludeParameterValues(), + customizers.getIfAvailable(ArrayList::new)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java index f12826584..b5633a019 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java @@ -1,187 +1,188 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.io.Closeable; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import com.p6spy.engine.logging.P6LogFactory; -import com.p6spy.engine.spy.P6ModuleManager; -import com.p6spy.engine.spy.P6SpyFactory; -import com.p6spy.engine.spy.option.EnvironmentVariables; -import com.p6spy.engine.spy.option.P6OptionsSource; -import com.p6spy.engine.spy.option.SpyDotProperties; -import com.p6spy.engine.spy.option.SystemProperties; -import org.slf4j.Logger; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.env.ConfigurableEnvironment; - -import static org.slf4j.LoggerFactory.getLogger; - -/** - * Sets p6spy properties to / from system properties. - * - * @author Arthur Gavlyukovskiy - * @since 3.1.0 - */ -class P6SpyPropertiesSetter implements BeanDefinitionRegistryPostProcessor, Closeable { - - private static final Logger log = getLogger(P6SpyPropertiesSetter.class); - - private final ConfigurableApplicationContext context; - - private final Map initialP6SpyOptions; - - P6SpyPropertiesSetter(ConfigurableApplicationContext context) { - this.context = context; - this.initialP6SpyOptions = findDefinedOptions(); - } - - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { - ConfigurableEnvironment environment = this.context.getEnvironment(); - String customModuleList = initialP6SpyOptions.get("modulelist"); - boolean isEnableLogging = environment.getProperty("spring.sleuth.jdbc.p6spy.enable-logging", Boolean.class, - true); - if (customModuleList != null) { - log.info("P6Spy modulelist is overridden, some p6spy configuration features will not be applied"); - } - else { - List moduleList = new ArrayList<>(); - // default factory, holds P6Spy configuration - moduleList.add(P6SpyFactory.class.getName()); - if (isEnableLogging) { - moduleList.add(P6LogFactory.class.getName()); - } - System.setProperty("p6spy.config.modulelist", String.join(",", moduleList)); - } - if (!initialP6SpyOptions.containsKey("logMessageFormat")) { - String logFormat = logFormat(environment); - boolean isMultiline = multiLine(environment); - if (logFormat != null) { - System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); - System.setProperty("p6spy.config.customLogMessageFormat", logFormat); - } - else if (isMultiline) { - System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.MultiLineFormat"); - } - } - if (isEnableLogging && !initialP6SpyOptions.containsKey("appender")) { - TraceJdbcProperties.P6SpyProperties.P6SpyLogging logging = TraceJdbcProperties.P6SpyProperties.P6SpyLogging - .valueOf(environment - .getProperty("spring.sleuth.jdbc.p6spy.logging", String.class, - TraceJdbcProperties.P6SpyProperties.P6SpyLogging.SLF4J.toString()) - .toUpperCase()); - switch (logging) { - case SYSOUT: - System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.StdoutLogger"); - break; - case SLF4J: - System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.Slf4JLogger"); - break; - case FILE: - System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.FileLogger"); - break; - case CUSTOM: - String customAppender = environment.getProperty("spring.sleuth.jdbc.p6spy.custom-appender-class", - String.class, ""); - System.setProperty("p6spy.config.appender", customAppender); - break; - } - } - if (!initialP6SpyOptions.containsKey("logfile")) { - String logFile = environment.getProperty("spring.sleuth.jdbc.p6spy.log-file", String.class, "spy.log"); - System.setProperty("p6spy.config.logfile", logFile); - } - String pattern = environment.getProperty("spring.sleuth.jdbc.p6spy.log-filter.pattern", String.class); - if (pattern != null) { - System.setProperty("p6spy.config.filter", "true"); - System.setProperty("p6spy.config.sqlexpression", pattern); - } - // If factories were loaded before this method is initialized changing properties - // will not be applied - // Changes done in this method could not override anything user specified, - // therefore it is safe to call reload - P6ModuleManager.getInstance().reload(); - } - - private Boolean multiLine(ConfigurableEnvironment environment) { - return environment.getProperty("spring.sleuth.jdbc.p6spy.multiline", Boolean.class, true); - } - - private String logFormat(ConfigurableEnvironment environment) { - return environment.getProperty("spring.sleuth.jdbc.p6spy.log-format", String.class); - } - - @Override - public void close() throws IOException { - if (!initialP6SpyOptions.containsKey("modulelist")) { - System.clearProperty("p6spy.config.modulelist"); - } - if (!initialP6SpyOptions.containsKey("logMessageFormat")) { - ConfigurableEnvironment environment = this.context.getEnvironment(); - String logFormat = logFormat(environment); - boolean isMultiline = multiLine(environment); - if (logFormat != null) { - System.clearProperty("p6spy.config.logMessageFormat"); - System.clearProperty("p6spy.config.customLogMessageFormat"); - } - else if (isMultiline) { - System.clearProperty("p6spy.config.logMessageFormat"); - } - } - if (!initialP6SpyOptions.containsKey("appender")) { - System.clearProperty("p6spy.config.appender"); - } - if (!initialP6SpyOptions.containsKey("logfile")) { - System.clearProperty("p6spy.config.logfile"); - } - P6ModuleManager.getInstance().reload(); - } - - private Map findDefinedOptions() { - SpyDotProperties spyDotProperties = null; - try { - spyDotProperties = new SpyDotProperties(); - } - catch (IOException ignored) { - } - return Stream.of(spyDotProperties, new EnvironmentVariables(), new SystemProperties()).filter(Objects::nonNull) - .map(P6OptionsSource::getOptions).filter(Objects::nonNull) - .flatMap(options -> options.entrySet().stream()) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, - // always using value from the first P6OptionsSource - (value1, value2) -> value1)); - } - - @Override - public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.p6spy.engine.logging.P6LogFactory; +import com.p6spy.engine.spy.P6ModuleManager; +import com.p6spy.engine.spy.P6SpyFactory; +import com.p6spy.engine.spy.option.EnvironmentVariables; +import com.p6spy.engine.spy.option.P6OptionsSource; +import com.p6spy.engine.spy.option.SpyDotProperties; +import com.p6spy.engine.spy.option.SystemProperties; +import org.slf4j.Logger; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; + +import static org.slf4j.LoggerFactory.getLogger; + +/** + * Sets p6spy properties to / from system properties. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +class P6SpyPropertiesSetter implements BeanDefinitionRegistryPostProcessor, Closeable { + + private static final Logger log = getLogger(P6SpyPropertiesSetter.class); + + private final ConfigurableApplicationContext context; + + private final Map initialP6SpyOptions; + + P6SpyPropertiesSetter(ConfigurableApplicationContext context) { + this.context = context; + this.initialP6SpyOptions = findDefinedOptions(); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + ConfigurableEnvironment environment = this.context.getEnvironment(); + String customModuleList = initialP6SpyOptions.get("modulelist"); + boolean isEnableLogging = environment.getProperty("spring.sleuth.jdbc.p6spy.enable-logging", Boolean.class, + true); + if (customModuleList != null) { + log.info("P6Spy modulelist is overridden, some p6spy configuration features will not be applied"); + } + else { + List moduleList = new ArrayList<>(); + // default factory, holds P6Spy configuration + moduleList.add(P6SpyFactory.class.getName()); + if (isEnableLogging) { + moduleList.add(P6LogFactory.class.getName()); + } + System.setProperty("p6spy.config.modulelist", String.join(",", moduleList)); + } + if (!initialP6SpyOptions.containsKey("logMessageFormat")) { + String logFormat = logFormat(environment); + boolean isMultiline = multiLine(environment); + if (logFormat != null) { + System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); + System.setProperty("p6spy.config.customLogMessageFormat", logFormat); + } + else if (isMultiline) { + System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.MultiLineFormat"); + } + } + if (isEnableLogging && !initialP6SpyOptions.containsKey("appender")) { + TraceJdbcProperties.P6SpyProperties.P6SpyLogging logging = TraceJdbcProperties.P6SpyProperties.P6SpyLogging + .valueOf( + environment + .getProperty("spring.sleuth.jdbc.p6spy.logging", String.class, + TraceJdbcProperties.P6SpyProperties.P6SpyLogging.SLF4J.toString()) + .toUpperCase()); + switch (logging) { + case SYSOUT: + System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.StdoutLogger"); + break; + case SLF4J: + System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.Slf4JLogger"); + break; + case FILE: + System.setProperty("p6spy.config.appender", "com.p6spy.engine.spy.appender.FileLogger"); + break; + case CUSTOM: + String customAppender = environment.getProperty("spring.sleuth.jdbc.p6spy.custom-appender-class", + String.class, ""); + System.setProperty("p6spy.config.appender", customAppender); + break; + } + } + if (!initialP6SpyOptions.containsKey("logfile")) { + String logFile = environment.getProperty("spring.sleuth.jdbc.p6spy.log-file", String.class, "spy.log"); + System.setProperty("p6spy.config.logfile", logFile); + } + String pattern = environment.getProperty("spring.sleuth.jdbc.p6spy.log-filter.pattern", String.class); + if (pattern != null) { + System.setProperty("p6spy.config.filter", "true"); + System.setProperty("p6spy.config.sqlexpression", pattern); + } + // If factories were loaded before this method is initialized changing properties + // will not be applied + // Changes done in this method could not override anything user specified, + // therefore it is safe to call reload + P6ModuleManager.getInstance().reload(); + } + + private Boolean multiLine(ConfigurableEnvironment environment) { + return environment.getProperty("spring.sleuth.jdbc.p6spy.multiline", Boolean.class, true); + } + + private String logFormat(ConfigurableEnvironment environment) { + return environment.getProperty("spring.sleuth.jdbc.p6spy.log-format", String.class); + } + + @Override + public void close() throws IOException { + if (!initialP6SpyOptions.containsKey("modulelist")) { + System.clearProperty("p6spy.config.modulelist"); + } + if (!initialP6SpyOptions.containsKey("logMessageFormat")) { + ConfigurableEnvironment environment = this.context.getEnvironment(); + String logFormat = logFormat(environment); + boolean isMultiline = multiLine(environment); + if (logFormat != null) { + System.clearProperty("p6spy.config.logMessageFormat"); + System.clearProperty("p6spy.config.customLogMessageFormat"); + } + else if (isMultiline) { + System.clearProperty("p6spy.config.logMessageFormat"); + } + } + if (!initialP6SpyOptions.containsKey("appender")) { + System.clearProperty("p6spy.config.appender"); + } + if (!initialP6SpyOptions.containsKey("logfile")) { + System.clearProperty("p6spy.config.logfile"); + } + P6ModuleManager.getInstance().reload(); + } + + private Map findDefinedOptions() { + SpyDotProperties spyDotProperties = null; + try { + spyDotProperties = new SpyDotProperties(); + } + catch (IOException ignored) { + } + return Stream.of(spyDotProperties, new EnvironmentVariables(), new SystemProperties()).filter(Objects::nonNull) + .map(P6OptionsSource::getOptions).filter(Objects::nonNull) + .flatMap(options -> options.entrySet().stream()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, + // always using value from the first P6OptionsSource + (value1, value2) -> value1)); + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorBeanPostProcessor.java index 3c1a78142..1788fddeb 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorBeanPostProcessor.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceDataSourceDecoratorBeanPostProcessor.java @@ -1,140 +1,140 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.stream.Collectors; - -import javax.sql.DataSource; - -import com.zaxxer.hikari.HikariDataSource; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.aop.scope.ScopedProxyUtils; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.util.ClassUtils; - -/** - * {@link BeanPostProcessor} that wraps all data source beans in {@link DataSource} - * proxies specified in property 'spring.datasource.type'. - * - * @author Arthur Gavlyukovskiy - * @since 3.1.0 - */ -public class TraceDataSourceDecoratorBeanPostProcessor implements BeanPostProcessor, Ordered, ApplicationContextAware { - - private static final Log log = LogFactory.getLog(TraceDataSourceDecoratorBeanPostProcessor.class); - - private final static boolean HIKARI_AVAILABLE = ClassUtils.isPresent("com.zaxxer.hikari.HikariDataSource", - DataSourceNameResolver.class.getClassLoader()); - - private ApplicationContext applicationContext; - - private DataSourceNameResolver dataSourceNameResolver; - - private final Collection excludedBeans; - - public TraceDataSourceDecoratorBeanPostProcessor(Collection excludedBeans) { - this.excludedBeans = excludedBeans; - } - - @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - return bean; - } - - @Override - public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { - if (bean instanceof DataSource && !ScopedProxyUtils.isScopedTarget(beanName) - && !this.excludedBeans.contains(beanName)) { - // TODO: This might be a problem - it can lead to eager bean init - Map decorators = this.applicationContext - .getBeansOfType(DataSourceDecorator.class).entrySet().stream() - .sorted(Entry.comparingByValue(AnnotationAwareOrderComparator.INSTANCE)) - .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (v1, v2) -> v2, LinkedHashMap::new)); - return decorate((DataSource) bean, getDataSourceName(bean, beanName), decorators); - } - else { - return bean; - } - } - - private String getDataSourceName(Object bean, String beanName) { - if (HIKARI_AVAILABLE && bean instanceof HikariDataSource) { - HikariDataSource hikariDataSource = (HikariDataSource) bean; - if (hikariDataSource.getPoolName() != null && !hikariDataSource.getPoolName().startsWith("HikariPool-")) { - return hikariDataSource.getPoolName(); - } - } - return beanName; - } - - private DataSource decorate(DataSource dataSource, String name, Map decorators) { - getDataSourceNameResolver().addDataSource(name, dataSource); - DataSource decoratedDataSource = dataSource; - for (Entry decoratorEntry : decorators.entrySet()) { - String decoratorBeanName = decoratorEntry.getKey(); - DataSourceDecorator decorator = decoratorEntry.getValue(); - DataSource dataSourceBeforeDecorating = decoratedDataSource; - decoratedDataSource = Objects.requireNonNull(decorator.decorate(name, decoratedDataSource), - "DataSourceDecorator (" + decoratorBeanName + ", " + decorator + ") should not return null"); - if (dataSourceBeforeDecorating != decoratedDataSource) { - getDataSourceNameResolver().addDataSource(name, decoratedDataSource); - } - } - if (dataSource != decoratedDataSource) { - if (log.isDebugEnabled()) { - log.debug("The decorated data source [" + decoratedDataSource + "] will replace the original one [" - + dataSource + "]"); - } - decoratedDataSource = new DataSourceWrapper(dataSource, decoratedDataSource); - getDataSourceNameResolver().addDataSource(name, decoratedDataSource); - } - return decoratedDataSource; - } - - private DataSourceNameResolver getDataSourceNameResolver() { - if (this.dataSourceNameResolver == null) { - this.dataSourceNameResolver = this.applicationContext.getBean(DataSourceNameResolver.class); - } - return this.dataSourceNameResolver; - } - - @Override - public int getOrder() { - return Ordered.LOWEST_PRECEDENCE - 10; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariDataSource; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.util.ClassUtils; + +/** + * {@link BeanPostProcessor} that wraps all data source beans in {@link DataSource} + * proxies specified in property 'spring.datasource.type'. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +public class TraceDataSourceDecoratorBeanPostProcessor implements BeanPostProcessor, Ordered, ApplicationContextAware { + + private static final Log log = LogFactory.getLog(TraceDataSourceDecoratorBeanPostProcessor.class); + + private final static boolean HIKARI_AVAILABLE = ClassUtils.isPresent("com.zaxxer.hikari.HikariDataSource", + DataSourceNameResolver.class.getClassLoader()); + + private ApplicationContext applicationContext; + + private DataSourceNameResolver dataSourceNameResolver; + + private final Collection excludedBeans; + + public TraceDataSourceDecoratorBeanPostProcessor(Collection excludedBeans) { + this.excludedBeans = excludedBeans; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof DataSource && !ScopedProxyUtils.isScopedTarget(beanName) + && !this.excludedBeans.contains(beanName)) { + // TODO: This might be a problem - it can lead to eager bean init + Map decorators = this.applicationContext + .getBeansOfType(DataSourceDecorator.class).entrySet().stream() + .sorted(Entry.comparingByValue(AnnotationAwareOrderComparator.INSTANCE)) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (v1, v2) -> v2, LinkedHashMap::new)); + return decorate((DataSource) bean, getDataSourceName(bean, beanName), decorators); + } + else { + return bean; + } + } + + private String getDataSourceName(Object bean, String beanName) { + if (HIKARI_AVAILABLE && bean instanceof HikariDataSource) { + HikariDataSource hikariDataSource = (HikariDataSource) bean; + if (hikariDataSource.getPoolName() != null && !hikariDataSource.getPoolName().startsWith("HikariPool-")) { + return hikariDataSource.getPoolName(); + } + } + return beanName; + } + + private DataSource decorate(DataSource dataSource, String name, Map decorators) { + getDataSourceNameResolver().addDataSource(name, dataSource); + DataSource decoratedDataSource = dataSource; + for (Entry decoratorEntry : decorators.entrySet()) { + String decoratorBeanName = decoratorEntry.getKey(); + DataSourceDecorator decorator = decoratorEntry.getValue(); + DataSource dataSourceBeforeDecorating = decoratedDataSource; + decoratedDataSource = Objects.requireNonNull(decorator.decorate(name, decoratedDataSource), + "DataSourceDecorator (" + decoratorBeanName + ", " + decorator + ") should not return null"); + if (dataSourceBeforeDecorating != decoratedDataSource) { + getDataSourceNameResolver().addDataSource(name, decoratedDataSource); + } + } + if (dataSource != decoratedDataSource) { + if (log.isDebugEnabled()) { + log.debug("The decorated data source [" + decoratedDataSource + "] will replace the original one [" + + dataSource + "]"); + } + decoratedDataSource = new DataSourceWrapper(dataSource, decoratedDataSource); + getDataSourceNameResolver().addDataSource(name, decoratedDataSource); + } + return decoratedDataSource; + } + + private DataSourceNameResolver getDataSourceNameResolver() { + if (this.dataSourceNameResolver == null) { + this.dataSourceNameResolver = this.applicationContext.getBean(DataSourceNameResolver.class); + } + return this.dataSourceNameResolver; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 10; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcAutoConfiguration.java index c8c385ba9..80233866e 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcAutoConfiguration.java @@ -1,70 +1,71 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import javax.sql.DataSource; - -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceHikariListenerStrategySpanCustomizer; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for proxying DataSource. - * - * @author Arthur Gavlyukovskiy - */ -@Configuration(proxyBeanMethods = false) -@EnableConfigurationProperties(TraceJdbcProperties.class) -@ConditionalOnProperty(name = "spring.sleuth.jdbc.enabled", havingValue = "true", matchIfMissing = true) -@ConditionalOnBean({ DataSource.class, Tracer.class }) -@AutoConfigureAfter(DataSourceAutoConfiguration.class) -@Import({ P6SpyConfiguration.class, DataSourceProxyConfiguration.class }) -public class TraceJdbcAutoConfiguration { - - @Bean - @ConditionalOnBean(DataSourceDecorator.class) - static TraceDataSourceDecoratorBeanPostProcessor traceDataSourceDecoratorBeanPostProcessor( - TraceJdbcProperties dataSourceDecoratorProperties) { - return new TraceDataSourceDecoratorBeanPostProcessor(dataSourceDecoratorProperties.getExcludedDataSourceBeanNames()); - } - - @Bean - @ConditionalOnMissingBean - DataSourceNameResolver traceDataSourceNameResolver() { - return new DataSourceNameResolver(); - } - - @Bean - @ConditionalOnClass(name = "com.zaxxer.hikari.HikariDataSource") - TraceListenerStrategySpanCustomizer hikariTraceListenerStrategySpanCustomizer() { - return new TraceHikariListenerStrategySpanCustomizer(); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import javax.sql.DataSource; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceHikariListenerStrategySpanCustomizer; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceListenerStrategySpanCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for proxying DataSource. + * + * @author Arthur Gavlyukovskiy + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(TraceJdbcProperties.class) +@ConditionalOnProperty(name = "spring.sleuth.jdbc.enabled", havingValue = "true", matchIfMissing = true) +@ConditionalOnBean({ DataSource.class, Tracer.class }) +@AutoConfigureAfter(DataSourceAutoConfiguration.class) +@Import({ P6SpyConfiguration.class, DataSourceProxyConfiguration.class }) +public class TraceJdbcAutoConfiguration { + + @Bean + @ConditionalOnBean(DataSourceDecorator.class) + static TraceDataSourceDecoratorBeanPostProcessor traceDataSourceDecoratorBeanPostProcessor( + TraceJdbcProperties dataSourceDecoratorProperties) { + return new TraceDataSourceDecoratorBeanPostProcessor( + dataSourceDecoratorProperties.getExcludedDataSourceBeanNames()); + } + + @Bean + @ConditionalOnMissingBean + DataSourceNameResolver traceDataSourceNameResolver() { + return new DataSourceNameResolver(); + } + + @Bean + @ConditionalOnClass(name = "com.zaxxer.hikari.HikariDataSource") + TraceListenerStrategySpanCustomizer hikariTraceListenerStrategySpanCustomizer() { + return new TraceHikariListenerStrategySpanCustomizer(); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java index bd16574da..a0ffa1f4e 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java @@ -1,511 +1,511 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.regex.Pattern; - -import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceType; - -/** - * Properties for JDBC instrumentation. - * - * @author Arthur Gavlyukovskiy - * @since 3.1.0 - */ -@ConfigurationProperties(prefix = "spring.sleuth.jdbc") -public class TraceJdbcProperties { - - /** - * Enables JDBC instrumentation. - */ - private boolean enabled = true; - - /** - * List of DataSource bean names that will not be decorated. - */ - private Collection excludedDataSourceBeanNames = Collections.emptyList(); - - /** - * Which types of tracing we would like to include. - */ - private List includes = Arrays.asList(TraceType.CONNECTION, TraceType.QUERY, TraceType.FETCH); - - private DataSourceProxyProperties datasourceProxy = new DataSourceProxyProperties(); - - private P6SpyProperties p6spy = new P6SpyProperties(); - - public boolean isEnabled() { - return this.enabled; - } - - public Collection getExcludedDataSourceBeanNames() { - return this.excludedDataSourceBeanNames; - } - - public List getIncludes() { - return this.includes; - } - - public void setIncludes(List includes) { - this.includes = includes; - } - - public DataSourceProxyProperties getDatasourceProxy() { - return datasourceProxy; - } - - public P6SpyProperties getP6spy() { - return this.p6spy; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public void setExcludedDataSourceBeanNames(Collection excludedDataSourceBeanNames) { - this.excludedDataSourceBeanNames = excludedDataSourceBeanNames; - } - - public void setDatasourceProxy(DataSourceProxyProperties datasourceProxy) { - this.datasourceProxy = datasourceProxy; - } - - public void setP6spy(P6SpyProperties p6spy) { - this.p6spy = p6spy; - } - - /** - * Properties for datasource-proxy. - */ - public static class DataSourceProxyProperties { - - /** - * Should the datasource-proxy tracing be enabled? - */ - private boolean enabled = true; - - /** - * Logging to use for logging queries. - */ - private DataSourceProxyLogging logging = DataSourceProxyLogging.SLF4J; - - /** - * Query configuration. - */ - private Query query = new Query(); - - /** - * Slow query configuration. - */ - private SlowQuery slowQuery = new SlowQuery(); - - /** - * Use multiline output for logging query. - * - * @see ProxyDataSourceBuilder#multiline() - */ - private boolean multiline = true; - - /** - * Use json output for logging query. - * - * @see ProxyDataSourceBuilder#asJson() - */ - private boolean jsonFormat = false; - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public DataSourceProxyLogging getLogging() { - return this.logging; - } - - public void setLogging(DataSourceProxyLogging logging) { - this.logging = logging; - } - - public Query getQuery() { - return this.query; - } - - public void setQuery(Query query) { - this.query = query; - } - - public SlowQuery getSlowQuery() { - return this.slowQuery; - } - - public void setSlowQuery(SlowQuery slowQuery) { - this.slowQuery = slowQuery; - } - - public boolean isMultiline() { - return this.multiline; - } - - public void setMultiline(boolean multiline) { - this.multiline = multiline; - } - - public boolean isJsonFormat() { - return this.jsonFormat; - } - - public void setJsonFormat(boolean jsonFormat) { - this.jsonFormat = jsonFormat; - } - - /** - * Properties to configure query logging listener. - */ - public static class Query { - - /** - * Enable logging all queries to the log. - */ - private boolean enableLogging = true; - - /** - * Name of query logger. - */ - private String loggerName; - - /** - * Severity of query logger. - */ - private String logLevel = "DEBUG"; - - public boolean isEnableLogging() { - return this.enableLogging; - } - - public void setEnableLogging(boolean enableLogging) { - this.enableLogging = enableLogging; - } - - public String getLoggerName() { - return this.loggerName; - } - - public void setLoggerName(String loggerName) { - this.loggerName = loggerName; - } - - public String getLogLevel() { - return this.logLevel; - } - - public void setLogLevel(String logLevel) { - this.logLevel = logLevel; - } - - } - - /** - * Properties to configure slow query logging listener. - */ - public static class SlowQuery { - - /** - * Enable logging slow queries to the log. - */ - private boolean enableLogging = true; - - /** - * Name of slow query logger. - */ - private String loggerName; - - /** - * Severity of slow query logger. - */ - private String logLevel = "WARN"; - - /** - * Number of seconds to consider query as slow. - */ - private long threshold = 300; - - boolean isEnableLogging() { - return enableLogging; - } - - public void setEnableLogging(boolean enableLogging) { - this.enableLogging = enableLogging; - } - - public String getLoggerName() { - return loggerName; - } - - public void setLoggerName(String loggerName) { - this.loggerName = loggerName; - } - - public String getLogLevel() { - return logLevel; - } - - public void setLogLevel(String logLevel) { - this.logLevel = logLevel; - } - - public long getThreshold() { - return threshold; - } - - public void setThreshold(long threshold) { - this.threshold = threshold; - } - - } - - /** - * Query logging listener is the most used listener that logs executing query with - * actual parameters to. You can pick one of the following proxy logging - * mechanisms. - */ - public enum DataSourceProxyLogging { - - /** - * Log using System.out. - */ - SYSOUT, - - /** - * Log using SLF4J. - */ - SLF4J, - - /** - * Log using Commons. - */ - COMMONS, - - /** - * Log using Java Util Logging. - */ - JUL - - } - - } - - /** - * Properties for configuring p6spy. - */ - public static class P6SpyProperties { - - /** - * Should the p6spy tracing be enabled? - */ - private boolean enabled = true; - - /** - * Enables logging JDBC events. - */ - private boolean enableLogging = true; - - /** - * Enables multiline output. - */ - private boolean multiline = true; - - /** - * Logging to use for logging queries. - */ - private P6SpyLogging logging = P6SpyLogging.SLF4J; - - /** - * Name of log file to use (only with logging=file). - */ - private String logFile = "spy.log"; - - /** - * Custom log format. - */ - private String logFormat; - - /** - * Tracing related properties. - */ - private P6SpyTracing tracing = new P6SpyTracing(); - - /** - * Class file to use (only with logging=custom). The class must implement - * {@link com.p6spy.engine.spy.appender.FormattedLogger}. - */ - private String customAppenderClass; - - /** - * Log filtering related properties. - */ - private P6SpyLogFilter logFilter = new P6SpyLogFilter(); - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public boolean isEnableLogging() { - return this.enableLogging; - } - - public void setEnableLogging(boolean enableLogging) { - this.enableLogging = enableLogging; - } - - public boolean isMultiline() { - return this.multiline; - } - - public void setMultiline(boolean multiline) { - this.multiline = multiline; - } - - public P6SpyLogging getLogging() { - return this.logging; - } - - public void setLogging(P6SpyLogging logging) { - this.logging = logging; - } - - public String getLogFile() { - return this.logFile; - } - - public void setLogFile(String logFile) { - this.logFile = logFile; - } - - public String getLogFormat() { - return this.logFormat; - } - - public void setLogFormat(String logFormat) { - this.logFormat = logFormat; - } - - public P6SpyTracing getTracing() { - return this.tracing; - } - - public void setTracing(P6SpyTracing tracing) { - this.tracing = tracing; - } - - public String getCustomAppenderClass() { - return this.customAppenderClass; - } - - public void setCustomAppenderClass(String customAppenderClass) { - this.customAppenderClass = customAppenderClass; - } - - public P6SpyLogFilter getLogFilter() { - return this.logFilter; - } - - public void setLogFilter(P6SpyLogFilter logFilter) { - this.logFilter = logFilter; - } - - /** - * P6Spy logging options. - */ - public enum P6SpyLogging { - - /** - * Log using System.out. - */ - SYSOUT, - - /** - * Log using SLF4J. - */ - SLF4J, - - /** - * Log to file. - */ - FILE, - - /** - * Custom logging. - */ - CUSTOM - - } - - public static class P6SpyTracing { - - /** - * Report the effective sql string (with '?' replaced with real values) to - * tracing systems. - *

- * NOTE this setting does not affect the logging message. - */ - private boolean includeParameterValues = true; - - public boolean isIncludeParameterValues() { - return this.includeParameterValues; - } - - public void setIncludeParameterValues(boolean includeParameterValues) { - this.includeParameterValues = includeParameterValues; - } - - } - - public static class P6SpyLogFilter { - - /** - * Use regex pattern to filter log messages. Only matched messages will be - * logged. - */ - private Pattern pattern; - - public Pattern getPattern() { - return this.pattern; - } - - public void setPattern(Pattern pattern) { - this.pattern = pattern; - } - - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceType; + +/** + * Properties for JDBC instrumentation. + * + * @author Arthur Gavlyukovskiy + * @since 3.1.0 + */ +@ConfigurationProperties(prefix = "spring.sleuth.jdbc") +public class TraceJdbcProperties { + + /** + * Enables JDBC instrumentation. + */ + private boolean enabled = true; + + /** + * List of DataSource bean names that will not be decorated. + */ + private Collection excludedDataSourceBeanNames = Collections.emptyList(); + + /** + * Which types of tracing we would like to include. + */ + private List includes = Arrays.asList(TraceType.CONNECTION, TraceType.QUERY, TraceType.FETCH); + + private DataSourceProxyProperties datasourceProxy = new DataSourceProxyProperties(); + + private P6SpyProperties p6spy = new P6SpyProperties(); + + public boolean isEnabled() { + return this.enabled; + } + + public Collection getExcludedDataSourceBeanNames() { + return this.excludedDataSourceBeanNames; + } + + public List getIncludes() { + return this.includes; + } + + public void setIncludes(List includes) { + this.includes = includes; + } + + public DataSourceProxyProperties getDatasourceProxy() { + return datasourceProxy; + } + + public P6SpyProperties getP6spy() { + return this.p6spy; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setExcludedDataSourceBeanNames(Collection excludedDataSourceBeanNames) { + this.excludedDataSourceBeanNames = excludedDataSourceBeanNames; + } + + public void setDatasourceProxy(DataSourceProxyProperties datasourceProxy) { + this.datasourceProxy = datasourceProxy; + } + + public void setP6spy(P6SpyProperties p6spy) { + this.p6spy = p6spy; + } + + /** + * Properties for datasource-proxy. + */ + public static class DataSourceProxyProperties { + + /** + * Should the datasource-proxy tracing be enabled? + */ + private boolean enabled = true; + + /** + * Logging to use for logging queries. + */ + private DataSourceProxyLogging logging = DataSourceProxyLogging.SLF4J; + + /** + * Query configuration. + */ + private Query query = new Query(); + + /** + * Slow query configuration. + */ + private SlowQuery slowQuery = new SlowQuery(); + + /** + * Use multiline output for logging query. + * + * @see ProxyDataSourceBuilder#multiline() + */ + private boolean multiline = true; + + /** + * Use json output for logging query. + * + * @see ProxyDataSourceBuilder#asJson() + */ + private boolean jsonFormat = false; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public DataSourceProxyLogging getLogging() { + return this.logging; + } + + public void setLogging(DataSourceProxyLogging logging) { + this.logging = logging; + } + + public Query getQuery() { + return this.query; + } + + public void setQuery(Query query) { + this.query = query; + } + + public SlowQuery getSlowQuery() { + return this.slowQuery; + } + + public void setSlowQuery(SlowQuery slowQuery) { + this.slowQuery = slowQuery; + } + + public boolean isMultiline() { + return this.multiline; + } + + public void setMultiline(boolean multiline) { + this.multiline = multiline; + } + + public boolean isJsonFormat() { + return this.jsonFormat; + } + + public void setJsonFormat(boolean jsonFormat) { + this.jsonFormat = jsonFormat; + } + + /** + * Properties to configure query logging listener. + */ + public static class Query { + + /** + * Enable logging all queries to the log. + */ + private boolean enableLogging = true; + + /** + * Name of query logger. + */ + private String loggerName; + + /** + * Severity of query logger. + */ + private String logLevel = "DEBUG"; + + public boolean isEnableLogging() { + return this.enableLogging; + } + + public void setEnableLogging(boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public String getLoggerName() { + return this.loggerName; + } + + public void setLoggerName(String loggerName) { + this.loggerName = loggerName; + } + + public String getLogLevel() { + return this.logLevel; + } + + public void setLogLevel(String logLevel) { + this.logLevel = logLevel; + } + + } + + /** + * Properties to configure slow query logging listener. + */ + public static class SlowQuery { + + /** + * Enable logging slow queries to the log. + */ + private boolean enableLogging = true; + + /** + * Name of slow query logger. + */ + private String loggerName; + + /** + * Severity of slow query logger. + */ + private String logLevel = "WARN"; + + /** + * Number of seconds to consider query as slow. + */ + private long threshold = 300; + + boolean isEnableLogging() { + return enableLogging; + } + + public void setEnableLogging(boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public String getLoggerName() { + return loggerName; + } + + public void setLoggerName(String loggerName) { + this.loggerName = loggerName; + } + + public String getLogLevel() { + return logLevel; + } + + public void setLogLevel(String logLevel) { + this.logLevel = logLevel; + } + + public long getThreshold() { + return threshold; + } + + public void setThreshold(long threshold) { + this.threshold = threshold; + } + + } + + /** + * Query logging listener is the most used listener that logs executing query with + * actual parameters to. You can pick one of the following proxy logging + * mechanisms. + */ + public enum DataSourceProxyLogging { + + /** + * Log using System.out. + */ + SYSOUT, + + /** + * Log using SLF4J. + */ + SLF4J, + + /** + * Log using Commons. + */ + COMMONS, + + /** + * Log using Java Util Logging. + */ + JUL + + } + + } + + /** + * Properties for configuring p6spy. + */ + public static class P6SpyProperties { + + /** + * Should the p6spy tracing be enabled? + */ + private boolean enabled = true; + + /** + * Enables logging JDBC events. + */ + private boolean enableLogging = true; + + /** + * Enables multiline output. + */ + private boolean multiline = true; + + /** + * Logging to use for logging queries. + */ + private P6SpyLogging logging = P6SpyLogging.SLF4J; + + /** + * Name of log file to use (only with logging=file). + */ + private String logFile = "spy.log"; + + /** + * Custom log format. + */ + private String logFormat; + + /** + * Tracing related properties. + */ + private P6SpyTracing tracing = new P6SpyTracing(); + + /** + * Class file to use (only with logging=custom). The class must implement + * {@link com.p6spy.engine.spy.appender.FormattedLogger}. + */ + private String customAppenderClass; + + /** + * Log filtering related properties. + */ + private P6SpyLogFilter logFilter = new P6SpyLogFilter(); + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnableLogging() { + return this.enableLogging; + } + + public void setEnableLogging(boolean enableLogging) { + this.enableLogging = enableLogging; + } + + public boolean isMultiline() { + return this.multiline; + } + + public void setMultiline(boolean multiline) { + this.multiline = multiline; + } + + public P6SpyLogging getLogging() { + return this.logging; + } + + public void setLogging(P6SpyLogging logging) { + this.logging = logging; + } + + public String getLogFile() { + return this.logFile; + } + + public void setLogFile(String logFile) { + this.logFile = logFile; + } + + public String getLogFormat() { + return this.logFormat; + } + + public void setLogFormat(String logFormat) { + this.logFormat = logFormat; + } + + public P6SpyTracing getTracing() { + return this.tracing; + } + + public void setTracing(P6SpyTracing tracing) { + this.tracing = tracing; + } + + public String getCustomAppenderClass() { + return this.customAppenderClass; + } + + public void setCustomAppenderClass(String customAppenderClass) { + this.customAppenderClass = customAppenderClass; + } + + public P6SpyLogFilter getLogFilter() { + return this.logFilter; + } + + public void setLogFilter(P6SpyLogFilter logFilter) { + this.logFilter = logFilter; + } + + /** + * P6Spy logging options. + */ + public enum P6SpyLogging { + + /** + * Log using System.out. + */ + SYSOUT, + + /** + * Log using SLF4J. + */ + SLF4J, + + /** + * Log to file. + */ + FILE, + + /** + * Custom logging. + */ + CUSTOM + + } + + public static class P6SpyTracing { + + /** + * Report the effective sql string (with '?' replaced with real values) to + * tracing systems. + *

+ * NOTE this setting does not affect the logging message. + */ + private boolean includeParameterValues = true; + + public boolean isIncludeParameterValues() { + return this.includeParameterValues; + } + + public void setIncludeParameterValues(boolean includeParameterValues) { + this.includeParameterValues = includeParameterValues; + } + + } + + public static class P6SpyLogFilter { + + /** + * Use regex pattern to filter log messages. Only matched messages will be + * logged. + */ + private Pattern pattern; + + public Pattern getPattern() { + return this.pattern; + } + + public void setPattern(Pattern pattern) { + this.pattern = pattern; + } + + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/TraceNoOpAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/TraceNoOpAutoConfiguration.java index 491a6ea42..c6ed16337 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/TraceNoOpAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/TraceNoOpAutoConfiguration.java @@ -1,104 +1,103 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.cloud.sleuth.CurrentTraceContext; -import org.springframework.cloud.sleuth.SpanCustomizer; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.autoconfig.instrument.web.ConditionalOnSleuthWeb; -import org.springframework.cloud.sleuth.autoconfig.instrument.web.client.ConditionalnOnSleuthWebClient; -import org.springframework.cloud.sleuth.http.HttpClientHandler; -import org.springframework.cloud.sleuth.http.HttpServerHandler; -import org.springframework.cloud.sleuth.propagation.Propagator; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -/** - * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration - * Auto-configuration} to enable tracing via Spring Cloud Sleuth. - * - * @author Spencer Gibb - * @author Marcin Grzejszczak - * @author Tim Ysewyn - * @since 2.0.0 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnSleuth -@ConditionalOnProperty("spring.sleuth.noop.enabled") -@AutoConfigureBefore(BraveAutoConfiguration.class) -@Import(TraceConfiguration.class) -public class TraceNoOpAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - Tracer defaultTracer() { - return new NoOpTracer(); - } - - @Bean - Propagator defaultPropagator() { - return new NoOpPropagator(); - } - - @Bean - CurrentTraceContext defaultCurrentTraceContext() { - return new NoOpCurrentTraceContext(); - } - - @Bean - SpanCustomizer defaultSpanCustomizer() { - return new NoOpSpanCustomizer(); - } - - @Configuration(proxyBeanMethods = false) - static class TraceHttpConfiguration { - - @Bean - @ConditionalnOnSleuthWebClient - HttpClientHandler defaultHttpClientHandler() { - return new NoOpHttpClientHandler(); - } - - @Bean - @ConditionalOnSleuthWeb - HttpServerHandler defaultHttpServerHandler() { - return new NoOpHttpServerHandler(); - } - - } -} - -@Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.TYPE, ElementType.METHOD }) -@Documented -@ConditionalOnProperty(value = "spring.sleuth.enabled", matchIfMissing = true) -@interface ConditionalOnSleuth { - -} - - +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.SpanCustomizer; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.autoconfig.instrument.web.ConditionalOnSleuthWeb; +import org.springframework.cloud.sleuth.autoconfig.instrument.web.client.ConditionalnOnSleuthWebClient; +import org.springframework.cloud.sleuth.http.HttpClientHandler; +import org.springframework.cloud.sleuth.http.HttpServerHandler; +import org.springframework.cloud.sleuth.propagation.Propagator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} to enable tracing via Spring Cloud Sleuth. + * + * @author Spencer Gibb + * @author Marcin Grzejszczak + * @author Tim Ysewyn + * @since 2.0.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnSleuth +@ConditionalOnProperty("spring.sleuth.noop.enabled") +@AutoConfigureBefore(BraveAutoConfiguration.class) +@Import(TraceConfiguration.class) +public class TraceNoOpAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + Tracer defaultTracer() { + return new NoOpTracer(); + } + + @Bean + Propagator defaultPropagator() { + return new NoOpPropagator(); + } + + @Bean + CurrentTraceContext defaultCurrentTraceContext() { + return new NoOpCurrentTraceContext(); + } + + @Bean + SpanCustomizer defaultSpanCustomizer() { + return new NoOpSpanCustomizer(); + } + + @Configuration(proxyBeanMethods = false) + static class TraceHttpConfiguration { + + @Bean + @ConditionalnOnSleuthWebClient + HttpClientHandler defaultHttpClientHandler() { + return new NoOpHttpClientHandler(); + } + + @Bean + @ConditionalOnSleuthWeb + HttpServerHandler defaultHttpServerHandler() { + return new NoOpHttpServerHandler(); + } + + } + +} + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ConditionalOnProperty(value = "spring.sleuth.enabled", matchIfMissing = true) +@interface ConditionalOnSleuth { + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java index f2a08a139..f9d79f63c 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceDecoratorAutoConfigurationTests.java @@ -1,371 +1,370 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.io.PrintWriter; -import java.sql.Connection; -import java.util.concurrent.ThreadLocalRandom; -import java.util.logging.Logger; - -import javax.sql.DataSource; - -import com.p6spy.engine.spy.P6DataSource; -import com.zaxxer.hikari.HikariDataSource; -import net.ttddyy.dsproxy.support.ProxyDataSource; -import org.apache.commons.dbcp2.BasicDataSource; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.DirectFieldAccessor; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.Scope; -import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.context.support.GenericApplicationContext; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -class DataSourceDecoratorAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceJdbcAutoConfiguration.class, TraceNoOpAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.sleuth.noop.enabled=true", - "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()); - - @Test - void testDecoratingInDefaultOrder() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); - - DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; - assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(P6DataSource.class); - P6DataSource p6DataSource = (P6DataSource) DataSourceWrapper.getDecoratedDataSource(); - - DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); - assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; - }); - } - - @Test - void testNoDecoratingForExcludeBeans() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.excluded-data-source-bean-names:dataSource"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - - assertThat(dataSource).isInstanceOf(HikariDataSource.class); - }); - } - - @Test - void testDecoratingWhenDefaultProxyProviderNotAvailable() { - ApplicationContextRunner contextRunner = this.contextRunner; - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - - assertThat(((DataSourceWrapper) dataSource).getOriginalDataSource()).isInstanceOf(HikariDataSource.class); - - DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; - assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(P6DataSource.class); - P6DataSource p6DataSource = (P6DataSource) DataSourceWrapper.getDecoratedDataSource(); - - DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); - assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; - - DataSource dsProxyWrappedDataSource = (DataSource) ReflectionTestUtils.getField(proxyDataSource, - "dataSource"); - assertThat(dsProxyWrappedDataSource).isEqualTo(DataSourceWrapper.getOriginalDataSource()); - }); - } - - @Test - void testDecoratedHikariSpecificPropertiesIsSet() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.datasource.type:" + HikariDataSource.class.getName(), - "spring.datasource.hikari.catalog:test_catalog"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isNotNull(); - assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); - DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); - assertThat(realDataSource).isInstanceOf(HikariDataSource.class); - assertThat(((HikariDataSource) realDataSource).getCatalog()).isEqualTo("test_catalog"); - }); - } - - @Test - void testCustomDataSourceIsDecorated() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(TestDataSourceConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); - DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); - assertThat(realDataSource).isInstanceOf(BasicDataSource.class); - }); - } - - @Test - void testScopedDataSourceIsNotDecorated() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(TestScopedDataSourceConfiguration.class); - - contextRunner.run(context -> { - assertThat(context).getBeanNames(DataSource.class).containsOnly("dataSource", "scopedTarget.dataSource"); - assertThat(context).getBean("dataSource").isInstanceOf(DataSourceWrapper.class); - assertThat(context).getBean("scopedTarget.dataSource").isNotInstanceOf(DataSourceWrapper.class); - }); - } - - @Test - void testCustomDataSourceDecoratorApplied() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(TestDataSourceDecoratorConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isNotNull(); - - DataSource customDataSource = ((DataSourceWrapper) dataSource).getDecoratedDataSource(); - assertThat(customDataSource).isInstanceOf(CustomDataSourceProxy.class); - - DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); - assertThat(realDataSource).isInstanceOf(HikariDataSource.class); - - assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); - - DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; - - assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(CustomDataSourceProxy.class); - CustomDataSourceProxy customDataSourceProxy = (CustomDataSourceProxy) DataSourceWrapper - .getDecoratedDataSource(); - - assertThat(customDataSourceProxy.delegate).isInstanceOf(P6DataSource.class); - P6DataSource p6DataSource = (P6DataSource) customDataSourceProxy.delegate; - - DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); - assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; - }); - } - - @Test - void testDecoratingCanBeDisabled() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.enabled:false"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - assertThat(dataSource).isInstanceOf(HikariDataSource.class); - }); - } - - @Test - void testDecoratingCanBeDisabledForSpecificBeans() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.excluded-data-source-bean-names:secondDataSource") - .withUserConfiguration(TestMultiDataSourceConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean("dataSource", DataSource.class); - assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); - - DataSource secondDataSource = context.getBean("secondDataSource", DataSource.class); - assertThat(secondDataSource).isInstanceOf(BasicDataSource.class); - }); - } - - @Test - void testDecoratingChainBuiltCorrectly() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - - DataSourceWrapper dataSource1 = context.getBean(DataSourceWrapper.class); - assertThat(dataSource1).isNotNull(); - - DataSource p6DataSource = dataSource1.getDecoratedDataSource(); - assertThat(p6DataSource).isNotNull(); - assertThat(p6DataSource).isInstanceOf(P6DataSource.class); - - DataSource proxyDataSource = (DataSource) new DirectFieldAccessor(p6DataSource) - .getPropertyValue("realDataSource"); - assertThat(proxyDataSource).isNotNull(); - assertThat(proxyDataSource).isInstanceOf(ProxyDataSource.class); - }); - } - - @Test - void testDecorateDynamicallyRegisteredBeans() { - ApplicationContextRunner contextRunner = this.contextRunner.withInitializer(context -> { - GenericApplicationContext gac = (GenericApplicationContext) context; - gac.registerBean("ds1", DataSource.class, () -> new HikariDataSource()); - gac.registerBean("ds2", DataSource.class, () -> new HikariDataSource()); - }); - - contextRunner.run(context -> { - DataSource dataSource1 = context.getBean("ds1", DataSource.class); - assertThat(dataSource1).isNotNull(); - assertThat(dataSource1).isInstanceOf(DataSourceWrapper.class); - - DataSource dataSource2 = context.getBean("ds2", DataSource.class); - assertThat(dataSource2).isNotNull(); - assertThat(dataSource2).isInstanceOf(DataSourceWrapper.class); - }); - } - - @Configuration - static class TestDataSourceConfiguration { - - @Bean - public DataSource dataSource() { - BasicDataSource pool = new BasicDataSource(); - pool.setDriverClassName("org.hsqldb.jdbcDriver"); - pool.setUrl("jdbc:hsqldb:target/overridedb"); - pool.setUsername("sa"); - return pool; - } - - } - - @Configuration - static class TestDataSourceDecoratorConfiguration { - - @Bean - public DataSourceDecorator customDataSourceDecorator() { - return (beanName, dataSource) -> new CustomDataSourceProxy(dataSource); - } - - } - - @Configuration - static class TestMultiDataSourceConfiguration { - - @Bean - @Primary - public DataSource dataSource() { - BasicDataSource pool = new BasicDataSource(); - pool.setDriverClassName("org.hsqldb.jdbcDriver"); - pool.setUrl("jdbc:hsqldb:target/db"); - pool.setUsername("sa"); - return pool; - } - - @Bean - public DataSource secondDataSource() { - BasicDataSource pool = new BasicDataSource(); - pool.setDriverClassName("org.hsqldb.jdbcDriver"); - pool.setUrl("jdbc:hsqldb:target/db2"); - pool.setUsername("sa"); - return pool; - } - - } - - @Configuration - static class TestScopedDataSourceConfiguration { - - @Bean - @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) - public DataSource dataSource() { - BasicDataSource pool = new BasicDataSource(); - pool.setDriverClassName("org.hsqldb.jdbcDriver"); - pool.setUrl("jdbc:hsqldb:target/overridedb"); - pool.setUsername("sa"); - return pool; - } - - } - - /** - * Custom proxy data source for tests. - * - * @author Arthur Gavlyukovskiy - */ - static class CustomDataSourceProxy implements DataSource { - - private final DataSource delegate; - - CustomDataSourceProxy(DataSource delegate) { - this.delegate = delegate; - } - - @Override - public Connection getConnection() { - return null; - } - - @Override - public Connection getConnection(String username, String password) { - return null; - } - - @Override - public T unwrap(Class iface) { - return null; - } - - @Override - public boolean isWrapperFor(Class iface) { - return false; - } - - @Override - public PrintWriter getLogWriter() { - return null; - } - - @Override - public void setLogWriter(PrintWriter out) { - - } - - @Override - public void setLoginTimeout(int seconds) { - - } - - @Override - public int getLoginTimeout() { - return 0; - } - - @Override - public Logger getParentLogger() { - return null; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.io.PrintWriter; +import java.sql.Connection; +import java.util.concurrent.ThreadLocalRandom; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +import com.p6spy.engine.spy.P6DataSource; +import com.zaxxer.hikari.HikariDataSource; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import org.apache.commons.dbcp2.BasicDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +class DataSourceDecoratorAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(DataSourceAutoConfiguration.class, TraceJdbcAutoConfiguration.class, + TraceNoOpAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", "spring.sleuth.noop.enabled=true", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()); + + @Test + void testDecoratingInDefaultOrder() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + + DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; + assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(P6DataSource.class); + P6DataSource p6DataSource = (P6DataSource) DataSourceWrapper.getDecoratedDataSource(); + + DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); + assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; + }); + } + + @Test + void testNoDecoratingForExcludeBeans() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.excluded-data-source-bean-names:dataSource"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + }); + } + + @Test + void testDecoratingWhenDefaultProxyProviderNotAvailable() { + ApplicationContextRunner contextRunner = this.contextRunner; + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + assertThat(((DataSourceWrapper) dataSource).getOriginalDataSource()).isInstanceOf(HikariDataSource.class); + + DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; + assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(P6DataSource.class); + P6DataSource p6DataSource = (P6DataSource) DataSourceWrapper.getDecoratedDataSource(); + + DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); + assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; + + DataSource dsProxyWrappedDataSource = (DataSource) ReflectionTestUtils.getField(proxyDataSource, + "dataSource"); + assertThat(dsProxyWrappedDataSource).isEqualTo(DataSourceWrapper.getOriginalDataSource()); + }); + } + + @Test + void testDecoratedHikariSpecificPropertiesIsSet() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.datasource.type:" + HikariDataSource.class.getName(), + "spring.datasource.hikari.catalog:test_catalog"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isNotNull(); + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); + assertThat(realDataSource).isInstanceOf(HikariDataSource.class); + assertThat(((HikariDataSource) realDataSource).getCatalog()).isEqualTo("test_catalog"); + }); + } + + @Test + void testCustomDataSourceIsDecorated() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(TestDataSourceConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); + assertThat(realDataSource).isInstanceOf(BasicDataSource.class); + }); + } + + @Test + void testScopedDataSourceIsNotDecorated() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(TestScopedDataSourceConfiguration.class); + + contextRunner.run(context -> { + assertThat(context).getBeanNames(DataSource.class).containsOnly("dataSource", "scopedTarget.dataSource"); + assertThat(context).getBean("dataSource").isInstanceOf(DataSourceWrapper.class); + assertThat(context).getBean("scopedTarget.dataSource").isNotInstanceOf(DataSourceWrapper.class); + }); + } + + @Test + void testCustomDataSourceDecoratorApplied() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(TestDataSourceDecoratorConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isNotNull(); + + DataSource customDataSource = ((DataSourceWrapper) dataSource).getDecoratedDataSource(); + assertThat(customDataSource).isInstanceOf(CustomDataSourceProxy.class); + + DataSource realDataSource = ((DataSourceWrapper) dataSource).getOriginalDataSource(); + assertThat(realDataSource).isInstanceOf(HikariDataSource.class); + + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + + DataSourceWrapper DataSourceWrapper = (DataSourceWrapper) dataSource; + + assertThat(DataSourceWrapper.getDecoratedDataSource()).isInstanceOf(CustomDataSourceProxy.class); + CustomDataSourceProxy customDataSourceProxy = (CustomDataSourceProxy) DataSourceWrapper + .getDecoratedDataSource(); + + assertThat(customDataSourceProxy.delegate).isInstanceOf(P6DataSource.class); + P6DataSource p6DataSource = (P6DataSource) customDataSourceProxy.delegate; + + DataSource p6WrappedDataSource = (DataSource) ReflectionTestUtils.getField(p6DataSource, "realDataSource"); + assertThat(p6WrappedDataSource).isInstanceOf(ProxyDataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) p6WrappedDataSource; + }); + } + + @Test + void testDecoratingCanBeDisabled() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.enabled:false"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + assertThat(dataSource).isInstanceOf(HikariDataSource.class); + }); + } + + @Test + void testDecoratingCanBeDisabledForSpecificBeans() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.excluded-data-source-bean-names:secondDataSource") + .withUserConfiguration(TestMultiDataSourceConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean("dataSource", DataSource.class); + assertThat(dataSource).isInstanceOf(DataSourceWrapper.class); + + DataSource secondDataSource = context.getBean("secondDataSource", DataSource.class); + assertThat(secondDataSource).isInstanceOf(BasicDataSource.class); + }); + } + + @Test + void testDecoratingChainBuiltCorrectly() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + DataSourceWrapper dataSource1 = context.getBean(DataSourceWrapper.class); + assertThat(dataSource1).isNotNull(); + + DataSource p6DataSource = dataSource1.getDecoratedDataSource(); + assertThat(p6DataSource).isNotNull(); + assertThat(p6DataSource).isInstanceOf(P6DataSource.class); + + DataSource proxyDataSource = (DataSource) new DirectFieldAccessor(p6DataSource) + .getPropertyValue("realDataSource"); + assertThat(proxyDataSource).isNotNull(); + assertThat(proxyDataSource).isInstanceOf(ProxyDataSource.class); + }); + } + + @Test + void testDecorateDynamicallyRegisteredBeans() { + ApplicationContextRunner contextRunner = this.contextRunner.withInitializer(context -> { + GenericApplicationContext gac = (GenericApplicationContext) context; + gac.registerBean("ds1", DataSource.class, () -> new HikariDataSource()); + gac.registerBean("ds2", DataSource.class, () -> new HikariDataSource()); + }); + + contextRunner.run(context -> { + DataSource dataSource1 = context.getBean("ds1", DataSource.class); + assertThat(dataSource1).isNotNull(); + assertThat(dataSource1).isInstanceOf(DataSourceWrapper.class); + + DataSource dataSource2 = context.getBean("ds2", DataSource.class); + assertThat(dataSource2).isNotNull(); + assertThat(dataSource2).isInstanceOf(DataSourceWrapper.class); + }); + } + + @Configuration + static class TestDataSourceConfiguration { + + @Bean + public DataSource dataSource() { + BasicDataSource pool = new BasicDataSource(); + pool.setDriverClassName("org.hsqldb.jdbcDriver"); + pool.setUrl("jdbc:hsqldb:target/overridedb"); + pool.setUsername("sa"); + return pool; + } + + } + + @Configuration + static class TestDataSourceDecoratorConfiguration { + + @Bean + public DataSourceDecorator customDataSourceDecorator() { + return (beanName, dataSource) -> new CustomDataSourceProxy(dataSource); + } + + } + + @Configuration + static class TestMultiDataSourceConfiguration { + + @Bean + @Primary + public DataSource dataSource() { + BasicDataSource pool = new BasicDataSource(); + pool.setDriverClassName("org.hsqldb.jdbcDriver"); + pool.setUrl("jdbc:hsqldb:target/db"); + pool.setUsername("sa"); + return pool; + } + + @Bean + public DataSource secondDataSource() { + BasicDataSource pool = new BasicDataSource(); + pool.setDriverClassName("org.hsqldb.jdbcDriver"); + pool.setUrl("jdbc:hsqldb:target/db2"); + pool.setUsername("sa"); + return pool; + } + + } + + @Configuration + static class TestScopedDataSourceConfiguration { + + @Bean + @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) + public DataSource dataSource() { + BasicDataSource pool = new BasicDataSource(); + pool.setDriverClassName("org.hsqldb.jdbcDriver"); + pool.setUrl("jdbc:hsqldb:target/overridedb"); + pool.setUsername("sa"); + return pool; + } + + } + + /** + * Custom proxy data source for tests. + * + * @author Arthur Gavlyukovskiy + */ + static class CustomDataSourceProxy implements DataSource { + + private final DataSource delegate; + + CustomDataSourceProxy(DataSource delegate) { + this.delegate = delegate; + } + + @Override + public Connection getConnection() { + return null; + } + + @Override + public Connection getConnection(String username, String password) { + return null; + } + + @Override + public T unwrap(Class iface) { + return null; + } + + @Override + public boolean isWrapperFor(Class iface) { + return false; + } + + @Override + public PrintWriter getLogWriter() { + return null; + } + + @Override + public void setLogWriter(PrintWriter out) { + + } + + @Override + public void setLoginTimeout(int seconds) { + + } + + @Override + public int getLoginTimeout() { + return 0; + } + + @Override + public Logger getParentLogger() { + return null; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java index 0573e862d..2c5a9c551 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java @@ -1,280 +1,278 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import com.p6spy.engine.common.ConnectionInformation; -import com.p6spy.engine.common.P6LogQuery; -import com.p6spy.engine.event.CompoundJdbcEventListener; -import com.p6spy.engine.event.JdbcEventListener; -import com.p6spy.engine.logging.Category; -import com.p6spy.engine.logging.LoggingEventListener; -import com.p6spy.engine.spy.JdbcEventListenerFactory; -import com.p6spy.engine.spy.P6DataSource; -import com.p6spy.engine.spy.appender.CustomLineFormat; -import com.p6spy.engine.spy.appender.FormattedLogger; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; - -class P6SpyConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceJdbcAutoConfiguration.class, TraceNoOpAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.sleuth.noop.enabled=true", - "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) - .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); - - @BeforeEach - @AfterEach - void resetLogAccumulator() { - LogAccumulator.reset(); - } - - @Test - void testCustomListeners() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomListenerConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - GetCountingListener getCountingListener = context.getBean(GetCountingListener.class); - ClosingCountingListener closingCountingListener = context.getBean(ClosingCountingListener.class); - P6DataSource p6DataSource = (P6DataSource) ((DataSourceWrapper) dataSource).getDecoratedDataSource(); - assertThat(p6DataSource).extracting("jdbcEventListenerFactory").isEqualTo(jdbcEventListenerFactory); - - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).contains(getCountingListener, closingCountingListener); - assertThat(getCountingListener.connectionCount).isEqualTo(0); - - Connection connection1 = p6DataSource.getConnection(); - - assertThat(getCountingListener.connectionCount).isEqualTo(1); - assertThat(closingCountingListener.connectionCount).isEqualTo(0); - - Connection connection2 = p6DataSource.getConnection(); - - assertThat(getCountingListener.connectionCount).isEqualTo(2); - - // order matters! - connection2.close(); - - assertThat(closingCountingListener.connectionCount).isEqualTo(1); - - // order matters! - connection1.close(); - - assertThat(closingCountingListener.connectionCount).isEqualTo(2); - }); - } - - @Test - void testDoesNotRegisterLoggingListenerIfDisabled() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.p6spy.enable-logging=false"); - - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).extracting("class") - .doesNotContain(LoggingEventListener.class); - }); - } - - @Test - void testCanSetCustomLoggingFormat() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.p6spy.log-format:test %{connectionId}"); - - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); - assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") - .isEqualTo(CustomLineFormat.class); - }); - } - - @Test - void testMultilineShouldNotOverrideCustomProperties() { - System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); - System.setProperty("p6spy.config.excludecategories", "debug"); - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.p6spy.multiline:true"); - - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - - assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); - assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") - .isEqualTo(CustomLineFormat.class); - }); - } - - @Test - void testUseCustomLogger() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.sleuth.jdbc.p6spy.logging=custom", - "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName()); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - dataSource.getConnection().close(); - - assertThat(P6LogQuery.getLogger()).isInstanceOf(LogAccumulator.class); - }); - } - - @Test - void testLogFilterPattern() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.sleuth.jdbc.p6spy.logging=custom", - "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), - "spring.sleuth.jdbc.p6spy.log-filter.pattern:.*table1.*"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - try (Connection connection = dataSource.getConnection(); - PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); - PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { - ps1.execute(); - ps2.execute(); - } - - assertThat(LogAccumulator.MESSAGES).hasSize(1); - assertThat(LogAccumulator.MESSAGES).allMatch(message -> message.contains("table1")); - }); - } - - @Test - void testLogFilterPatternMatchAll() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( - "spring.sleuth.jdbc.p6spy.logging=custom", - "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), - "spring.sleuth.jdbc.p6spy.log-filter.pattern:.*"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - try (Connection connection = dataSource.getConnection(); - PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); - PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { - ps1.execute(); - ps2.execute(); - } - - assertThat(LogAccumulator.MESSAGES).hasSize(2); - }); - } - - @Configuration - static class CustomListenerConfiguration { - - @Bean - public GetCountingListener wrappingCountingListener() { - return new GetCountingListener(); - } - - @Bean - public ClosingCountingListener closingCountingListener() { - return new ClosingCountingListener(); - } - - } - - static class GetCountingListener extends JdbcEventListener { - - int connectionCount = 0; - - @Override - public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) { - connectionCount++; - } - - } - - static class ClosingCountingListener extends JdbcEventListener { - - int connectionCount = 0; - - @Override - public void onAfterConnectionClose(ConnectionInformation connectionInformation, SQLException e) { - connectionCount++; - } - - } - - public static class LogAccumulator extends FormattedLogger { - - static final List MESSAGES = new ArrayList<>(); - static final List EXCEPTIONS = new ArrayList<>(); - - public static void reset() { - MESSAGES.clear(); - EXCEPTIONS.clear(); - } - - @Override - public void logException(Exception e) { - EXCEPTIONS.add(e); - } - - @Override - public void logText(String text) { - MESSAGES.add(text); - } - - @Override - public boolean isCategoryEnabled(Category category) { - return true; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import com.p6spy.engine.common.ConnectionInformation; +import com.p6spy.engine.common.P6LogQuery; +import com.p6spy.engine.event.CompoundJdbcEventListener; +import com.p6spy.engine.event.JdbcEventListener; +import com.p6spy.engine.logging.Category; +import com.p6spy.engine.logging.LoggingEventListener; +import com.p6spy.engine.spy.JdbcEventListenerFactory; +import com.p6spy.engine.spy.P6DataSource; +import com.p6spy.engine.spy.appender.CustomLineFormat; +import com.p6spy.engine.spy.appender.FormattedLogger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +class P6SpyConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(DataSourceAutoConfiguration.class, TraceJdbcAutoConfiguration.class, + TraceNoOpAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", "spring.sleuth.noop.enabled=true", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); + + @BeforeEach + @AfterEach + void resetLogAccumulator() { + LogAccumulator.reset(); + } + + @Test + void testCustomListeners() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomListenerConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + GetCountingListener getCountingListener = context.getBean(GetCountingListener.class); + ClosingCountingListener closingCountingListener = context.getBean(ClosingCountingListener.class); + P6DataSource p6DataSource = (P6DataSource) ((DataSourceWrapper) dataSource).getDecoratedDataSource(); + assertThat(p6DataSource).extracting("jdbcEventListenerFactory").isEqualTo(jdbcEventListenerFactory); + + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).contains(getCountingListener, closingCountingListener); + assertThat(getCountingListener.connectionCount).isEqualTo(0); + + Connection connection1 = p6DataSource.getConnection(); + + assertThat(getCountingListener.connectionCount).isEqualTo(1); + assertThat(closingCountingListener.connectionCount).isEqualTo(0); + + Connection connection2 = p6DataSource.getConnection(); + + assertThat(getCountingListener.connectionCount).isEqualTo(2); + + // order matters! + connection2.close(); + + assertThat(closingCountingListener.connectionCount).isEqualTo(1); + + // order matters! + connection1.close(); + + assertThat(closingCountingListener.connectionCount).isEqualTo(2); + }); + } + + @Test + void testDoesNotRegisterLoggingListenerIfDisabled() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.p6spy.enable-logging=false"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class") + .doesNotContain(LoggingEventListener.class); + }); + } + + @Test + void testCanSetCustomLoggingFormat() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.p6spy.log-format:test %{connectionId}"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); + assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") + .isEqualTo(CustomLineFormat.class); + }); + } + + @Test + void testMultilineShouldNotOverrideCustomProperties() { + System.setProperty("p6spy.config.logMessageFormat", "com.p6spy.engine.spy.appender.CustomLineFormat"); + System.setProperty("p6spy.config.excludecategories", "debug"); + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.p6spy.multiline:true"); + + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + + assertThat(jdbcEventListener.getEventListeners()).extracting("class").contains(LoggingEventListener.class); + assertThat(P6LogQuery.getLogger()).extracting("strategy").extracting("class") + .isEqualTo(CustomLineFormat.class); + }); + } + + @Test + void testUseCustomLogger() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.p6spy.logging=custom", + "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName()); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + dataSource.getConnection().close(); + + assertThat(P6LogQuery.getLogger()).isInstanceOf(LogAccumulator.class); + }); + } + + @Test + void testLogFilterPattern() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.p6spy.logging=custom", + "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), + "spring.sleuth.jdbc.p6spy.log-filter.pattern:.*table1.*"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); + PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { + ps1.execute(); + ps2.execute(); + } + + assertThat(LogAccumulator.MESSAGES).hasSize(1); + assertThat(LogAccumulator.MESSAGES).allMatch(message -> message.contains("table1")); + }); + } + + @Test + void testLogFilterPatternMatchAll() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.p6spy.logging=custom", + "spring.sleuth.jdbc.p6spy.custom-appender-class:" + LogAccumulator.class.getName(), + "spring.sleuth.jdbc.p6spy.log-filter.pattern:.*"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps1 = connection.prepareStatement("select 1 /* from table1 */"); + PreparedStatement ps2 = connection.prepareStatement("select 1 /* from table2 */")) { + ps1.execute(); + ps2.execute(); + } + + assertThat(LogAccumulator.MESSAGES).hasSize(2); + }); + } + + @Configuration + static class CustomListenerConfiguration { + + @Bean + public GetCountingListener wrappingCountingListener() { + return new GetCountingListener(); + } + + @Bean + public ClosingCountingListener closingCountingListener() { + return new ClosingCountingListener(); + } + + } + + static class GetCountingListener extends JdbcEventListener { + + int connectionCount = 0; + + @Override + public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) { + connectionCount++; + } + + } + + static class ClosingCountingListener extends JdbcEventListener { + + int connectionCount = 0; + + @Override + public void onAfterConnectionClose(ConnectionInformation connectionInformation, SQLException e) { + connectionCount++; + } + + } + + public static class LogAccumulator extends FormattedLogger { + + static final List MESSAGES = new ArrayList<>(); + static final List EXCEPTIONS = new ArrayList<>(); + + public static void reset() { + MESSAGES.clear(); + EXCEPTIONS.clear(); + } + + @Override + public void logException(Exception e) { + EXCEPTIONS.add(e); + } + + @Override + public void logText(String text) { + MESSAGES.add(text); + } + + @Override + public boolean isCategoryEnabled(Category category) { + return true; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java index 6b10c2d95..844f34a23 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java @@ -1,239 +1,238 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import net.ttddyy.dsproxy.ExecutionInfo; -import net.ttddyy.dsproxy.QueryInfo; -import net.ttddyy.dsproxy.listener.ChainListener; -import net.ttddyy.dsproxy.listener.QueryExecutionListener; -import net.ttddyy.dsproxy.listener.logging.CommonsQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.CommonsSlowQueryListener; -import net.ttddyy.dsproxy.listener.logging.JULQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.JULSlowQueryListener; -import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.SLF4JSlowQueryListener; -import net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener; -import net.ttddyy.dsproxy.listener.logging.SystemOutSlowQueryListener; -import net.ttddyy.dsproxy.proxy.DefaultConnectionIdManager; -import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; -import net.ttddyy.dsproxy.support.ProxyDataSource; -import net.ttddyy.dsproxy.transform.ParameterTransformer; -import net.ttddyy.dsproxy.transform.QueryTransformer; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; - -import static org.assertj.core.api.Assertions.assertThat; - -class ProxyDataSourceConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceJdbcAutoConfiguration.class, TraceNoOpAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.sleuth.noop.enabled=true", - "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) - .withClassLoader(new FilteredClassLoader("com.p6spy")); - - @Test - void testRegisterLogAndSlowQueryLogByDefaultToSlf4j() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogByUsingSlf4j() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.datasource-proxy.logging=slf4j"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogUsingSystemOut() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.datasource-proxy.logging=sysout"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutQueryLoggingListener.class); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogUsingJUL() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.datasourceProxy.logging=jul"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(JULSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(JULQueryLoggingListener.class); - }); - } - - @Test - void testRegisterLogAndSlowQueryLogUsingApacheCommons() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.datasourceProxy.logging=commons"); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(CommonsSlowQueryListener.class); - assertThat(chainListener.getListeners()).extracting("class").contains(CommonsQueryLoggingListener.class); - }); - } - - @Test - void testCustomParameterAndQueryTransformer() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomDataSourceProxyConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ParameterTransformer parameterTransformer = context.getBean(ParameterTransformer.class); - QueryTransformer queryTransformer = context.getBean(QueryTransformer.class); - assertThat(proxyDataSource.getProxyConfig().getParameterTransformer()).isSameAs(parameterTransformer); - assertThat(proxyDataSource.getProxyConfig().getQueryTransformer()).isSameAs(queryTransformer); - }); - } - - @Test - void testCustomListeners() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomListenerConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - QueryExecutionListener queryExecutionListener = context.getBean(QueryExecutionListener.class); - - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).contains(queryExecutionListener); - }); - } - - @Test - void testGlobalConnectionIdManagerByDefault() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - - assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(GlobalConnectionIdManager.class); - }); - } - - @Test - void testCustomConnectionIdManager() { - ApplicationContextRunner contextRunner = this.contextRunner - .withUserConfiguration(CustomDataSourceProxyConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - - assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(DefaultConnectionIdManager.class); - }); - } - - @Configuration - static class CustomDataSourceProxyConfiguration { - - @Bean - public ParameterTransformer parameterTransformer() { - return (replacer, transformInfo) -> { - }; - } - - @Bean - public QueryTransformer queryTransformer() { - return (transformInfo) -> "TestQuery"; - } - - @Bean - public DataSourceProxyConnectionIdManagerProvider connectionIdManagerProvider() { - return DefaultConnectionIdManager::new; - } - - } - - @Configuration - static class CustomListenerConfiguration { - - @Bean - @Primary - public QueryExecutionListener queryExecutionListener() { - return new QueryExecutionListener() { - @Override - public void beforeQuery(ExecutionInfo execInfo, List queryInfoList) { - System.out.println("beforeQuery"); - } - - @Override - public void afterQuery(ExecutionInfo execInfo, List queryInfoList) { - System.out.println("afterQuery"); - } - }; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import net.ttddyy.dsproxy.ExecutionInfo; +import net.ttddyy.dsproxy.QueryInfo; +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.listener.QueryExecutionListener; +import net.ttddyy.dsproxy.listener.logging.CommonsQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.CommonsSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.JULQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.JULSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener; +import net.ttddyy.dsproxy.listener.logging.SystemOutSlowQueryListener; +import net.ttddyy.dsproxy.proxy.DefaultConnectionIdManager; +import net.ttddyy.dsproxy.proxy.GlobalConnectionIdManager; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import net.ttddyy.dsproxy.transform.ParameterTransformer; +import net.ttddyy.dsproxy.transform.QueryTransformer; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceProxyConnectionIdManagerProvider; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import static org.assertj.core.api.Assertions.assertThat; + +class ProxyDataSourceConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(DataSourceAutoConfiguration.class, TraceJdbcAutoConfiguration.class, + TraceNoOpAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", "spring.sleuth.noop.enabled=true", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("com.p6spy")); + + @Test + void testRegisterLogAndSlowQueryLogByDefaultToSlf4j() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogByUsingSlf4j() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.datasource-proxy.logging=slf4j"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SLF4JQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingSystemOut() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.datasource-proxy.logging=sysout"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(SystemOutQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingJUL() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.datasourceProxy.logging=jul"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(JULSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(JULQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogUsingApacheCommons() { + ApplicationContextRunner contextRunner = this.contextRunner + .withPropertyValues("spring.sleuth.jdbc.datasourceProxy.logging=commons"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(CommonsSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class").contains(CommonsQueryLoggingListener.class); + }); + } + + @Test + void testCustomParameterAndQueryTransformer() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomDataSourceProxyConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ParameterTransformer parameterTransformer = context.getBean(ParameterTransformer.class); + QueryTransformer queryTransformer = context.getBean(QueryTransformer.class); + assertThat(proxyDataSource.getProxyConfig().getParameterTransformer()).isSameAs(parameterTransformer); + assertThat(proxyDataSource.getProxyConfig().getQueryTransformer()).isSameAs(queryTransformer); + }); + } + + @Test + void testCustomListeners() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomListenerConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + QueryExecutionListener queryExecutionListener = context.getBean(QueryExecutionListener.class); + + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).contains(queryExecutionListener); + }); + } + + @Test + void testGlobalConnectionIdManagerByDefault() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + + assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(GlobalConnectionIdManager.class); + }); + } + + @Test + void testCustomConnectionIdManager() { + ApplicationContextRunner contextRunner = this.contextRunner + .withUserConfiguration(CustomDataSourceProxyConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + + assertThat(proxyDataSource.getConnectionIdManager()).isInstanceOf(DefaultConnectionIdManager.class); + }); + } + + @Configuration + static class CustomDataSourceProxyConfiguration { + + @Bean + public ParameterTransformer parameterTransformer() { + return (replacer, transformInfo) -> { + }; + } + + @Bean + public QueryTransformer queryTransformer() { + return (transformInfo) -> "TestQuery"; + } + + @Bean + public DataSourceProxyConnectionIdManagerProvider connectionIdManagerProvider() { + return DefaultConnectionIdManager::new; + } + + } + + @Configuration + static class CustomListenerConfiguration { + + @Bean + @Primary + public QueryExecutionListener queryExecutionListener() { + return new QueryExecutionListener() { + @Override + public void beforeQuery(ExecutionInfo execInfo, List queryInfoList) { + System.out.println("beforeQuery"); + } + + @Override + public void afterQuery(ExecutionInfo execInfo, List queryInfoList) { + System.out.println("afterQuery"); + } + }; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java index 0d1153ef0..af03d9335 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthP6SpyListenerAutoConfigurationTests.java @@ -1,66 +1,65 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.concurrent.ThreadLocalRandom; - -import com.p6spy.engine.event.CompoundJdbcEventListener; -import com.p6spy.engine.spy.JdbcEventListenerFactory; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; - -import static org.assertj.core.api.Assertions.assertThat; - -class SleuthP6SpyListenerAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceJdbcAutoConfiguration.class, TraceNoOpAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.sleuth.noop.enabled=true", - "spring.datasource.url=jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) - .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); - - @Test - void testAddsP6SpyListener() { - contextRunner.run(context -> { - JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); - CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory - .createJdbcEventListener(); - assertThat(jdbcEventListener.getEventListeners()).extracting("class") - .contains(TraceJdbcEventListener.class); - }); - } - - @Test - void testDoesNotAddP6SpyListenerIfNoTracer() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues("spring.sleuth.enabled=false"); - - contextRunner.run(context -> { - assertThat(context).doesNotHaveBean(JdbcEventListenerFactory.class); - }); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.concurrent.ThreadLocalRandom; + +import com.p6spy.engine.event.CompoundJdbcEventListener; +import com.p6spy.engine.spy.JdbcEventListenerFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceJdbcEventListener; + +import static org.assertj.core.api.Assertions.assertThat; + +class SleuthP6SpyListenerAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(DataSourceAutoConfiguration.class, TraceJdbcAutoConfiguration.class, + TraceNoOpAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", "spring.sleuth.noop.enabled=true", + "spring.datasource.url=jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); + + @Test + void testAddsP6SpyListener() { + contextRunner.run(context -> { + JdbcEventListenerFactory jdbcEventListenerFactory = context.getBean(JdbcEventListenerFactory.class); + CompoundJdbcEventListener jdbcEventListener = (CompoundJdbcEventListener) jdbcEventListenerFactory + .createJdbcEventListener(); + assertThat(jdbcEventListener.getEventListeners()).extracting("class") + .contains(TraceJdbcEventListener.class); + }); + } + + @Test + void testDoesNotAddP6SpyListenerIfNoTracer() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues("spring.sleuth.enabled=false"); + + contextRunner.run(context -> { + assertThat(context).doesNotHaveBean(JdbcEventListenerFactory.class); + }); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java index 9380ce316..1bc7cd052 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/SleuthProxyDataSourceListenerAutoConfigurationTests.java @@ -1,69 +1,68 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; - -import java.util.concurrent.ThreadLocalRandom; - -import javax.sql.DataSource; - -import net.ttddyy.dsproxy.listener.ChainListener; -import net.ttddyy.dsproxy.support.ProxyDataSource; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; -import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; -import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; - -import static org.assertj.core.api.Assertions.assertThat; - -class SleuthProxyDataSourceListenerAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceJdbcAutoConfiguration.class, TraceNoOpAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.sleuth.noop.enabled=true", - "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) - .withClassLoader(new FilteredClassLoader("com.p6spy")); - - @Test - void testAddsDatasourceProxyListener() { - contextRunner.run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) - .getDecoratedDataSource(); - ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); - assertThat(chainListener.getListeners()).extracting("class").contains(TraceQueryExecutionListener.class); - }); - } - - @Test - void testDoesNotAddDatasourceProxyListenerIfNoTracer() { - ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues("spring.sleuth.enabled:false"); - - contextRunner.run(context -> { - assertThat(context).doesNotHaveBean(DataSourceWrapper.class); - }); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.jdbc; + +import java.util.concurrent.ThreadLocalRandom; + +import javax.sql.DataSource; + +import net.ttddyy.dsproxy.listener.ChainListener; +import net.ttddyy.dsproxy.support.ProxyDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceWrapper; +import org.springframework.cloud.sleuth.instrument.jdbc.TraceQueryExecutionListener; + +import static org.assertj.core.api.Assertions.assertThat; + +class SleuthProxyDataSourceListenerAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(DataSourceAutoConfiguration.class, TraceJdbcAutoConfiguration.class, + TraceNoOpAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) + .withPropertyValues("spring.datasource.initialization-mode=never", "spring.sleuth.noop.enabled=true", + "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) + .withClassLoader(new FilteredClassLoader("com.p6spy")); + + @Test + void testAddsDatasourceProxyListener() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").contains(TraceQueryExecutionListener.class); + }); + } + + @Test + void testDoesNotAddDatasourceProxyListenerIfNoTracer() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues("spring.sleuth.enabled:false"); + + contextRunner.run(context -> { + assertThat(context).doesNotHaveBean(DataSourceWrapper.class); + }); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java index 3fd40e9d5..52fe0ae6b 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java @@ -1,43 +1,43 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.jdbc; - -import javax.sql.CommonDataSource; - -import org.springframework.cloud.sleuth.Span; - -/** - * Customizer for {@link TraceListenerStrategy} client span. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -public interface TraceListenerStrategySpanCustomizer { - - /** - * Customizes the client database span. - * @param spanBuilder span builder - */ - void customizeConnectionSpan(T dataSource, Span.Builder spanBuilder); - - /** - * @param dataSource data source for which we're building the span - * @return {@code true} when this customizer can be applied - */ - boolean isApplicable(CommonDataSource dataSource); - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import javax.sql.CommonDataSource; + +import org.springframework.cloud.sleuth.Span; + +/** + * Customizer for {@link TraceListenerStrategy} client span. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface TraceListenerStrategySpanCustomizer { + + /** + * Customizes the client database span. + * @param spanBuilder span builder + */ + void customizeConnectionSpan(T dataSource, Span.Builder spanBuilder); + + /** + * @param dataSource data source for which we're building the span + * @return {@code true} when this customizer can be applied + */ + boolean isApplicable(CommonDataSource dataSource); + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceType.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceType.java index 2348ae3fc..cef75651a 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceType.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceType.java @@ -1,39 +1,39 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.jdbc; - -/** - * Trace types. - */ -public enum TraceType { - - /** - * Related to JDBC connections. - */ - CONNECTION, - - /** - * Related to query executions. - */ - QUERY, - - /** - * Related to ResultSets. - */ - FETCH - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +/** + * Trace types. + */ +public enum TraceType { + + /** + * Related to JDBC connections. + */ + CONNECTION, + + /** + * Related to query executions. + */ + QUERY, + + /** + * Related to ResultSets. + */ + FETCH + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingJdbcEventListenerTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingJdbcEventListenerTests.java index 96b6c08aa..0abe1b11e 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingJdbcEventListenerTests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingJdbcEventListenerTests.java @@ -1,60 +1,60 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.brave.instrument.jdbc; - -import brave.sampler.Sampler; - -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; -import org.springframework.cloud.sleuth.test.TestSpanHandler; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -public class TracingJdbcEventListenerTests - extends org.springframework.cloud.sleuth.instrument.jdbc.TracingJdbcEventListenerTests { - - @Override - protected Class autoConfiguration() { - return BraveAutoConfiguration.class; - } - - @Override - protected Class testConfiguration() { - return Config.class; - } - - @Configuration(proxyBeanMethods = false) - static class Config { - - @Bean - TestSpanHandler testSpanHandlerSupplier(brave.test.TestSpanHandler testSpanHandler) { - return new BraveTestSpanHandler(testSpanHandler); - } - - @Bean - Sampler alwaysSampler() { - return Sampler.ALWAYS_SAMPLE; - } - - @Bean - brave.test.TestSpanHandler braveTestSpanHandler() { - return new brave.test.TestSpanHandler(); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.jdbc; + +import brave.sampler.Sampler; + +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +public class TracingJdbcEventListenerTests + extends org.springframework.cloud.sleuth.instrument.jdbc.TracingJdbcEventListenerTests { + + @Override + protected Class autoConfiguration() { + return BraveAutoConfiguration.class; + } + + @Override + protected Class testConfiguration() { + return Config.class; + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + TestSpanHandler testSpanHandlerSupplier(brave.test.TestSpanHandler testSpanHandler) { + return new BraveTestSpanHandler(testSpanHandler); + } + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + brave.test.TestSpanHandler braveTestSpanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingQueryExecutionListenerTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingQueryExecutionListenerTests.java index c2a442054..e9bacd2b8 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingQueryExecutionListenerTests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-jdbc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/jdbc/TracingQueryExecutionListenerTests.java @@ -1,60 +1,60 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.brave.instrument.jdbc; - -import brave.sampler.Sampler; - -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; -import org.springframework.cloud.sleuth.test.TestSpanHandler; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -public class TracingQueryExecutionListenerTests - extends org.springframework.cloud.sleuth.instrument.jdbc.TracingQueryExecutionListenerTests { - - @Override - protected Class autoConfiguration() { - return BraveAutoConfiguration.class; - } - - @Override - protected Class testConfiguration() { - return Config.class; - } - - @Configuration(proxyBeanMethods = false) - static class Config { - - @Bean - TestSpanHandler testSpanHandlerSupplier(brave.test.TestSpanHandler testSpanHandler) { - return new BraveTestSpanHandler(testSpanHandler); - } - - @Bean - Sampler alwaysSampler() { - return Sampler.ALWAYS_SAMPLE; - } - - @Bean - brave.test.TestSpanHandler braveTestSpanHandler() { - return new brave.test.TestSpanHandler(); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.jdbc; + +import brave.sampler.Sampler; + +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +public class TracingQueryExecutionListenerTests + extends org.springframework.cloud.sleuth.instrument.jdbc.TracingQueryExecutionListenerTests { + + @Override + protected Class autoConfiguration() { + return BraveAutoConfiguration.class; + } + + @Override + protected Class testConfiguration() { + return Config.class; + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + TestSpanHandler testSpanHandlerSupplier(brave.test.TestSpanHandler testSpanHandler) { + return new BraveTestSpanHandler(testSpanHandler); + } + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + brave.test.TestSpanHandler braveTestSpanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java index 5ebdeb20d..e24da75ef 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java @@ -1,237 +1,238 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.brave.instrument.web; - -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import brave.Span; -import brave.Tracer; -import brave.handler.MutableSpan; -import brave.handler.SpanHandler; -import brave.sampler.Sampler; -import brave.test.TestSpanHandler; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.sleuth.SpanName; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.EnableAsync; - -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.assertj.core.api.BDDAssertions.then; - -@SpringBootTest(classes = { TraceAsyncIntegrationTests.TraceAsyncITestConfiguration.class }) -public class TraceAsyncIntegrationTests { - - @Autowired - ClassPerformingAsyncLogic classPerformingAsyncLogic; - - @Autowired - Tracer tracer; - - @Autowired - TestSpanHandler spans; - - @BeforeEach - public void cleanup() { - this.spans.clear(); - this.classPerformingAsyncLogic.clear(); - } - - @Test - public void should_set_span_on_an_async_annotated_method() { - whenAsyncProcessingTakesPlace(); - - thenANewAsyncSpanGetsCreated(); - } - - @Test - public void should_set_span_with_custom_method_on_an_async_annotated_method() { - whenAsyncProcessingTakesPlaceWithCustomSpanName(); - - thenAsyncSpanHasCustomName(); - } - - @Test - public void should_continue_a_span_on_an_async_annotated_method() { - Span span = givenASpanInCurrentThread(); - - try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { - whenAsyncProcessingTakesPlace(); - } - finally { - span.finish(); - } - - thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(span); - } - - @Test - public void should_continue_a_span_with_custom_method_on_an_async_annotated_method() { - Span span = givenASpanInCurrentThread(); - - try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { - whenAsyncProcessingTakesPlaceWithCustomSpanName(); - } - finally { - span.finish(); - } - - thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(span); - } - - private Span givenASpanInCurrentThread() { - return this.tracer.nextSpan().name("http:existing"); - } - - private void whenAsyncProcessingTakesPlace() { - this.classPerformingAsyncLogic.invokeAsynchronousLogic(); - } - - private void whenAsyncProcessingTakesPlaceWithCustomSpanName() { - this.classPerformingAsyncLogic.customNameInvokeAsynchronousLogic(); - } - - private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(final Span span) { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) - .isEqualTo(span.context().traceId()); - List webSpans = this.spans.spans().stream().filter(mutableSpan -> mutableSpan.traceId().equalsIgnoreCase(span.context().traceIdString())) - .collect(Collectors.toList()); - then(webSpans).hasSize(2); - // HTTP - then(webSpans.get(0).name()).isEqualTo("http:existing"); - // ASYNC - then(webSpans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "invokeAsynchronousLogic"); - }); - } - - private void thenANewAsyncSpanGetsCreated() { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(this.spans).hasSize(1); - MutableSpan storedSpan = this.spans.get(0); - then(storedSpan.name()).isEqualTo("invoke-asynchronous-logic"); - then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "invokeAsynchronousLogic"); - }); - } - - private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(final Span span) { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan()).isNotNull(); - then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) - .isEqualTo(span.context().traceId()); - then(this.spans).hasSize(2); - // HTTP - then(this.spans.get(0).name()).isEqualTo("http:existing"); - // ASYNC - then(this.spans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "customNameInvokeAsynchronousLogic"); - }); - } - - private void thenAsyncSpanHasCustomName() { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(this.spans).hasSize(1); - MutableSpan storedSpan = this.spans.get(0); - then(storedSpan.name()).isEqualTo("foo"); - then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "customNameInvokeAsynchronousLogic"); - }); - } - - @AfterEach - public void cleanTrace() { - this.spans.clear(); - } - - @EnableAutoConfiguration - @EnableAsync - @Configuration(proxyBeanMethods = false) - static class TraceAsyncITestConfiguration { - - @Bean - ClassPerformingAsyncLogic asyncClass(Tracer tracer) { - return new ClassPerformingAsyncLogic(tracer); - } - - @Bean - Sampler defaultSampler() { - return Sampler.ALWAYS_SAMPLE; - } - - @Bean - SpanHandler testSpanHandler() { - return new TestSpanHandler(); - } - - @Bean - Executor fooExecutor() { - return new SimpleAsyncTaskExecutor(); - } - - @Bean - Executor barExecutor() { - return new SimpleAsyncTaskExecutor(); - } - - } - - static class ClassPerformingAsyncLogic { - - private final Tracer tracer; - - AtomicReference span = new AtomicReference<>(); - - ClassPerformingAsyncLogic(Tracer tracer) { - this.tracer = tracer; - } - - @Async("fooExecutor") - public void invokeAsynchronousLogic() { - this.span.set(this.tracer.currentSpan()); - } - - @Async - @SpanName("foo") - public void customNameInvokeAsynchronousLogic() { - this.span.set(this.tracer.currentSpan()); - } - - public Span getSpan() { - return this.span.get(); - } - - public void clear() { - this.span.set(null); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.web; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import brave.Span; +import brave.Tracer; +import brave.handler.MutableSpan; +import brave.handler.SpanHandler; +import brave.sampler.Sampler; +import brave.test.TestSpanHandler; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.SpanName; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.BDDAssertions.then; + +@SpringBootTest(classes = { TraceAsyncIntegrationTests.TraceAsyncITestConfiguration.class }) +public class TraceAsyncIntegrationTests { + + @Autowired + ClassPerformingAsyncLogic classPerformingAsyncLogic; + + @Autowired + Tracer tracer; + + @Autowired + TestSpanHandler spans; + + @BeforeEach + public void cleanup() { + this.spans.clear(); + this.classPerformingAsyncLogic.clear(); + } + + @Test + public void should_set_span_on_an_async_annotated_method() { + whenAsyncProcessingTakesPlace(); + + thenANewAsyncSpanGetsCreated(); + } + + @Test + public void should_set_span_with_custom_method_on_an_async_annotated_method() { + whenAsyncProcessingTakesPlaceWithCustomSpanName(); + + thenAsyncSpanHasCustomName(); + } + + @Test + public void should_continue_a_span_on_an_async_annotated_method() { + Span span = givenASpanInCurrentThread(); + + try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { + whenAsyncProcessingTakesPlace(); + } + finally { + span.finish(); + } + + thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(span); + } + + @Test + public void should_continue_a_span_with_custom_method_on_an_async_annotated_method() { + Span span = givenASpanInCurrentThread(); + + try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { + whenAsyncProcessingTakesPlaceWithCustomSpanName(); + } + finally { + span.finish(); + } + + thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(span); + } + + private Span givenASpanInCurrentThread() { + return this.tracer.nextSpan().name("http:existing"); + } + + private void whenAsyncProcessingTakesPlace() { + this.classPerformingAsyncLogic.invokeAsynchronousLogic(); + } + + private void whenAsyncProcessingTakesPlaceWithCustomSpanName() { + this.classPerformingAsyncLogic.customNameInvokeAsynchronousLogic(); + } + + private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(final Span span) { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) + .isEqualTo(span.context().traceId()); + List webSpans = this.spans.spans().stream() + .filter(mutableSpan -> mutableSpan.traceId().equalsIgnoreCase(span.context().traceIdString())) + .collect(Collectors.toList()); + then(webSpans).hasSize(2); + // HTTP + then(webSpans.get(0).name()).isEqualTo("http:existing"); + // ASYNC + then(webSpans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "invokeAsynchronousLogic"); + }); + } + + private void thenANewAsyncSpanGetsCreated() { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(this.spans).hasSize(1); + MutableSpan storedSpan = this.spans.get(0); + then(storedSpan.name()).isEqualTo("invoke-asynchronous-logic"); + then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "invokeAsynchronousLogic"); + }); + } + + private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(final Span span) { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan()).isNotNull(); + then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) + .isEqualTo(span.context().traceId()); + then(this.spans).hasSize(2); + // HTTP + then(this.spans.get(0).name()).isEqualTo("http:existing"); + // ASYNC + then(this.spans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "customNameInvokeAsynchronousLogic"); + }); + } + + private void thenAsyncSpanHasCustomName() { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(this.spans).hasSize(1); + MutableSpan storedSpan = this.spans.get(0); + then(storedSpan.name()).isEqualTo("foo"); + then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "customNameInvokeAsynchronousLogic"); + }); + } + + @AfterEach + public void cleanTrace() { + this.spans.clear(); + } + + @EnableAutoConfiguration + @EnableAsync + @Configuration(proxyBeanMethods = false) + static class TraceAsyncITestConfiguration { + + @Bean + ClassPerformingAsyncLogic asyncClass(Tracer tracer) { + return new ClassPerformingAsyncLogic(tracer); + } + + @Bean + Sampler defaultSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + SpanHandler testSpanHandler() { + return new TestSpanHandler(); + } + + @Bean + Executor fooExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + @Bean + Executor barExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + } + + static class ClassPerformingAsyncLogic { + + private final Tracer tracer; + + AtomicReference span = new AtomicReference<>(); + + ClassPerformingAsyncLogic(Tracer tracer) { + this.tracer = tracer; + } + + @Async("fooExecutor") + public void invokeAsynchronousLogic() { + this.span.set(this.tracer.currentSpan()); + } + + @Async + @SpanName("foo") + public void customNameInvokeAsynchronousLogic() { + this.span.set(this.tracer.currentSpan()); + } + + public Span getSpan() { + return this.span.get(); + } + + public void clear() { + this.span.set(null); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/view/Issue469Tests.java b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/view/Issue469Tests.java index 9f12664e7..530b1e496 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/view/Issue469Tests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/view/Issue469Tests.java @@ -1,60 +1,60 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.brave.instrument.web.view; - -import brave.test.TestSpanHandler; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.core.env.Environment; -import org.springframework.test.context.TestPropertySource; -import org.springframework.web.client.RestTemplate; - -import static org.assertj.core.api.BDDAssertions.then; - -@SpringBootTest(classes = Issue469.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@TestPropertySource(properties = { "spring.mvc.view.prefix=/WEB-INF/jsp/", "spring.mvc.view.suffix=.jsp" }) -public class Issue469Tests { - - @Autowired - TestSpanHandler spans; - - @Autowired - Environment environment; - - RestTemplate restTemplate = new RestTemplate(); - - @Test - public void should_not_result_in_tracing_exceptions_when_using_view_controllers() { - try { - this.restTemplate.getForObject("http://localhost:" + port() + "/welcome", String.class); - } - catch (Exception e) { - // JSPs are not rendered - then(e).hasMessageContaining("404"); - } - - Awaitility.await().untilAsserted(() -> then(this.spans).isNotEmpty()); - } - - private int port() { - return this.environment.getProperty("local.server.port", Integer.class); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.web.view; + +import brave.test.TestSpanHandler; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.env.Environment; +import org.springframework.test.context.TestPropertySource; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.BDDAssertions.then; + +@SpringBootTest(classes = Issue469.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = { "spring.mvc.view.prefix=/WEB-INF/jsp/", "spring.mvc.view.suffix=.jsp" }) +public class Issue469Tests { + + @Autowired + TestSpanHandler spans; + + @Autowired + Environment environment; + + RestTemplate restTemplate = new RestTemplate(); + + @Test + public void should_not_result_in_tracing_exceptions_when_using_view_controllers() { + try { + this.restTemplate.getForObject("http://localhost:" + port() + "/welcome", String.class); + } + catch (Exception e) { + // JSPs are not rendered + then(e).hasMessageContaining("404"); + } + + Awaitility.await().untilAsserted(() -> then(this.spans).isNotEmpty()); + } + + private int port() { + return this.environment.getProperty("local.server.port", Integer.class); + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingJdbcEventListenerTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingJdbcEventListenerTests.java index ac0cf1b03..776dd5aa1 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingJdbcEventListenerTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingJdbcEventListenerTests.java @@ -1,79 +1,78 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.jdbc; - -import java.sql.Connection; -import java.sql.PreparedStatement; - -import javax.sql.DataSource; - -import brave.handler.MutableSpan; -import brave.test.TestSpanHandler; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.autoconfig.instrument.jdbc.TraceJdbcAutoConfiguration; - -import static org.assertj.core.api.Assertions.assertThat; - -public abstract class TracingJdbcEventListenerTests extends TracingListenerStrategyTests { - - protected final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceJdbcAutoConfiguration.class, autoConfiguration(), - PropertyPlaceholderAutoConfiguration.class)) - .withUserConfiguration(testConfiguration()) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url=jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") - .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); - - @Override - ApplicationContextRunner parentContextRunner() { - return this.contextRunner; - } - - @Test - void testShouldUsePlaceholderInSqlTagOfSpansForPreparedStatementIfIncludeParameterValuesIsSetToFalse() { - contextRunner.withPropertyValues("spring.sleuth.jdbc.p6spy.tracing.include-parameter-values=false") - .run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - PreparedStatement preparedStatement = connection - .prepareStatement("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = ? WHERE 0 = ?"); - preparedStatement.setString(1, ""); - preparedStatement.setInt(2, 1); - preparedStatement.executeUpdate(); - connection.close(); - - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan connectionSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("update"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, - "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = ? WHERE 0 = ?"); - assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); - }); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +import javax.sql.DataSource; + +import brave.handler.MutableSpan; +import brave.test.TestSpanHandler; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.instrument.jdbc.TraceJdbcAutoConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +public abstract class TracingJdbcEventListenerTests extends TracingListenerStrategyTests { + + protected final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + TraceJdbcAutoConfiguration.class, autoConfiguration(), PropertyPlaceholderAutoConfiguration.class)) + .withUserConfiguration(testConfiguration()) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url=jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") + .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); + + @Override + ApplicationContextRunner parentContextRunner() { + return this.contextRunner; + } + + @Test + void testShouldUsePlaceholderInSqlTagOfSpansForPreparedStatementIfIncludeParameterValuesIsSetToFalse() { + contextRunner.withPropertyValues("spring.sleuth.jdbc.p6spy.tracing.include-parameter-values=false") + .run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + PreparedStatement preparedStatement = connection + .prepareStatement("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = ? WHERE 0 = ?"); + preparedStatement.setString(1, ""); + preparedStatement.setInt(2, 1); + preparedStatement.executeUpdate(); + connection.close(); + + assertThat(spanReporter.spans()).hasSize(2); + MutableSpan connectionSpan = spanReporter.spans().get(1); + MutableSpan statementSpan = spanReporter.spans().get(0); + assertThat(connectionSpan.name()).isEqualTo("connection"); + assertThat(statementSpan.name()).isEqualTo("update"); + assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, + "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = ? WHERE 0 = ?"); + assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); + }); + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingListenerStrategyTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingListenerStrategyTests.java index de201d45d..a598e9504 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingListenerStrategyTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingListenerStrategyTests.java @@ -1,743 +1,744 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.jdbc; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import javax.sql.DataSource; - -import com.zaxxer.hikari.HikariDataSource; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.test.context.assertj.AssertableApplicationContext; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.exporter.FinishedSpan; -import org.springframework.cloud.sleuth.test.TestSpanHandler; -import org.springframework.context.annotation.Bean; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -abstract class TracingListenerStrategyTests { - - public static final String SPAN_SQL_QUERY_TAG_NAME = "jdbc.query"; - - public static final String SPAN_ROW_COUNT_TAG_NAME = "jdbc.row-count"; - - abstract ApplicationContextRunner parentContextRunner(); - - protected abstract Class autoConfiguration(); - - protected abstract Class testConfiguration(); - - @Test - void testShouldAddSpanForConnection() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - connection.commit(); - connection.rollback(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(1); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(connectionSpan.getRemoteServiceName()).isEqualTo("TESTDB-BAZ"); - assertThat(connectionSpan.getEvents()).extracting("value").contains("jdbc.commit"); - assertThat(connectionSpan.getEvents()).extracting("value").contains("jdbc.rollback"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldAddSpanForConnectionWithFixedRemoteServiceName() { - parentContextRunner().withPropertyValues("spring.datasource.url:jdbc:h2:mem:testdb-baz?sleuthServiceName=aaaabbbb") - .run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - connection.commit(); - connection.rollback(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(1); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(connectionSpan.getRemoteServiceName()).isEqualTo("aaaabbbb"); - assertThat(connectionSpan.getEvents()).extracting("value").contains("jdbc.commit"); - assertThat(connectionSpan.getEvents()).extracting("value").contains("jdbc.rollback"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldAddSpanForPreparedStatementExecute() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - connection.prepareStatement("SELECT NOW()").execute(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(2); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(statementSpan.getName()).isEqualTo("select"); - assertThat(statementSpan.getRemoteServiceName()).isEqualTo("TESTDB-BAZ"); - assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldAddSpanForPreparedStatementExecuteUpdate() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - connection.prepareStatement("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1") - .executeUpdate(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(2); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(statementSpan.getName()).isEqualTo("update"); - assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, - "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); - assertThat(statementSpan.getTags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldAddSpanForStatementExecuteUpdate() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - connection.createStatement() - .executeUpdate("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(2); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(statementSpan.getName()).isEqualTo("update"); - assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, - "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); - assertThat(statementSpan.getTags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldAddSpanForPreparedStatementExecuteQueryIncludingTimeToCloseResultSet() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - ResultSet resultSet = connection.prepareStatement("SELECT NOW() UNION ALL select NOW()").executeQuery(); - resultSet.next(); - resultSet.next(); - resultSet.close(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(3); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); - FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(statementSpan.getName()).isEqualTo("select"); - assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, - "SELECT NOW() UNION ALL select NOW()"); - assertThat(resultSetSpan.getName()).isEqualTo("result-set"); - assertThat(resultSetSpan.getRemoteServiceName()).isEqualTo("TESTDB-BAZ"); - if (isP6Spy(context)) { - assertThat(resultSetSpan.getTags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "2"); - } - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldAddSpanForStatementAndResultSet() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - ResultSet resultSet = connection.createStatement().executeQuery("SELECT NOW()"); - resultSet.next(); - Thread.sleep(200L); - resultSet.close(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(3); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); - FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(statementSpan.getName()).isEqualTo("select"); - assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(resultSetSpan.getName()).isEqualTo("result-set"); - if (isP6Spy(context)) { - assertThat(resultSetSpan.getTags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "1"); - } - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenStatementIsClosedWihoutResultSet() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.next(); - statement.close(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(3); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); - FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(statementSpan.getName()).isEqualTo("select"); - assertThat(resultSetSpan.getName()).isEqualTo("result-set"); - assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenConnectionIsClosedWihoutResultSet() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.next(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(3); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); - FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(statementSpan.getName()).isEqualTo("select"); - assertThat(resultSetSpan.getName()).isEqualTo("result-set"); - assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenResultSetNextWasNotCalled() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(3); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); - FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(statementSpan.getName()).isEqualTo("select"); - assertThat(resultSetSpan.getName()).isEqualTo("result-set"); - assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenResourceIsAlreadyClosed() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.next(); - resultSet.close(); - resultSet.close(); - statement.close(); - statement.close(); - connection.close(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(3); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); - FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(statementSpan.getName()).isEqualTo("select"); - assertThat(resultSetSpan.getName()).isEqualTo("result-set"); - assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenResourceIsAlreadyClosed2() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - assertThatThrownBy(() -> { - connection.close(); - connection.prepareStatement("SELECT NOW()"); - }).isInstanceOf(SQLException.class); - - assertThat(spanReporter.reportedSpans()).hasSize(1); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenResourceIsAlreadyClosed3() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - assertThatThrownBy(() -> { - statement.close(); - statement.executeQuery("SELECT NOW()"); - }).isInstanceOf(SQLException.class); - connection.close(); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenResourceIsAlreadyClosed4() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - assertThatThrownBy(() -> { - resultSet.close(); - resultSet.next(); - }).isInstanceOf(SQLException.class); - statement.close(); - connection.close(); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailToCloseSpanForTwoConsecutiveConnections() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection1 = dataSource.getConnection(); - Connection connection2 = dataSource.getConnection(); - connection2.close(); - connection1.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(2); - FinishedSpan connection1Span = spanReporter.reportedSpans().get(0); - FinishedSpan connection2Span = spanReporter.reportedSpans().get(1); - assertThat(connection1Span.getName()).isEqualTo("connection"); - assertThat(connection2Span.getName()).isEqualTo("connection"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenClosedInReversedOrder() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.next(); - connection.close(); - statement.close(); - resultSet.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(3); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); - FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(statementSpan.getName()).isEqualTo("select"); - assertThat(resultSetSpan.getName()).isEqualTo("result-set"); - assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - @SuppressWarnings("unchecked") - void testShouldNotCauseMemoryLeakOnTomcatPool() { - parentContextRunner().withPropertyValues("spring.datasource.type:org.apache.tomcat.jdbc.pool.DataSource") - .run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - Object listener = isP6Spy(context) ? context.getBean(TraceJdbcEventListener.class) - : context.getBean(TraceQueryExecutionListener.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(listener).extracting("strategy").extracting("openConnections") - .isInstanceOfSatisfying(Map.class, map -> assertThat(map).isEmpty()); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - private boolean isP6Spy(AssertableApplicationContext context) { - if (context.getBeansOfType(TraceJdbcEventListener.class).size() == 1) { - return true; - } - else if (context.getBeansOfType(TraceQueryExecutionListener.class).size() == 1) { - return false; - } - else { - throw new IllegalStateException("Expected exactly 1 tracing listener bean in the context."); - } - } - - @Test - void testSingleConnectionAcrossMultipleThreads() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - IntStream.range(0, 5).mapToObj(i -> CompletableFuture.runAsync(() -> { - try { - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.next(); - statement.close(); - resultSet.close(); - } - catch (SQLException e) { - throw new IllegalStateException(e); - } - })).collect(Collectors.toList()).forEach(CompletableFuture::join); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(1 + 2 * 5); - assertThat(spanReporter.reportedSpans()).extracting("name").contains("select", "result-set", "connection"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldIncludeOnlyConnectionTraces() { - parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: connection").run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(1); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldIncludeOnlyQueryTraces() { - parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: query").run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(statementSpan.getName()).isEqualTo("select"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldIncludeOnlyFetchTraces() { - parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: fetch").run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(1); - FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(0); - assertThat(resultSetSpan.getName()).isEqualTo("result-set"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldIncludeOnlyConnectionAndQueryTraces() { - parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: connection, query").run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(2); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(statementSpan.getName()).isEqualTo("select"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldIncludeOnlyConnectionAndFetchTraces() { - parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: connection, fetch").run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(2); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); - FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(resultSetSpan.getName()).isEqualTo("result-set"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldIncludeOnlyQueryAndFetchTraces() { - parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: query, fetch").run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); - resultSet.next(); - resultSet.close(); - statement.close(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(2); - FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(statementSpan.getName()).isEqualTo("select"); - assertThat(resultSetSpan.getName()).isEqualTo("result-set"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotOverrideExceptionWhenConnectionWasClosedBeforeExecutingQuery() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - PreparedStatement statement = connection.prepareStatement("SELECT NOW()"); - connection.close(); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - - assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class); - - assertThat(spanReporter.reportedSpans()).hasSize(1); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotOverrideExceptionWhenStatementWasClosedBeforeExecutingQuery() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - PreparedStatement statement = connection.prepareStatement("SELECT NOW()"); - statement.close(); - assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(2); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(statementSpan.getName()).isEqualTo("select"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotOverrideExceptionWhenResultSetWasClosedBeforeNext() { - parentContextRunner().run(context -> { - DataSource dataSource = context.getBean(DataSource.class); - TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); - - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT NOW()"); - resultSet.close(); - assertThatThrownBy(resultSet::next).isInstanceOf(SQLException.class); - statement.close(); - connection.close(); - - assertThat(spanReporter.reportedSpans()).hasSize(3); - FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); - FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); - FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); - assertThat(connectionSpan.getName()).isEqualTo("connection"); - assertThat(statementSpan.getName()).isEqualTo("select"); - assertThat(resultSetSpan.getName()).isEqualTo("result-set"); - assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); - assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); - }); - } - - @Test - void testShouldNotFailWhenClosingConnectionFromDifferentDataSource() { - ApplicationContextRunner contextRunner = parentContextRunner() - .withUserConfiguration(MultiDataSourceConfiguration.class); - - contextRunner.run(context -> { - DataSource dataSource1 = context.getBean("test1", DataSource.class); - DataSource dataSource2 = context.getBean("test2", DataSource.class); - - dataSource1.getConnection().close(); - dataSource2.getConnection().close(); - - CompletableFuture future = CompletableFuture.runAsync(() -> { - try { - Connection connection1 = dataSource1.getConnection(); - PreparedStatement statement = connection1.prepareStatement("SELECT NOW()"); - ResultSet resultSet = statement.executeQuery(); - Thread.sleep(200); - resultSet.close(); - statement.close(); - connection1.close(); - } - catch (SQLException | InterruptedException e) { - throw new IllegalStateException(e); - } - }); - Thread.sleep(100); - Connection connection2 = dataSource2.getConnection(); - Thread.sleep(300); - connection2.close(); - - future.join(); - }); - } - - private static class MultiDataSourceConfiguration { - - @Bean - public HikariDataSource test1() { - HikariDataSource dataSource = new HikariDataSource(); - dataSource.setJdbcUrl("jdbc:h2:mem:testdb-1-foo"); - dataSource.setPoolName("test1"); - return dataSource; - } - - @Bean - public HikariDataSource test2() { - HikariDataSource dataSource = new HikariDataSource(); - dataSource.setJdbcUrl("jdbc:h2:mem:testdb-2-bar"); - dataSource.setPoolName("test2"); - return dataSource; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariDataSource; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +abstract class TracingListenerStrategyTests { + + public static final String SPAN_SQL_QUERY_TAG_NAME = "jdbc.query"; + + public static final String SPAN_ROW_COUNT_TAG_NAME = "jdbc.row-count"; + + abstract ApplicationContextRunner parentContextRunner(); + + protected abstract Class autoConfiguration(); + + protected abstract Class testConfiguration(); + + @Test + void testShouldAddSpanForConnection() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.commit(); + connection.rollback(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(connectionSpan.getRemoteServiceName()).isEqualTo("TESTDB-BAZ"); + assertThat(connectionSpan.getEvents()).extracting("value").contains("jdbc.commit"); + assertThat(connectionSpan.getEvents()).extracting("value").contains("jdbc.rollback"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldAddSpanForConnectionWithFixedRemoteServiceName() { + parentContextRunner() + .withPropertyValues("spring.datasource.url:jdbc:h2:mem:testdb-baz?sleuthServiceName=aaaabbbb") + .run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.commit(); + connection.rollback(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(connectionSpan.getRemoteServiceName()).isEqualTo("aaaabbbb"); + assertThat(connectionSpan.getEvents()).extracting("value").contains("jdbc.commit"); + assertThat(connectionSpan.getEvents()).extracting("value").contains("jdbc.rollback"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldAddSpanForPreparedStatementExecute() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.prepareStatement("SELECT NOW()").execute(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(statementSpan.getRemoteServiceName()).isEqualTo("TESTDB-BAZ"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldAddSpanForPreparedStatementExecuteUpdate() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.prepareStatement("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1") + .executeUpdate(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("update"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, + "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldAddSpanForStatementExecuteUpdate() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + connection.createStatement() + .executeUpdate("UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("update"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, + "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = '' WHERE 0 = 1"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldAddSpanForPreparedStatementExecuteQueryIncludingTimeToCloseResultSet() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + ResultSet resultSet = connection.prepareStatement("SELECT NOW() UNION ALL select NOW()").executeQuery(); + resultSet.next(); + resultSet.next(); + resultSet.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, + "SELECT NOW() UNION ALL select NOW()"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(resultSetSpan.getRemoteServiceName()).isEqualTo("TESTDB-BAZ"); + if (isP6Spy(context)) { + assertThat(resultSetSpan.getTags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "2"); + } + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldAddSpanForStatementAndResultSet() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + ResultSet resultSet = connection.createStatement().executeQuery("SELECT NOW()"); + resultSet.next(); + Thread.sleep(200L); + resultSet.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + if (isP6Spy(context)) { + assertThat(resultSetSpan.getTags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "1"); + } + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenStatementIsClosedWihoutResultSet() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenConnectionIsClosedWihoutResultSet() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenResultSetNextWasNotCalled() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenResourceIsAlreadyClosed() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + resultSet.close(); + resultSet.close(); + statement.close(); + statement.close(); + connection.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenResourceIsAlreadyClosed2() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + assertThatThrownBy(() -> { + connection.close(); + connection.prepareStatement("SELECT NOW()"); + }).isInstanceOf(SQLException.class); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenResourceIsAlreadyClosed3() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + assertThatThrownBy(() -> { + statement.close(); + statement.executeQuery("SELECT NOW()"); + }).isInstanceOf(SQLException.class); + connection.close(); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenResourceIsAlreadyClosed4() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + assertThatThrownBy(() -> { + resultSet.close(); + resultSet.next(); + }).isInstanceOf(SQLException.class); + statement.close(); + connection.close(); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailToCloseSpanForTwoConsecutiveConnections() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection1 = dataSource.getConnection(); + Connection connection2 = dataSource.getConnection(); + connection2.close(); + connection1.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connection1Span = spanReporter.reportedSpans().get(0); + FinishedSpan connection2Span = spanReporter.reportedSpans().get(1); + assertThat(connection1Span.getName()).isEqualTo("connection"); + assertThat(connection2Span.getName()).isEqualTo("connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenClosedInReversedOrder() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + connection.close(); + statement.close(); + resultSet.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + @SuppressWarnings("unchecked") + void testShouldNotCauseMemoryLeakOnTomcatPool() { + parentContextRunner().withPropertyValues("spring.datasource.type:org.apache.tomcat.jdbc.pool.DataSource") + .run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + Object listener = isP6Spy(context) ? context.getBean(TraceJdbcEventListener.class) + : context.getBean(TraceQueryExecutionListener.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(listener).extracting("strategy").extracting("openConnections") + .isInstanceOfSatisfying(Map.class, map -> assertThat(map).isEmpty()); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + private boolean isP6Spy(AssertableApplicationContext context) { + if (context.getBeansOfType(TraceJdbcEventListener.class).size() == 1) { + return true; + } + else if (context.getBeansOfType(TraceQueryExecutionListener.class).size() == 1) { + return false; + } + else { + throw new IllegalStateException("Expected exactly 1 tracing listener bean in the context."); + } + } + + @Test + void testSingleConnectionAcrossMultipleThreads() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + IntStream.range(0, 5).mapToObj(i -> CompletableFuture.runAsync(() -> { + try { + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.next(); + statement.close(); + resultSet.close(); + } + catch (SQLException e) { + throw new IllegalStateException(e); + } + })).collect(Collectors.toList()).forEach(CompletableFuture::join); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(1 + 2 * 5); + assertThat(spanReporter.reportedSpans()).extracting("name").contains("select", "result-set", "connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldIncludeOnlyConnectionTraces() { + parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: connection").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldIncludeOnlyQueryTraces() { + parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: query").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldIncludeOnlyFetchTraces() { + parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: fetch").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(0); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldIncludeOnlyConnectionAndQueryTraces() { + parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: connection, query").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldIncludeOnlyConnectionAndFetchTraces() { + parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: connection, fetch").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldIncludeOnlyQueryAndFetchTraces() { + parentContextRunner().withPropertyValues("spring.sleuth.jdbc.includes: query, fetch").run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1 FROM dual"); + resultSet.next(); + resultSet.close(); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotOverrideExceptionWhenConnectionWasClosedBeforeExecutingQuery() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT NOW()"); + connection.close(); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + + assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class); + + assertThat(spanReporter.reportedSpans()).hasSize(1); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotOverrideExceptionWhenStatementWasClosedBeforeExecutingQuery() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement("SELECT NOW()"); + statement.close(); + assertThatThrownBy(statement::executeQuery).isInstanceOf(SQLException.class); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotOverrideExceptionWhenResultSetWasClosedBeforeNext() { + parentContextRunner().run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + TestSpanHandler spanReporter = context.getBean(TestSpanHandler.class); + + Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT NOW()"); + resultSet.close(); + assertThatThrownBy(resultSet::next).isInstanceOf(SQLException.class); + statement.close(); + connection.close(); + + assertThat(spanReporter.reportedSpans()).hasSize(3); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(2); + FinishedSpan resultSetSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("select"); + assertThat(resultSetSpan.getName()).isEqualTo("result-set"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "SELECT NOW()"); + assertThat(context.getBean(Tracer.class).currentSpan()).isNull(); + }); + } + + @Test + void testShouldNotFailWhenClosingConnectionFromDifferentDataSource() { + ApplicationContextRunner contextRunner = parentContextRunner() + .withUserConfiguration(MultiDataSourceConfiguration.class); + + contextRunner.run(context -> { + DataSource dataSource1 = context.getBean("test1", DataSource.class); + DataSource dataSource2 = context.getBean("test2", DataSource.class); + + dataSource1.getConnection().close(); + dataSource2.getConnection().close(); + + CompletableFuture future = CompletableFuture.runAsync(() -> { + try { + Connection connection1 = dataSource1.getConnection(); + PreparedStatement statement = connection1.prepareStatement("SELECT NOW()"); + ResultSet resultSet = statement.executeQuery(); + Thread.sleep(200); + resultSet.close(); + statement.close(); + connection1.close(); + } + catch (SQLException | InterruptedException e) { + throw new IllegalStateException(e); + } + }); + Thread.sleep(100); + Connection connection2 = dataSource2.getConnection(); + Thread.sleep(300); + connection2.close(); + + future.join(); + }); + } + + private static class MultiDataSourceConfiguration { + + @Bean + public HikariDataSource test1() { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setJdbcUrl("jdbc:h2:mem:testdb-1-foo"); + dataSource.setPoolName("test1"); + return dataSource; + } + + @Bean + public HikariDataSource test2() { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setJdbcUrl("jdbc:h2:mem:testdb-2-bar"); + dataSource.setPoolName("test2"); + return dataSource; + } + + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingQueryExecutionListenerTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingQueryExecutionListenerTests.java index b54480927..6897f4293 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingQueryExecutionListenerTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingQueryExecutionListenerTests.java @@ -1,39 +1,40 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.jdbc; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.test.context.FilteredClassLoader; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.cloud.sleuth.autoconfig.instrument.jdbc.TraceJdbcAutoConfiguration; - -public abstract class TracingQueryExecutionListenerTests extends TracingListenerStrategyTests { - - @Override - ApplicationContextRunner parentContextRunner() { - return new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, - TraceJdbcAutoConfiguration.class, autoConfiguration(), PropertyPlaceholderAutoConfiguration.class)) - .withUserConfiguration(testConfiguration()) - .withPropertyValues("spring.datasource.initialization-mode=never", - "spring.datasource.url:jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") - .withClassLoader(new FilteredClassLoader("com.p6spy")); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.jdbc; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.instrument.jdbc.TraceJdbcAutoConfiguration; + +public abstract class TracingQueryExecutionListenerTests extends TracingListenerStrategyTests { + + @Override + ApplicationContextRunner parentContextRunner() { + return new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(DataSourceAutoConfiguration.class, TraceJdbcAutoConfiguration.class, + autoConfiguration(), PropertyPlaceholderAutoConfiguration.class)) + .withUserConfiguration(testConfiguration()) + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.datasource.url:jdbc:h2:mem:testdb-baz", "spring.datasource.hikari.pool-name=test") + .withClassLoader(new FilteredClassLoader("com.p6spy")); + } + +} From d15ee050a6ea4a5865e0bf849c98c58605c45680 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 27 May 2021 14:08:12 +0200 Subject: [PATCH 41/59] Updated config props --- docs/src/main/asciidoc/_configprops.adoc | 225 ++++++++++++----------- 1 file changed, 113 insertions(+), 112 deletions(-) diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 26042bb09..d560507f6 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -1,112 +1,113 @@ -|=== -|Name | Default | Description - -|spring.sleuth.async.configurer.enabled | `true` | Enable default AsyncConfigurer. -|spring.sleuth.async.enabled | `true` | Enable instrumenting async related components so that the tracing information is passed between threads. -|spring.sleuth.async.ignored-beans | | List of {@link java.util.concurrent.Executor} bean names that should be ignored and not wrapped in a trace representation. -|spring.sleuth.baggage.correlation-enabled | `true` | Enables correlating the baggage context with logging contexts. -|spring.sleuth.baggage.correlation-fields | | List of fields that should be propagated over the wire. -|spring.sleuth.baggage.local-fields | | List of fields that should be accessible within the JVM process but not propagated over the wire. -|spring.sleuth.baggage.remote-fields | | List of fields that are referenced the same in-process as it is on the wire. For example, the field "x-vcap-request-id" would be set as-is including the prefix. -|spring.sleuth.baggage.tag-fields | | List of fields that should automatically become tags. -|spring.sleuth.batch.enabled | `true` | Enable Spring Batch instrumentation. -|spring.sleuth.circuitbreaker.enabled | `true` | Enable Spring Cloud CircuitBreaker instrumentation. -|spring.sleuth.config.server.enabled | `true` | Enable Spring Cloud Config Server instrumentation. -|spring.sleuth.deployer.enabled | `true` | Enable Spring Cloud Deployer instrumentation. -|spring.sleuth.deployer.status-poll-delay | `500` | Default poll delay to retrieve the deployed application status. -|spring.sleuth.enabled | `true` | -|spring.sleuth.feign.enabled | `true` | Enable span information propagation when using Feign. -|spring.sleuth.feign.processor.enabled | `true` | Enable post processor that wraps Feign Context in its tracing representations. -|spring.sleuth.function.enabled | `true` | Enable instrumenting of Spring Cloud Function and Spring Cloud Function based projects (e.g. Spring Cloud Stream). -|spring.sleuth.grpc.enabled | `true` | Enable span information propagation when using GRPC. -|spring.sleuth.http.enabled | `true` | Enables HTTP support. -|spring.sleuth.integration.enabled | `true` | Enable Spring Integration instrumentation. -|spring.sleuth.integration.patterns | `[!hystrixStreamOutput*, *, !channel*]` | An array of patterns against which channel names will be matched. @see org.springframework.integration.config.GlobalChannelInterceptor#patterns() Defaults to any channel name not matching the Hystrix Stream and functional Stream channel names. -|spring.sleuth.integration.websockets.enabled | `true` | Enable tracing for WebSockets. -|spring.sleuth.jdbc.datasource-proxy.count-query | `false` | Creates listener to count queries. @see ProxyDataSourceBuilder#countQuery() @see QueryCountHolder -|spring.sleuth.jdbc.datasource-proxy.json-format | `false` | Use json output for logging query. @see ProxyDataSourceBuilder#asJson() -|spring.sleuth.jdbc.datasource-proxy.logging | | Logging to use for logging queries. -|spring.sleuth.jdbc.datasource-proxy.multiline | `true` | Use multiline output for logging query. @see ProxyDataSourceBuilder#multiline() -|spring.sleuth.jdbc.datasource-proxy.query.enable-logging | `true` | Enable logging all queries to the log. -|spring.sleuth.jdbc.datasource-proxy.query.log-level | `DEBUG` | Severity of query logger. -|spring.sleuth.jdbc.datasource-proxy.query.logger-name | | Name of query logger. -|spring.sleuth.jdbc.datasource-proxy.slow-query.log-level | `WARN` | Severity of slow query logger. -|spring.sleuth.jdbc.datasource-proxy.slow-query.logger-name | | Name of slow query logger. -|spring.sleuth.jdbc.datasource-proxy.slow-query.threshold | `300` | Number of seconds to consider query as slow. -|spring.sleuth.jdbc.enabled | `true` | Enables data source decorating. -|spring.sleuth.jdbc.excluded-beans | | Beans that won't be decorated. -|spring.sleuth.jdbc.includes | | Which types of tracing we would like to include. -|spring.sleuth.jdbc.p6spy.custom-appender-class | | Class file to use (only with logging=custom). The class must implement {@link com.p6spy.engine.spy.appender.FormattedLogger}. -|spring.sleuth.jdbc.p6spy.enable-logging | `true` | Enables logging JDBC events. -|spring.sleuth.jdbc.p6spy.log-file | `spy.log` | Name of log file to use (only with logging=file). -|spring.sleuth.jdbc.p6spy.log-filter.pattern | | Use regex pattern to filter log messages. Only matched messages will be logged. -|spring.sleuth.jdbc.p6spy.log-format | | Custom log format. -|spring.sleuth.jdbc.p6spy.logging | | Logging to use for logging queries. -|spring.sleuth.jdbc.p6spy.multiline | `true` | Enables multiline output. -|spring.sleuth.jdbc.p6spy.tracing.include-parameter-values | `true` | Report the effective sql string (with '?' replaced with real values) to tracing systems.

NOTE this setting does not affect the logging message. -|spring.sleuth.kafka.enabled | `true` | Enable instrumenting of Apache Kafka clients. -|spring.sleuth.messaging.aspect.enabled | `false` | Should {@link MessageMapping} wrapping be enabled. -|spring.sleuth.messaging.enabled | `false` | Should messaging be turned on. -|spring.sleuth.messaging.jms.enabled | `true` | Enable tracing of JMS. -|spring.sleuth.messaging.jms.remote-service-name | `jms` | JMS remote service name. -|spring.sleuth.messaging.kafka.enabled | `true` | Enable tracing of Kafka. -|spring.sleuth.messaging.kafka.mapper.enabled | `true` | Enable DefaultKafkaHeaderMapper tracing for Kafka. -|spring.sleuth.messaging.kafka.remote-service-name | `kafka` | Kafka remote service name. -|spring.sleuth.messaging.kafka.streams.enabled | `false` | Should Kafka Streams be turned on. -|spring.sleuth.messaging.rabbit.enabled | `true` | Enable tracing of RabbitMQ. -|spring.sleuth.messaging.rabbit.remote-service-name | `rabbitmq` | Rabbit remote service name. -|spring.sleuth.mongodb.enabled | `true` | Enable tracing for MongoDb. -|spring.sleuth.opentracing.enabled | `true` | Enables OpenTracing support. -|spring.sleuth.propagation.type | | Tracing context propagation types. -|spring.sleuth.quartz.enabled | `true` | Enable tracing for Quartz. -|spring.sleuth.r2dbc.enabled | `true` | Enable R2dbc instrumentation. -|spring.sleuth.reactor.decorate-on-each | `true` | When true decorates on each operator, will be less performing, but logging will always contain the tracing entries in each operator. When false decorates on last operator, will be more performing, but logging might not always contain the tracing entries. @deprecated use explicit value via {@link SleuthReactorProperties#instrumentationType} -|spring.sleuth.reactor.enabled | `true` | When true enables instrumentation for reactor. -|spring.sleuth.reactor.instrumentation-type | | -|spring.sleuth.redis.enabled | `true` | Enable span information propagation when using Redis. -|spring.sleuth.redis.remote-service-name | `redis` | Service name for the remote Redis endpoint. -|spring.sleuth.rpc.enabled | `true` | Enable tracing of RPC. -|spring.sleuth.rsocket.enabled | `true` | When true enables instrumentation for rsocket. -|spring.sleuth.rxjava.schedulers.hook.enabled | `true` | Enable support for RxJava via RxJavaSchedulersHook. -|spring.sleuth.rxjava.schedulers.ignoredthreads | `[HystrixMetricPoller, ^RxComputation.*$]` | Thread names for which spans will not be sampled. -|spring.sleuth.sampler.probability | | Probability of requests that should be sampled. E.g. 1.0 - 100% requests should be sampled. The precision is whole-numbers only (i.e. there's no support for 0.1% of the traces). -|spring.sleuth.sampler.rate | `10` | A rate per second can be a nice choice for low-traffic endpoints as it allows you surge protection. For example, you may never expect the endpoint to get more than 50 requests per second. If there was a sudden surge of traffic, to 5000 requests per second, you would still end up with 50 traces per second. Conversely, if you had a percentage, like 10%, the same surge would end up with 500 traces per second, possibly overloading your storage. Amazon X-Ray includes a rate-limited sampler (named Reservoir) for this purpose. Brave has taken the same approach via the {@link brave.sampler.RateLimitingSampler}. -|spring.sleuth.sampler.refresh.enabled | `true` | Enable refresh scope for sampler. -|spring.sleuth.scheduled.enabled | `true` | Enable tracing for {@link org.springframework.scheduling.annotation.Scheduled}. -|spring.sleuth.scheduled.skip-pattern | | Pattern for the fully qualified name of a class that should be skipped. -|spring.sleuth.span-filter.additional-span-name-patterns-to-ignore | | Additional list of span names to ignore. Will be appended to {@link #spanNamePatternsToSkip}. -|spring.sleuth.span-filter.enabled | `false` | Will turn on the default Sleuth handler mechanism. Might ignore exporting of certain spans; -|spring.sleuth.span-filter.span-name-patterns-to-skip | `^catalogWatchTaskScheduler$` | List of span names to ignore. They will not be sent to external systems. -|spring.sleuth.supports-join | `true` | True means the tracing system supports sharing a span ID between a client and server. -|spring.sleuth.task.enabled | `true` | Enable Spring Cloud Task instrumentation. -|spring.sleuth.trace-id128 | `false` | When true, generate 128-bit trace IDs instead of 64-bit ones. -|spring.sleuth.tracer.mode | | Set which tracer implementation should be picked. -|spring.sleuth.tx.enabled | `true` | Enable Spring TX instrumentation. -|spring.sleuth.vault.enabled | `true` | Enable Spring Vault instrumentation. -|spring.sleuth.web.additional-skip-pattern | | Additional pattern for URLs that should be skipped in tracing. This will be appended to the {@link SleuthWebProperties#skipPattern}. -|spring.sleuth.web.client.enabled | `true` | Enable interceptor injecting into {@link org.springframework.web.client.RestTemplate}. -|spring.sleuth.web.client.skip-pattern | | Pattern for URLs that should be skipped in client side tracing. -|spring.sleuth.web.enabled | `true` | When true enables instrumentation for web applications. -|spring.sleuth.web.filter-order | `0` | Order in which the tracing filters should be registered. -|spring.sleuth.web.ignore-auto-configured-skip-patterns | `false` | If set to true, auto-configured skip patterns will be ignored. -|spring.sleuth.web.servlet.enabled | `true` | Enable servlet instrumentation. -|spring.sleuth.web.skip-pattern | `/api-docs.*\|/swagger.*\|.*\.png\|.*\.css\|.*\.js\|.*\.html\|/favicon.ico\|/hystrix.stream` | Pattern for URLs that should be skipped in tracing. -|spring.sleuth.web.tomcat.enabled | `true` | Enable tracing instrumentation for Tomcat. -|spring.sleuth.web.webclient.enabled | `true` | Enable tracing instrumentation for WebClient. -|spring.zipkin.activemq.message-max-bytes | `100000` | Maximum number of bytes for a given message with spans sent to Zipkin over ActiveMQ. -|spring.zipkin.activemq.queue | `zipkin` | Name of the ActiveMQ queue where spans should be sent to Zipkin. -|spring.zipkin.api-path | | The API path to append to baseUrl (above) as suffix. This applies if you use other monitoring tools, such as New Relic. The trace API doesn't need the API path, so you can set it to blank ("") in the configuration. -|spring.zipkin.base-url | `http://localhost:9411/` | URL of the zipkin query server instance. You can also provide the service id of the Zipkin server if Zipkin's registered in service discovery (e.g. https://zipkinserver/). -|spring.zipkin.compression.enabled | `false` | -|spring.zipkin.discovery-client-enabled | | If set to {@code false}, will treat the {@link ZipkinProperties#baseUrl} as a URL always. -|spring.zipkin.enabled | `true` | Enables sending spans to Zipkin. -|spring.zipkin.encoder | | Encoding type of spans sent to Zipkin. Set to {@link SpanBytesEncoder#JSON_V1} if your server is not recent. -|spring.zipkin.kafka.topic | `zipkin` | Name of the Kafka topic where spans should be sent to Zipkin. -|spring.zipkin.locator.discovery.enabled | `false` | Enabling of locating the host name via service discovery. -|spring.zipkin.message-timeout | `1` | Timeout in seconds before pending spans will be sent in batches to Zipkin. -|spring.zipkin.rabbitmq.addresses | | Addresses of the RabbitMQ brokers used to send spans to Zipkin -|spring.zipkin.rabbitmq.queue | `zipkin` | Name of the RabbitMQ queue where spans should be sent to Zipkin. -|spring.zipkin.sender.type | | Means of sending spans to Zipkin. -|spring.zipkin.service.name | | The name of the service, from which the Span was sent via HTTP, that should appear in Zipkin. - -|=== +|=== +|Name | Default | Description + +|spring.sleuth.async.configurer.enabled | `true` | Enable default AsyncConfigurer. +|spring.sleuth.async.enabled | `true` | Enable instrumenting async related components so that the tracing information is passed between threads. +|spring.sleuth.async.ignored-beans | | List of {@link java.util.concurrent.Executor} bean names that should be ignored and not wrapped in a trace representation. +|spring.sleuth.baggage.correlation-enabled | `true` | Enables correlating the baggage context with logging contexts. +|spring.sleuth.baggage.correlation-fields | | List of fields that should be propagated over the wire. +|spring.sleuth.baggage.local-fields | | List of fields that should be accessible within the JVM process but not propagated over the wire. +|spring.sleuth.baggage.remote-fields | | List of fields that are referenced the same in-process as it is on the wire. For example, the field "x-vcap-request-id" would be set as-is including the prefix. +|spring.sleuth.baggage.tag-fields | | List of fields that should automatically become tags. +|spring.sleuth.batch.enabled | `true` | Enable Spring Batch instrumentation. +|spring.sleuth.circuitbreaker.enabled | `true` | Enable Spring Cloud CircuitBreaker instrumentation. +|spring.sleuth.config.server.enabled | `true` | Enable Spring Cloud Config Server instrumentation. +|spring.sleuth.deployer.enabled | `true` | Enable Spring Cloud Deployer instrumentation. +|spring.sleuth.deployer.status-poll-delay | `500` | Default poll delay to retrieve the deployed application status. +|spring.sleuth.enabled | `true` | +|spring.sleuth.feign.enabled | `true` | Enable span information propagation when using Feign. +|spring.sleuth.feign.processor.enabled | `true` | Enable post processor that wraps Feign Context in its tracing representations. +|spring.sleuth.function.enabled | `true` | Enable instrumenting of Spring Cloud Function and Spring Cloud Function based projects (e.g. Spring Cloud Stream). +|spring.sleuth.grpc.enabled | `true` | Enable span information propagation when using GRPC. +|spring.sleuth.http.enabled | `true` | Enables HTTP support. +|spring.sleuth.integration.enabled | `true` | Enable Spring Integration instrumentation. +|spring.sleuth.integration.patterns | `[!hystrixStreamOutput*, *, !channel*]` | An array of patterns against which channel names will be matched. @see org.springframework.integration.config.GlobalChannelInterceptor#patterns() Defaults to any channel name not matching the Hystrix Stream and functional Stream channel names. +|spring.sleuth.integration.websockets.enabled | `true` | Enable tracing for WebSockets. +|spring.sleuth.jdbc.datasource-proxy.enabled | `true` | Should the datasource-proxy tracing be enabled? +|spring.sleuth.jdbc.datasource-proxy.json-format | `false` | Use json output for logging query. @see ProxyDataSourceBuilder#asJson() +|spring.sleuth.jdbc.datasource-proxy.logging | | Logging to use for logging queries. +|spring.sleuth.jdbc.datasource-proxy.multiline | `true` | Use multiline output for logging query. @see ProxyDataSourceBuilder#multiline() +|spring.sleuth.jdbc.datasource-proxy.query.enable-logging | `true` | Enable logging all queries to the log. +|spring.sleuth.jdbc.datasource-proxy.query.log-level | `DEBUG` | Severity of query logger. +|spring.sleuth.jdbc.datasource-proxy.query.logger-name | | Name of query logger. +|spring.sleuth.jdbc.datasource-proxy.slow-query.log-level | `WARN` | Severity of slow query logger. +|spring.sleuth.jdbc.datasource-proxy.slow-query.logger-name | | Name of slow query logger. +|spring.sleuth.jdbc.datasource-proxy.slow-query.threshold | `300` | Number of seconds to consider query as slow. +|spring.sleuth.jdbc.enabled | `true` | Enables JDBC instrumentation. +|spring.sleuth.jdbc.excluded-data-source-bean-names | | List of DataSource bean names that will not be decorated. +|spring.sleuth.jdbc.includes | | Which types of tracing we would like to include. +|spring.sleuth.jdbc.p6spy.custom-appender-class | | Class file to use (only with logging=custom). The class must implement {@link com.p6spy.engine.spy.appender.FormattedLogger}. +|spring.sleuth.jdbc.p6spy.enable-logging | `true` | Enables logging JDBC events. +|spring.sleuth.jdbc.p6spy.enabled | `true` | Should the p6spy tracing be enabled? +|spring.sleuth.jdbc.p6spy.log-file | `spy.log` | Name of log file to use (only with logging=file). +|spring.sleuth.jdbc.p6spy.log-filter.pattern | | Use regex pattern to filter log messages. Only matched messages will be logged. +|spring.sleuth.jdbc.p6spy.log-format | | Custom log format. +|spring.sleuth.jdbc.p6spy.logging | | Logging to use for logging queries. +|spring.sleuth.jdbc.p6spy.multiline | `true` | Enables multiline output. +|spring.sleuth.jdbc.p6spy.tracing.include-parameter-values | `true` | Report the effective sql string (with '?' replaced with real values) to tracing systems.

NOTE this setting does not affect the logging message. +|spring.sleuth.kafka.enabled | `true` | Enable instrumenting of Apache Kafka clients. +|spring.sleuth.messaging.aspect.enabled | `false` | Should {@link MessageMapping} wrapping be enabled. +|spring.sleuth.messaging.enabled | `false` | Should messaging be turned on. +|spring.sleuth.messaging.jms.enabled | `true` | Enable tracing of JMS. +|spring.sleuth.messaging.jms.remote-service-name | `jms` | JMS remote service name. +|spring.sleuth.messaging.kafka.enabled | `true` | Enable tracing of Kafka. +|spring.sleuth.messaging.kafka.mapper.enabled | `true` | Enable DefaultKafkaHeaderMapper tracing for Kafka. +|spring.sleuth.messaging.kafka.remote-service-name | `kafka` | Kafka remote service name. +|spring.sleuth.messaging.kafka.streams.enabled | `false` | Should Kafka Streams be turned on. +|spring.sleuth.messaging.rabbit.enabled | `true` | Enable tracing of RabbitMQ. +|spring.sleuth.messaging.rabbit.remote-service-name | `rabbitmq` | Rabbit remote service name. +|spring.sleuth.mongodb.enabled | `true` | Enable tracing for MongoDb. +|spring.sleuth.opentracing.enabled | `true` | Enables OpenTracing support. +|spring.sleuth.propagation.type | | Tracing context propagation types. +|spring.sleuth.quartz.enabled | `true` | Enable tracing for Quartz. +|spring.sleuth.r2dbc.enabled | `true` | Enable R2dbc instrumentation. +|spring.sleuth.reactor.decorate-on-each | `true` | When true decorates on each operator, will be less performing, but logging will always contain the tracing entries in each operator. When false decorates on last operator, will be more performing, but logging might not always contain the tracing entries. @deprecated use explicit value via {@link SleuthReactorProperties#instrumentationType} +|spring.sleuth.reactor.enabled | `true` | When true enables instrumentation for reactor. +|spring.sleuth.reactor.instrumentation-type | | +|spring.sleuth.redis.enabled | `true` | Enable span information propagation when using Redis. +|spring.sleuth.redis.remote-service-name | `redis` | Service name for the remote Redis endpoint. +|spring.sleuth.rpc.enabled | `true` | Enable tracing of RPC. +|spring.sleuth.rsocket.enabled | `true` | When true enables instrumentation for rsocket. +|spring.sleuth.rxjava.schedulers.hook.enabled | `true` | Enable support for RxJava via RxJavaSchedulersHook. +|spring.sleuth.rxjava.schedulers.ignoredthreads | `[HystrixMetricPoller, ^RxComputation.*$]` | Thread names for which spans will not be sampled. +|spring.sleuth.sampler.probability | | Probability of requests that should be sampled. E.g. 1.0 - 100% requests should be sampled. The precision is whole-numbers only (i.e. there's no support for 0.1% of the traces). +|spring.sleuth.sampler.rate | `10` | A rate per second can be a nice choice for low-traffic endpoints as it allows you surge protection. For example, you may never expect the endpoint to get more than 50 requests per second. If there was a sudden surge of traffic, to 5000 requests per second, you would still end up with 50 traces per second. Conversely, if you had a percentage, like 10%, the same surge would end up with 500 traces per second, possibly overloading your storage. Amazon X-Ray includes a rate-limited sampler (named Reservoir) for this purpose. Brave has taken the same approach via the {@link brave.sampler.RateLimitingSampler}. +|spring.sleuth.sampler.refresh.enabled | `true` | Enable refresh scope for sampler. +|spring.sleuth.scheduled.enabled | `true` | Enable tracing for {@link org.springframework.scheduling.annotation.Scheduled}. +|spring.sleuth.scheduled.skip-pattern | | Pattern for the fully qualified name of a class that should be skipped. +|spring.sleuth.span-filter.additional-span-name-patterns-to-ignore | | Additional list of span names to ignore. Will be appended to {@link #spanNamePatternsToSkip}. +|spring.sleuth.span-filter.enabled | `false` | Will turn on the default Sleuth handler mechanism. Might ignore exporting of certain spans; +|spring.sleuth.span-filter.span-name-patterns-to-skip | `^catalogWatchTaskScheduler$` | List of span names to ignore. They will not be sent to external systems. +|spring.sleuth.supports-join | `true` | True means the tracing system supports sharing a span ID between a client and server. +|spring.sleuth.task.enabled | `true` | Enable Spring Cloud Task instrumentation. +|spring.sleuth.trace-id128 | `false` | When true, generate 128-bit trace IDs instead of 64-bit ones. +|spring.sleuth.tracer.mode | | Set which tracer implementation should be picked. +|spring.sleuth.tx.enabled | `true` | Enable Spring TX instrumentation. +|spring.sleuth.vault.enabled | `true` | Enable Spring Vault instrumentation. +|spring.sleuth.web.additional-skip-pattern | | Additional pattern for URLs that should be skipped in tracing. This will be appended to the {@link SleuthWebProperties#skipPattern}. +|spring.sleuth.web.client.enabled | `true` | Enable interceptor injecting into {@link org.springframework.web.client.RestTemplate}. +|spring.sleuth.web.client.skip-pattern | | Pattern for URLs that should be skipped in client side tracing. +|spring.sleuth.web.enabled | `true` | When true enables instrumentation for web applications. +|spring.sleuth.web.filter-order | `0` | Order in which the tracing filters should be registered. +|spring.sleuth.web.ignore-auto-configured-skip-patterns | `false` | If set to true, auto-configured skip patterns will be ignored. +|spring.sleuth.web.servlet.enabled | `true` | Enable servlet instrumentation. +|spring.sleuth.web.skip-pattern | `/api-docs.*\|/swagger.*\|.*\.png\|.*\.css\|.*\.js\|.*\.html\|/favicon.ico\|/hystrix.stream` | Pattern for URLs that should be skipped in tracing. +|spring.sleuth.web.tomcat.enabled | `true` | Enable tracing instrumentation for Tomcat. +|spring.sleuth.web.webclient.enabled | `true` | Enable tracing instrumentation for WebClient. +|spring.zipkin.activemq.message-max-bytes | `100000` | Maximum number of bytes for a given message with spans sent to Zipkin over ActiveMQ. +|spring.zipkin.activemq.queue | `zipkin` | Name of the ActiveMQ queue where spans should be sent to Zipkin. +|spring.zipkin.api-path | | The API path to append to baseUrl (above) as suffix. This applies if you use other monitoring tools, such as New Relic. The trace API doesn't need the API path, so you can set it to blank ("") in the configuration. +|spring.zipkin.base-url | `http://localhost:9411/` | URL of the zipkin query server instance. You can also provide the service id of the Zipkin server if Zipkin's registered in service discovery (e.g. https://zipkinserver/). +|spring.zipkin.compression.enabled | `false` | +|spring.zipkin.discovery-client-enabled | | If set to {@code false}, will treat the {@link ZipkinProperties#baseUrl} as a URL always. +|spring.zipkin.enabled | `true` | Enables sending spans to Zipkin. +|spring.zipkin.encoder | | Encoding type of spans sent to Zipkin. Set to {@link SpanBytesEncoder#JSON_V1} if your server is not recent. +|spring.zipkin.kafka.topic | `zipkin` | Name of the Kafka topic where spans should be sent to Zipkin. +|spring.zipkin.locator.discovery.enabled | `false` | Enabling of locating the host name via service discovery. +|spring.zipkin.message-timeout | `1` | Timeout in seconds before pending spans will be sent in batches to Zipkin. +|spring.zipkin.rabbitmq.addresses | | Addresses of the RabbitMQ brokers used to send spans to Zipkin +|spring.zipkin.rabbitmq.queue | `zipkin` | Name of the RabbitMQ queue where spans should be sent to Zipkin. +|spring.zipkin.sender.type | | Means of sending spans to Zipkin. +|spring.zipkin.service.name | | The name of the service, from which the Span was sent via HTTP, that should appear in Zipkin. + +|=== \ No newline at end of file From e12f70d4f638682c2ef4e522ec82d4a493a9319c Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 27 May 2021 14:41:11 +0200 Subject: [PATCH 42/59] Made tests less brittle --- .../web/TraceAsyncIntegrationTests.java | 477 +++++++++--------- 1 file changed, 239 insertions(+), 238 deletions(-) diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java index e24da75ef..84cc39f15 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java @@ -1,238 +1,239 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.brave.instrument.web; - -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import brave.Span; -import brave.Tracer; -import brave.handler.MutableSpan; -import brave.handler.SpanHandler; -import brave.sampler.Sampler; -import brave.test.TestSpanHandler; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.sleuth.SpanName; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.EnableAsync; - -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.assertj.core.api.BDDAssertions.then; - -@SpringBootTest(classes = { TraceAsyncIntegrationTests.TraceAsyncITestConfiguration.class }) -public class TraceAsyncIntegrationTests { - - @Autowired - ClassPerformingAsyncLogic classPerformingAsyncLogic; - - @Autowired - Tracer tracer; - - @Autowired - TestSpanHandler spans; - - @BeforeEach - public void cleanup() { - this.spans.clear(); - this.classPerformingAsyncLogic.clear(); - } - - @Test - public void should_set_span_on_an_async_annotated_method() { - whenAsyncProcessingTakesPlace(); - - thenANewAsyncSpanGetsCreated(); - } - - @Test - public void should_set_span_with_custom_method_on_an_async_annotated_method() { - whenAsyncProcessingTakesPlaceWithCustomSpanName(); - - thenAsyncSpanHasCustomName(); - } - - @Test - public void should_continue_a_span_on_an_async_annotated_method() { - Span span = givenASpanInCurrentThread(); - - try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { - whenAsyncProcessingTakesPlace(); - } - finally { - span.finish(); - } - - thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(span); - } - - @Test - public void should_continue_a_span_with_custom_method_on_an_async_annotated_method() { - Span span = givenASpanInCurrentThread(); - - try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { - whenAsyncProcessingTakesPlaceWithCustomSpanName(); - } - finally { - span.finish(); - } - - thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(span); - } - - private Span givenASpanInCurrentThread() { - return this.tracer.nextSpan().name("http:existing"); - } - - private void whenAsyncProcessingTakesPlace() { - this.classPerformingAsyncLogic.invokeAsynchronousLogic(); - } - - private void whenAsyncProcessingTakesPlaceWithCustomSpanName() { - this.classPerformingAsyncLogic.customNameInvokeAsynchronousLogic(); - } - - private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(final Span span) { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) - .isEqualTo(span.context().traceId()); - List webSpans = this.spans.spans().stream() - .filter(mutableSpan -> mutableSpan.traceId().equalsIgnoreCase(span.context().traceIdString())) - .collect(Collectors.toList()); - then(webSpans).hasSize(2); - // HTTP - then(webSpans.get(0).name()).isEqualTo("http:existing"); - // ASYNC - then(webSpans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "invokeAsynchronousLogic"); - }); - } - - private void thenANewAsyncSpanGetsCreated() { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(this.spans).hasSize(1); - MutableSpan storedSpan = this.spans.get(0); - then(storedSpan.name()).isEqualTo("invoke-asynchronous-logic"); - then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "invokeAsynchronousLogic"); - }); - } - - private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(final Span span) { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan()).isNotNull(); - then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) - .isEqualTo(span.context().traceId()); - then(this.spans).hasSize(2); - // HTTP - then(this.spans.get(0).name()).isEqualTo("http:existing"); - // ASYNC - then(this.spans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "customNameInvokeAsynchronousLogic"); - }); - } - - private void thenAsyncSpanHasCustomName() { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(this.spans).hasSize(1); - MutableSpan storedSpan = this.spans.get(0); - then(storedSpan.name()).isEqualTo("foo"); - then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "customNameInvokeAsynchronousLogic"); - }); - } - - @AfterEach - public void cleanTrace() { - this.spans.clear(); - } - - @EnableAutoConfiguration - @EnableAsync - @Configuration(proxyBeanMethods = false) - static class TraceAsyncITestConfiguration { - - @Bean - ClassPerformingAsyncLogic asyncClass(Tracer tracer) { - return new ClassPerformingAsyncLogic(tracer); - } - - @Bean - Sampler defaultSampler() { - return Sampler.ALWAYS_SAMPLE; - } - - @Bean - SpanHandler testSpanHandler() { - return new TestSpanHandler(); - } - - @Bean - Executor fooExecutor() { - return new SimpleAsyncTaskExecutor(); - } - - @Bean - Executor barExecutor() { - return new SimpleAsyncTaskExecutor(); - } - - } - - static class ClassPerformingAsyncLogic { - - private final Tracer tracer; - - AtomicReference span = new AtomicReference<>(); - - ClassPerformingAsyncLogic(Tracer tracer) { - this.tracer = tracer; - } - - @Async("fooExecutor") - public void invokeAsynchronousLogic() { - this.span.set(this.tracer.currentSpan()); - } - - @Async - @SpanName("foo") - public void customNameInvokeAsynchronousLogic() { - this.span.set(this.tracer.currentSpan()); - } - - public Span getSpan() { - return this.span.get(); - } - - public void clear() { - this.span.set(null); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.web; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import brave.Span; +import brave.Tracer; +import brave.handler.MutableSpan; +import brave.handler.SpanHandler; +import brave.sampler.Sampler; +import brave.test.TestSpanHandler; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.SpanName; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.BDDAssertions.then; + +@SpringBootTest(classes = { TraceAsyncIntegrationTests.TraceAsyncITestConfiguration.class }) +public class TraceAsyncIntegrationTests { + + @Autowired + ClassPerformingAsyncLogic classPerformingAsyncLogic; + + @Autowired + Tracer tracer; + + @Autowired + TestSpanHandler spans; + + @BeforeEach + public void cleanup() { + this.spans.clear(); + this.classPerformingAsyncLogic.clear(); + } + + @Test + public void should_set_span_on_an_async_annotated_method() { + whenAsyncProcessingTakesPlace(); + + thenANewAsyncSpanGetsCreated(); + } + + @Test + public void should_set_span_with_custom_method_on_an_async_annotated_method() { + whenAsyncProcessingTakesPlaceWithCustomSpanName(); + + thenAsyncSpanHasCustomName(); + } + + @Test + public void should_continue_a_span_on_an_async_annotated_method() { + Span span = givenASpanInCurrentThread(); + + try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { + whenAsyncProcessingTakesPlace(); + } + finally { + span.finish(); + } + + thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(span); + } + + @Test + public void should_continue_a_span_with_custom_method_on_an_async_annotated_method() { + Span span = givenASpanInCurrentThread(); + + try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { + whenAsyncProcessingTakesPlaceWithCustomSpanName(); + } + finally { + span.finish(); + } + + thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(span); + } + + private Span givenASpanInCurrentThread() { + return this.tracer.nextSpan().name("http:existing"); + } + + private void whenAsyncProcessingTakesPlace() { + this.classPerformingAsyncLogic.invokeAsynchronousLogic(); + } + + private void whenAsyncProcessingTakesPlaceWithCustomSpanName() { + this.classPerformingAsyncLogic.customNameInvokeAsynchronousLogic(); + } + + private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(final Span span) { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) + .isEqualTo(span.context().traceId()); + List webSpans = this.spans.spans().stream() + .filter(mutableSpan -> mutableSpan.traceId().equalsIgnoreCase(span.context().traceIdString())) + .collect(Collectors.toList()); + then(webSpans).hasSize(2); + // HTTP + then(webSpans.get(0).name()).isEqualTo("http:existing"); + // ASYNC + then(webSpans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "invokeAsynchronousLogic"); + }); + } + + private void thenANewAsyncSpanGetsCreated() { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + List spans = this.spans.spans().stream().filter(mutableSpan -> mutableSpan.name().equals("invoke-asynchronous-logic")).collect(Collectors.toList()); + then(spans).hasSize(1); + MutableSpan storedSpan = spans.get(0); + then(storedSpan.name()).isEqualTo("invoke-asynchronous-logic"); + then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "invokeAsynchronousLogic"); + }); + } + + private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(final Span span) { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan()).isNotNull(); + then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) + .isEqualTo(span.context().traceId()); + then(this.spans).hasSize(2); + // HTTP + then(this.spans.get(0).name()).isEqualTo("http:existing"); + // ASYNC + then(this.spans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "customNameInvokeAsynchronousLogic"); + }); + } + + private void thenAsyncSpanHasCustomName() { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(this.spans).hasSize(1); + MutableSpan storedSpan = this.spans.get(0); + then(storedSpan.name()).isEqualTo("foo"); + then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "customNameInvokeAsynchronousLogic"); + }); + } + + @AfterEach + public void cleanTrace() { + this.spans.clear(); + } + + @EnableAutoConfiguration + @EnableAsync + @Configuration(proxyBeanMethods = false) + static class TraceAsyncITestConfiguration { + + @Bean + ClassPerformingAsyncLogic asyncClass(Tracer tracer) { + return new ClassPerformingAsyncLogic(tracer); + } + + @Bean + Sampler defaultSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + SpanHandler testSpanHandler() { + return new TestSpanHandler(); + } + + @Bean + Executor fooExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + @Bean + Executor barExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + } + + static class ClassPerformingAsyncLogic { + + private final Tracer tracer; + + AtomicReference span = new AtomicReference<>(); + + ClassPerformingAsyncLogic(Tracer tracer) { + this.tracer = tracer; + } + + @Async("fooExecutor") + public void invokeAsynchronousLogic() { + this.span.set(this.tracer.currentSpan()); + } + + @Async + @SpanName("foo") + public void customNameInvokeAsynchronousLogic() { + this.span.set(this.tracer.currentSpan()); + } + + public Span getSpan() { + return this.span.get(); + } + + public void clear() { + this.span.set(null); + } + + } + +} From b255974c00dc1a39f6c5dc9553ab04ec54fb8f38 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 27 May 2021 15:38:23 +0200 Subject: [PATCH 43/59] Fixed autoconfiguration ordering --- .../jdbc/TraceJdbcAutoConfiguration.java | 3 ++- .../jdbc/TracingJdbcEventListenerTests.java | 18 +++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcAutoConfiguration.java index 80233866e..cb982be83 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcAutoConfiguration.java @@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceDecorator; import org.springframework.cloud.sleuth.instrument.jdbc.DataSourceNameResolver; import org.springframework.cloud.sleuth.instrument.jdbc.TraceHikariListenerStrategySpanCustomizer; @@ -44,7 +45,7 @@ import org.springframework.context.annotation.Import; @EnableConfigurationProperties(TraceJdbcProperties.class) @ConditionalOnProperty(name = "spring.sleuth.jdbc.enabled", havingValue = "true", matchIfMissing = true) @ConditionalOnBean({ DataSource.class, Tracer.class }) -@AutoConfigureAfter(DataSourceAutoConfiguration.class) +@AutoConfigureAfter({ DataSourceAutoConfiguration.class, BraveAutoConfiguration.class }) @Import({ P6SpyConfiguration.class, DataSourceProxyConfiguration.class }) public class TraceJdbcAutoConfiguration { diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingJdbcEventListenerTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingJdbcEventListenerTests.java index 776dd5aa1..2cdc6f977 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingJdbcEventListenerTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingJdbcEventListenerTests.java @@ -21,8 +21,6 @@ import java.sql.PreparedStatement; import javax.sql.DataSource; -import brave.handler.MutableSpan; -import brave.test.TestSpanHandler; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -31,6 +29,8 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cloud.sleuth.autoconfig.instrument.jdbc.TraceJdbcAutoConfiguration; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; import static org.assertj.core.api.Assertions.assertThat; @@ -64,14 +64,14 @@ public abstract class TracingJdbcEventListenerTests extends TracingListenerStrat preparedStatement.executeUpdate(); connection.close(); - assertThat(spanReporter.spans()).hasSize(2); - MutableSpan connectionSpan = spanReporter.spans().get(1); - MutableSpan statementSpan = spanReporter.spans().get(0); - assertThat(connectionSpan.name()).isEqualTo("connection"); - assertThat(statementSpan.name()).isEqualTo("update"); - assertThat(statementSpan.tags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, + assertThat(spanReporter.reportedSpans()).hasSize(2); + FinishedSpan connectionSpan = spanReporter.reportedSpans().get(1); + FinishedSpan statementSpan = spanReporter.reportedSpans().get(0); + assertThat(connectionSpan.getName()).isEqualTo("connection"); + assertThat(statementSpan.getName()).isEqualTo("update"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_SQL_QUERY_TAG_NAME, "UPDATE INFORMATION_SCHEMA.TABLES SET table_Name = ? WHERE 0 = ?"); - assertThat(statementSpan.tags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); + assertThat(statementSpan.getTags()).containsEntry(SPAN_ROW_COUNT_TAG_NAME, "0"); }); } From 6a094db84e5f8037012a5b2732e40b4562b23376 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 28 May 2021 14:10:43 +0200 Subject: [PATCH 44/59] Changed the logging defaults --- docs/src/main/asciidoc/_configprops.adoc | 4 +- .../jdbc/P6SpyPropertiesSetter.java | 2 +- .../instrument/jdbc/TraceJdbcProperties.java | 6 +- .../jdbc/P6SpyConfigurationTests.java | 3 +- .../web/TraceAsyncIntegrationTests.java | 480 +++++++++--------- 5 files changed, 249 insertions(+), 246 deletions(-) diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index d560507f6..bc9c8b87b 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -27,7 +27,7 @@ |spring.sleuth.jdbc.datasource-proxy.json-format | `false` | Use json output for logging query. @see ProxyDataSourceBuilder#asJson() |spring.sleuth.jdbc.datasource-proxy.logging | | Logging to use for logging queries. |spring.sleuth.jdbc.datasource-proxy.multiline | `true` | Use multiline output for logging query. @see ProxyDataSourceBuilder#multiline() -|spring.sleuth.jdbc.datasource-proxy.query.enable-logging | `true` | Enable logging all queries to the log. +|spring.sleuth.jdbc.datasource-proxy.query.enable-logging | `false` | Enable logging all queries to the log. |spring.sleuth.jdbc.datasource-proxy.query.log-level | `DEBUG` | Severity of query logger. |spring.sleuth.jdbc.datasource-proxy.query.logger-name | | Name of query logger. |spring.sleuth.jdbc.datasource-proxy.slow-query.log-level | `WARN` | Severity of slow query logger. @@ -37,7 +37,7 @@ |spring.sleuth.jdbc.excluded-data-source-bean-names | | List of DataSource bean names that will not be decorated. |spring.sleuth.jdbc.includes | | Which types of tracing we would like to include. |spring.sleuth.jdbc.p6spy.custom-appender-class | | Class file to use (only with logging=custom). The class must implement {@link com.p6spy.engine.spy.appender.FormattedLogger}. -|spring.sleuth.jdbc.p6spy.enable-logging | `true` | Enables logging JDBC events. +|spring.sleuth.jdbc.p6spy.enable-logging | `false` | Enables logging JDBC events. |spring.sleuth.jdbc.p6spy.enabled | `true` | Should the p6spy tracing be enabled? |spring.sleuth.jdbc.p6spy.log-file | `spy.log` | Name of log file to use (only with logging=file). |spring.sleuth.jdbc.p6spy.log-filter.pattern | | Use regex pattern to filter log messages. Only matched messages will be logged. diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java index b5633a019..1b4ce7be3 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java @@ -67,7 +67,7 @@ class P6SpyPropertiesSetter implements BeanDefinitionRegistryPostProcessor, Clos ConfigurableEnvironment environment = this.context.getEnvironment(); String customModuleList = initialP6SpyOptions.get("modulelist"); boolean isEnableLogging = environment.getProperty("spring.sleuth.jdbc.p6spy.enable-logging", Boolean.class, - true); + false); if (customModuleList != null) { log.info("P6Spy modulelist is overridden, some p6spy configuration features will not be applied"); } diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java index a0ffa1f4e..32d4360b9 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java @@ -190,7 +190,7 @@ public class TraceJdbcProperties { /** * Enable logging all queries to the log. */ - private boolean enableLogging = true; + private boolean enableLogging = false; /** * Name of query logger. @@ -236,7 +236,7 @@ public class TraceJdbcProperties { /** * Enable logging slow queries to the log. */ - private boolean enableLogging = true; + private boolean enableLogging = false; /** * Name of slow query logger. @@ -331,7 +331,7 @@ public class TraceJdbcProperties { /** * Enables logging JDBC events. */ - private boolean enableLogging = true; + private boolean enableLogging = false; /** * Enables multiline output. diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java index 2c5a9c551..21f95af8c 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java @@ -57,7 +57,8 @@ class P6SpyConfigurationTests { .withConfiguration( AutoConfigurations.of(DataSourceAutoConfiguration.class, TraceJdbcAutoConfiguration.class, TraceNoOpAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class)) - .withPropertyValues("spring.datasource.initialization-mode=never", "spring.sleuth.noop.enabled=true", + .withPropertyValues("spring.datasource.initialization-mode=never", + "spring.sleuth.jdbc.p6spy.enable-logging=true", "spring.sleuth.noop.enabled=true", "spring.datasource.url:jdbc:h2:mem:testdb-" + ThreadLocalRandom.current().nextInt()) .withClassLoader(new FilteredClassLoader("net.ttddyy.dsproxy")); diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java index 84cc39f15..3c69bd8c0 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-mvc-tests/src/test/java/org/springframework/cloud/sleuth/brave/instrument/web/TraceAsyncIntegrationTests.java @@ -1,239 +1,241 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.brave.instrument.web; - -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import brave.Span; -import brave.Tracer; -import brave.handler.MutableSpan; -import brave.handler.SpanHandler; -import brave.sampler.Sampler; -import brave.test.TestSpanHandler; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.sleuth.SpanName; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.EnableAsync; - -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.assertj.core.api.BDDAssertions.then; - -@SpringBootTest(classes = { TraceAsyncIntegrationTests.TraceAsyncITestConfiguration.class }) -public class TraceAsyncIntegrationTests { - - @Autowired - ClassPerformingAsyncLogic classPerformingAsyncLogic; - - @Autowired - Tracer tracer; - - @Autowired - TestSpanHandler spans; - - @BeforeEach - public void cleanup() { - this.spans.clear(); - this.classPerformingAsyncLogic.clear(); - } - - @Test - public void should_set_span_on_an_async_annotated_method() { - whenAsyncProcessingTakesPlace(); - - thenANewAsyncSpanGetsCreated(); - } - - @Test - public void should_set_span_with_custom_method_on_an_async_annotated_method() { - whenAsyncProcessingTakesPlaceWithCustomSpanName(); - - thenAsyncSpanHasCustomName(); - } - - @Test - public void should_continue_a_span_on_an_async_annotated_method() { - Span span = givenASpanInCurrentThread(); - - try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { - whenAsyncProcessingTakesPlace(); - } - finally { - span.finish(); - } - - thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(span); - } - - @Test - public void should_continue_a_span_with_custom_method_on_an_async_annotated_method() { - Span span = givenASpanInCurrentThread(); - - try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { - whenAsyncProcessingTakesPlaceWithCustomSpanName(); - } - finally { - span.finish(); - } - - thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(span); - } - - private Span givenASpanInCurrentThread() { - return this.tracer.nextSpan().name("http:existing"); - } - - private void whenAsyncProcessingTakesPlace() { - this.classPerformingAsyncLogic.invokeAsynchronousLogic(); - } - - private void whenAsyncProcessingTakesPlaceWithCustomSpanName() { - this.classPerformingAsyncLogic.customNameInvokeAsynchronousLogic(); - } - - private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(final Span span) { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) - .isEqualTo(span.context().traceId()); - List webSpans = this.spans.spans().stream() - .filter(mutableSpan -> mutableSpan.traceId().equalsIgnoreCase(span.context().traceIdString())) - .collect(Collectors.toList()); - then(webSpans).hasSize(2); - // HTTP - then(webSpans.get(0).name()).isEqualTo("http:existing"); - // ASYNC - then(webSpans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "invokeAsynchronousLogic"); - }); - } - - private void thenANewAsyncSpanGetsCreated() { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - List spans = this.spans.spans().stream().filter(mutableSpan -> mutableSpan.name().equals("invoke-asynchronous-logic")).collect(Collectors.toList()); - then(spans).hasSize(1); - MutableSpan storedSpan = spans.get(0); - then(storedSpan.name()).isEqualTo("invoke-asynchronous-logic"); - then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "invokeAsynchronousLogic"); - }); - } - - private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(final Span span) { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan()).isNotNull(); - then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) - .isEqualTo(span.context().traceId()); - then(this.spans).hasSize(2); - // HTTP - then(this.spans.get(0).name()).isEqualTo("http:existing"); - // ASYNC - then(this.spans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "customNameInvokeAsynchronousLogic"); - }); - } - - private void thenAsyncSpanHasCustomName() { - Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { - then(this.spans).hasSize(1); - MutableSpan storedSpan = this.spans.get(0); - then(storedSpan.name()).isEqualTo("foo"); - then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", - "customNameInvokeAsynchronousLogic"); - }); - } - - @AfterEach - public void cleanTrace() { - this.spans.clear(); - } - - @EnableAutoConfiguration - @EnableAsync - @Configuration(proxyBeanMethods = false) - static class TraceAsyncITestConfiguration { - - @Bean - ClassPerformingAsyncLogic asyncClass(Tracer tracer) { - return new ClassPerformingAsyncLogic(tracer); - } - - @Bean - Sampler defaultSampler() { - return Sampler.ALWAYS_SAMPLE; - } - - @Bean - SpanHandler testSpanHandler() { - return new TestSpanHandler(); - } - - @Bean - Executor fooExecutor() { - return new SimpleAsyncTaskExecutor(); - } - - @Bean - Executor barExecutor() { - return new SimpleAsyncTaskExecutor(); - } - - } - - static class ClassPerformingAsyncLogic { - - private final Tracer tracer; - - AtomicReference span = new AtomicReference<>(); - - ClassPerformingAsyncLogic(Tracer tracer) { - this.tracer = tracer; - } - - @Async("fooExecutor") - public void invokeAsynchronousLogic() { - this.span.set(this.tracer.currentSpan()); - } - - @Async - @SpanName("foo") - public void customNameInvokeAsynchronousLogic() { - this.span.set(this.tracer.currentSpan()); - } - - public Span getSpan() { - return this.span.get(); - } - - public void clear() { - this.span.set(null); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.instrument.web; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import brave.Span; +import brave.Tracer; +import brave.handler.MutableSpan; +import brave.handler.SpanHandler; +import brave.sampler.Sampler; +import brave.test.TestSpanHandler; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.SpanName; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.BDDAssertions.then; + +@SpringBootTest(classes = { TraceAsyncIntegrationTests.TraceAsyncITestConfiguration.class }) +public class TraceAsyncIntegrationTests { + + @Autowired + ClassPerformingAsyncLogic classPerformingAsyncLogic; + + @Autowired + Tracer tracer; + + @Autowired + TestSpanHandler spans; + + @BeforeEach + public void cleanup() { + this.spans.clear(); + this.classPerformingAsyncLogic.clear(); + } + + @Test + public void should_set_span_on_an_async_annotated_method() { + whenAsyncProcessingTakesPlace(); + + thenANewAsyncSpanGetsCreated(); + } + + @Test + public void should_set_span_with_custom_method_on_an_async_annotated_method() { + whenAsyncProcessingTakesPlaceWithCustomSpanName(); + + thenAsyncSpanHasCustomName(); + } + + @Test + public void should_continue_a_span_on_an_async_annotated_method() { + Span span = givenASpanInCurrentThread(); + + try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { + whenAsyncProcessingTakesPlace(); + } + finally { + span.finish(); + } + + thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(span); + } + + @Test + public void should_continue_a_span_with_custom_method_on_an_async_annotated_method() { + Span span = givenASpanInCurrentThread(); + + try (Tracer.SpanInScope ws = this.tracer.withSpanInScope(span)) { + whenAsyncProcessingTakesPlaceWithCustomSpanName(); + } + finally { + span.finish(); + } + + thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(span); + } + + private Span givenASpanInCurrentThread() { + return this.tracer.nextSpan().name("http:existing"); + } + + private void whenAsyncProcessingTakesPlace() { + this.classPerformingAsyncLogic.invokeAsynchronousLogic(); + } + + private void whenAsyncProcessingTakesPlaceWithCustomSpanName() { + this.classPerformingAsyncLogic.customNameInvokeAsynchronousLogic(); + } + + private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOne(final Span span) { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) + .isEqualTo(span.context().traceId()); + List webSpans = this.spans.spans().stream() + .filter(mutableSpan -> mutableSpan.traceId().equalsIgnoreCase(span.context().traceIdString())) + .collect(Collectors.toList()); + then(webSpans).hasSize(2); + // HTTP + then(webSpans.get(0).name()).isEqualTo("http:existing"); + // ASYNC + then(webSpans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "invokeAsynchronousLogic"); + }); + } + + private void thenANewAsyncSpanGetsCreated() { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + List spans = this.spans.spans().stream() + .filter(mutableSpan -> mutableSpan.name().equals("invoke-asynchronous-logic")) + .collect(Collectors.toList()); + then(spans).hasSize(1); + MutableSpan storedSpan = spans.get(0); + then(storedSpan.name()).isEqualTo("invoke-asynchronous-logic"); + then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "invokeAsynchronousLogic"); + }); + } + + private void thenTraceIdIsPassedFromTheCurrentThreadToTheAsyncOneAndSpanHasCustomName(final Span span) { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan()).isNotNull(); + then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) + .isEqualTo(span.context().traceId()); + then(this.spans).hasSize(2); + // HTTP + then(this.spans.get(0).name()).isEqualTo("http:existing"); + // ASYNC + then(this.spans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "customNameInvokeAsynchronousLogic"); + }); + } + + private void thenAsyncSpanHasCustomName() { + Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { + then(this.spans).hasSize(1); + MutableSpan storedSpan = this.spans.get(0); + then(storedSpan.name()).isEqualTo("foo"); + then(storedSpan.tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + "customNameInvokeAsynchronousLogic"); + }); + } + + @AfterEach + public void cleanTrace() { + this.spans.clear(); + } + + @EnableAutoConfiguration + @EnableAsync + @Configuration(proxyBeanMethods = false) + static class TraceAsyncITestConfiguration { + + @Bean + ClassPerformingAsyncLogic asyncClass(Tracer tracer) { + return new ClassPerformingAsyncLogic(tracer); + } + + @Bean + Sampler defaultSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + SpanHandler testSpanHandler() { + return new TestSpanHandler(); + } + + @Bean + Executor fooExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + @Bean + Executor barExecutor() { + return new SimpleAsyncTaskExecutor(); + } + + } + + static class ClassPerformingAsyncLogic { + + private final Tracer tracer; + + AtomicReference span = new AtomicReference<>(); + + ClassPerformingAsyncLogic(Tracer tracer) { + this.tracer = tracer; + } + + @Async("fooExecutor") + public void invokeAsynchronousLogic() { + this.span.set(this.tracer.currentSpan()); + } + + @Async + @SpanName("foo") + public void customNameInvokeAsynchronousLogic() { + this.span.set(this.tracer.currentSpan()); + } + + public Span getSpan() { + return this.span.get(); + } + + public void clear() { + this.span.set(null); + } + + } + +} From 4fcc7cc32e885efa48d143725daf7e13c31925ff Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 9 Jun 2021 16:11:23 +0200 Subject: [PATCH 45/59] Adds Spring Session support (#1961) * Adds Spring Session support * Changes following the review --- docs/src/main/asciidoc/_spans.adoc | 40 +++++ docs/src/main/asciidoc/integrations.adoc | 8 + spring-cloud-sleuth-autoconfigure/pom.xml | 5 + .../TraceSessionAutoConfiguration.java | 51 +++++++ ...itional-spring-configuration-metadata.json | 6 + .../main/resources/META-INF/spring.factories | 1 + ...eRpcAutoConfigurationIntegrationTests.java | 3 +- .../instrument/web/client/WebClientTests.java | 5 +- ...aceAsyncDefaultAutoConfigurationTests.java | 3 +- .../TraceQuartzAutoConfigurationTest.java | 11 +- .../TraceSessionAutoConfigurationTests.java | 53 +++++++ .../BraveWebClientAutoConfigurationTests.java | 3 +- .../instrument/web/client/GH846Tests.java | 3 +- .../client/TraceWebClientDisabledTests.java | 3 +- .../zipkin2/ZipkinSamplerTests.java | 11 +- .../src/test/resources/application.yml | 2 +- spring-cloud-sleuth-instrumentation/pom.xml | 5 + .../TraceCircuitBreakerFactoryAspect.java | 2 +- .../instrument/session/SleuthSessionSpan.java | 85 +++++++++++ ...TraceFindByIndexNameSessionRepository.java | 62 ++++++++ .../TraceReactiveSessionRepository.java | 66 ++++++++ .../session/TraceSessionRepository.java | 87 +++++++++++ .../session/TraceSessionRepositoryAspect.java | 143 ++++++++++++++++++ ...FindByIndexNameSessionRepositoryTests.java | 66 ++++++++ .../TraceReactiveSessionRepositoryTests.java | 93 ++++++++++++ .../session/TraceSessionRepositoryTests.java | 77 ++++++++++ .../cloud/sleuth/tracer/SimpleSpan.java | 7 + .../baggage/MultipleHopsIntegrationTests.java | 6 +- .../JmsTracingConfigurationTest.java | 6 +- .../issue_943/HelloSpringIntegration.java | 3 +- 30 files changed, 894 insertions(+), 22 deletions(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/session/TraceSessionAutoConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/session/TraceSessionAutoConfigurationTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/SleuthSessionSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceFindByIndexNameSessionRepository.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceReactiveSessionRepository.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceSessionRepository.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceSessionRepositoryAspect.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/session/TraceFindByIndexNameSessionRepositoryTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/session/TraceReactiveSessionRepositoryTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/session/TraceSessionRepositoryTests.java diff --git a/docs/src/main/asciidoc/_spans.adoc b/docs/src/main/asciidoc/_spans.adoc index 4172c04e5..2ded3abf3 100644 --- a/docs/src/main/asciidoc/_spans.adoc +++ b/docs/src/main/asciidoc/_spans.adoc @@ -507,6 +507,46 @@ Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.in |method|Method name that got annotated with @Scheduled. |=== +=== Session Create Span + +> Span created when a new session has to be created. + +**Span name** `session.create`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.session.SleuthSessionSpan` + +=== Session Delete Span + +> Span created when a session is deleted. + +**Span name** `session.delete`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.session.SleuthSessionSpan` + +=== Session Find Span + +> Span created when a new session is searched for. + +**Span name** `session.find`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.session.SleuthSessionSpan` + +IMPORTANT: All tags and events must be prefixed with `session.` prefix! + +.Tag Keys +|=== +|Name | Description +|session.index.name| +|=== + +=== Session Save Span + +> Span created when a new session is saved. + +**Span name** `session.save`. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.session.SleuthSessionSpan` + === Task Execution Listener Span > Span created within the lifecycle of a task. diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 1d43c2604..a0d24f277 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -714,3 +714,11 @@ Please check the <> page under `spring.sleuth.j You can configure P6Spy manually using one of available configuration methods. For more information please refer to the http://p6spy.readthedocs.io/en/latest/configandusage.html[P6Spy Configuration Guide]. In order to disable this instrumentation set `spring.sleuth.jdbc.enabled` to `false`. + +[[sleuth-session-integration]] +== Spring Session + +This feature is available for all tracer implementations. + +We're instrumenting the `Session` repositories that wraps all operations in a span. +In order to disable this instrumentation set `spring.sleuth.session.enabled` to `false`. diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index 34c1f7bfa..de8c4d1b1 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -213,6 +213,11 @@ HikariCP true + + org.springframework.session + spring-session-data-redis + true + org.springframework.cloud diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/session/TraceSessionAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/session/TraceSessionAutoConfiguration.java new file mode 100644 index 000000000..1dda32807 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/session/TraceSessionAutoConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.session; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.session.TraceSessionRepositoryAspect; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.SessionRepository; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Spring Session. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(SessionRepository.class) +@ConditionalOnProperty(value = "spring.sleuth.session.enabled", matchIfMissing = true) +@ConditionalOnBean(Tracer.class) +@AutoConfigureAfter(BraveAutoConfiguration.class) +public class TraceSessionAutoConfiguration { + + // Bean post processors don't work cause session-redis requires concrete classes + @Bean + TraceSessionRepositoryAspect traceSessionRepositoryAspect(Tracer tracer, CurrentTraceContext currentTraceContext) { + return new TraceSessionRepositoryAspect(tracer, currentTraceContext); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 95cc9ca24..1cfec09f1 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -185,6 +185,12 @@ "description": "Enable R2dbc instrumentation.", "defaultValue": true }, + { + "name": "spring.sleuth.session.enabled", + "type": "java.lang.Boolean", + "description": "Enable Spring Session instrumentation.", + "defaultValue": true + }, { "name": "spring.sleuth.vault.enabled", "type": "java.lang.Boolean", diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index 16b2fd99f..f859d669d 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -18,6 +18,7 @@ org.springframework.cloud.sleuth.autoconfig.instrument.web.client.TraceWebClient org.springframework.cloud.sleuth.autoconfig.instrument.web.client.feign.TraceFeignClientAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.web.client.TraceWebAsyncClientAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.scheduling.TraceSchedulingAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.session.TraceSessionAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.reactor.TraceReactorAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceFunctionAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.messaging.TraceSpringIntegrationAutoConfiguration,\ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java index 0c2ea1e76..730c7a278 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java @@ -30,6 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; @@ -62,7 +63,7 @@ public class BraveRpcAutoConfigurationIntegrationTests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) @Configuration(proxyBeanMethods = false) public static class Config { diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java index 5da06cf57..c5fcf7908 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java @@ -43,6 +43,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; @@ -150,8 +151,8 @@ public class WebClientTests { @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, - R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, MongoAutoConfiguration.class, - MongoDataAutoConfiguration.class }) + R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, + MongoAutoConfiguration.class, MongoDataAutoConfiguration.class }) @DisableSecurity public static class TestConfiguration { diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java index 4f001fb06..d96ce17c4 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java @@ -26,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; @@ -53,7 +54,7 @@ public class TraceAsyncDefaultAutoConfigurationTests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) static class Config { @Bean diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java index 165e728c6..8ce3abc66 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java @@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; @@ -115,10 +116,12 @@ public class TraceQuartzAutoConfigurationTest { } @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, - GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, - MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class }) + @EnableAutoConfiguration( + exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, + GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, + MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }, + excludeName = "org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration") public static class EnableAutoConfig { } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/session/TraceSessionAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/session/TraceSessionAutoConfigurationTests.java new file mode 100644 index 000000000..67e03c395 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/session/TraceSessionAutoConfigurationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.session; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.session.TraceSessionRepositoryAspect; +import org.springframework.session.SessionRepository; + +class TraceSessionAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.sleuth.noop.enabled=true").withConfiguration( + AutoConfigurations.of(TraceNoOpAutoConfiguration.class, TraceSessionAutoConfiguration.class)); + + @Test + void should_register_session_aspect() { + this.contextRunner + .run(context -> Assertions.assertThat(context).hasSingleBean(TraceSessionRepositoryAspect.class)); + } + + @Test + void should_not_register_session_aspect_when_session_repository_not_on_classpath() { + this.contextRunner.withClassLoader(new FilteredClassLoader(SessionRepository.class)) + .run(context -> Assertions.assertThat(context).doesNotHaveBean(TraceSessionRepositoryAspect.class)); + } + + @Test + void should_not_register_session_aspect_when_session_instrumentation_is_disabled() { + this.contextRunner.withPropertyValues("spring.sleuth.session.enabled=false") + .run(context -> Assertions.assertThat(context).doesNotHaveBean(TraceSessionRepositoryAspect.class)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java index 8306face3..016442269 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java @@ -30,6 +30,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; @@ -123,7 +124,7 @@ public class BraveWebClientAutoConfigurationTests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) static class Config { // custom builder diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java index 0fd557aad..0801a617d 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java @@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; @@ -57,7 +58,7 @@ public class GH846Tests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) static class App { @Bean diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java index 43e9d29d9..07c6b8e7b 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; @@ -46,7 +47,7 @@ public class TraceWebClientDisabledTests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) public static class Config { } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java index f3497539d..b3061be84 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java @@ -23,6 +23,7 @@ import org.springframework.boot.actuate.autoconfigure.security.servlet.Managemen import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; @@ -53,10 +54,12 @@ public class ZipkinSamplerTests { } @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, - GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, - MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class }) + @EnableAutoConfiguration( + exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, + GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, + MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }, + excludeName = "org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration") static class TestConfig { } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml b/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml index 1e05c6764..e5b38daa4 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml +++ b/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml @@ -1,3 +1,3 @@ logging.level.org.springframework.cloud: DEBUG -spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration, org.springframework.cloud.gateway.config.GatewayAutoConfiguration, org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration, org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration, org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration, org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration, org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration +spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration, org.springframework.cloud.gateway.config.GatewayAutoConfiguration, org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration, org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration, org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration, org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration, org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration, org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration, org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index 17b95a094..fd7a5aa4d 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -197,6 +197,11 @@ spring-security-oauth2-autoconfigure true + + org.springframework.session + spring-session-data-redis + true + p6spy diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceCircuitBreakerFactoryAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceCircuitBreakerFactoryAspect.java index 32505fbc7..58c38607e 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceCircuitBreakerFactoryAspect.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceCircuitBreakerFactoryAspect.java @@ -24,7 +24,7 @@ import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; import org.springframework.cloud.sleuth.Tracer; /** - * Aspec around {@link CircuitBreaker} creation. + * Aspect around {@link CircuitBreaker} creation. * * @author Marcin Grzejszczak * @since 3.0.0 diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/SleuthSessionSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/SleuthSessionSpan.java new file mode 100644 index 000000000..d34a1b025 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/SleuthSessionSpan.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.session; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthSessionSpan implements DocumentedSpan { + + /** + * Span created when a new session has to be created. + */ + SESSION_CREATE_SPAN { + @Override + public String getName() { + return "session.create"; + } + }, + + /** + * Span created when a new session is searched for. + */ + SESSION_FIND_SPAN { + @Override + public String getName() { + return "session.find"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + @Override + public String prefix() { + return "session."; + } + }, + + /** + * Span created when a new session is saved. + */ + SESSION_SAVE_SPAN { + @Override + public String getName() { + return "session.save"; + } + }, + + /** + * Span created when a session is deleted. + */ + SESSION_DELETE_SPAN { + @Override + public String getName() { + return "session.delete"; + } + }; + + enum Tags implements TagKey { + + INDEX_NAME { + @Override + public String getKey() { + return "session.index.name"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceFindByIndexNameSessionRepository.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceFindByIndexNameSessionRepository.java new file mode 100644 index 000000000..eb2480f39 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceFindByIndexNameSessionRepository.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.session; + +import java.util.Map; + +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; +import org.springframework.session.FindByIndexNameSessionRepository; + +class TraceFindByIndexNameSessionRepository extends TraceSessionRepository implements FindByIndexNameSessionRepository { + + private final FindByIndexNameSessionRepository delegate; + + TraceFindByIndexNameSessionRepository(Tracer tracer, FindByIndexNameSessionRepository delegate) { + super(tracer, delegate); + this.delegate = delegate; + } + + @Override + public Map findByPrincipalName(String principalName) { + AssertingSpan span = newSessionFindSpan(); + try (Tracer.SpanInScope ws = this.tracer.withSpan(span.start())) { + return this.delegate.findByPrincipalName(principalName); + } + finally { + span.end(); + } + } + + private AssertingSpan newSessionFindSpan() { + return AssertingSpan.of(SleuthSessionSpan.SESSION_FIND_SPAN, this.tracer.nextSpan()) + .name(SleuthSessionSpan.SESSION_FIND_SPAN.getName()); + } + + @Override + public Map findByIndexNameAndIndexValue(String indexName, String indexValue) { + AssertingSpan span = newSessionFindSpan(); + try (Tracer.SpanInScope ws = this.tracer.withSpan(span.start())) { + span.tag(SleuthSessionSpan.Tags.INDEX_NAME, indexName); + return this.delegate.findByIndexNameAndIndexValue(indexName, indexValue); + } + finally { + span.end(); + } + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceReactiveSessionRepository.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceReactiveSessionRepository.java new file mode 100644 index 000000000..ffd3506a2 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceReactiveSessionRepository.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.session; + +import reactor.core.publisher.Mono; + +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; +import org.springframework.session.ReactiveSessionRepository; +import org.springframework.session.Session; + +class TraceReactiveSessionRepository implements ReactiveSessionRepository { + + private final ReactiveSessionRepository delegate; + + private final Tracer tracer; + + private final CurrentTraceContext currentTraceContext; + + TraceReactiveSessionRepository(Tracer tracer, CurrentTraceContext currentTraceContext, + ReactiveSessionRepository delegate) { + this.delegate = delegate; + this.tracer = tracer; + this.currentTraceContext = currentTraceContext; + } + + @Override + public Mono createSession() { + return ReactorSleuth.tracedMono(this.tracer, this.currentTraceContext, + SleuthSessionSpan.SESSION_CREATE_SPAN.getName(), () -> this.delegate.createSession()); + } + + @Override + public Mono save(Session session) { + return ReactorSleuth.tracedMono(this.tracer, this.currentTraceContext, + SleuthSessionSpan.SESSION_SAVE_SPAN.getName(), () -> this.delegate.save(session)); + } + + @Override + public Mono findById(String id) { + return ReactorSleuth.tracedMono(this.tracer, this.currentTraceContext, + SleuthSessionSpan.SESSION_FIND_SPAN.getName(), () -> this.delegate.findById(id)); + } + + @Override + public Mono deleteById(String id) { + return ReactorSleuth.tracedMono(this.tracer, this.currentTraceContext, + SleuthSessionSpan.SESSION_DELETE_SPAN.getName(), () -> this.delegate.deleteById(id)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceSessionRepository.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceSessionRepository.java new file mode 100644 index 000000000..9a0a1bba0 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceSessionRepository.java @@ -0,0 +1,87 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.session; + +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; +import org.springframework.session.Session; +import org.springframework.session.SessionRepository; + +class TraceSessionRepository implements SessionRepository { + + private static final Log log = LogFactory.getLog(TraceSessionRepository.class); + + private final SessionRepository delegate; + + protected final Tracer tracer; + + TraceSessionRepository(Tracer tracer, SessionRepository delegate) { + this.delegate = delegate; + this.tracer = tracer; + } + + @Override + public Session createSession() { + return wrap(SleuthSessionSpan.SESSION_CREATE_SPAN, (Supplier) this.delegate::createSession); + } + + private T wrap(SleuthSessionSpan sessionSpan, Supplier supplier) { + AssertingSpan span = newSpan(sessionSpan); + if (log.isDebugEnabled()) { + log.debug( + "Wrapping call in a span with name [" + span.getDocumentedSpan().getName() + "] - [" + span + "]"); + } + try (Tracer.SpanInScope ws = this.tracer.withSpan(span.start())) { + return supplier.get(); + } + finally { + span.end(); + } + } + + private void wrap(SleuthSessionSpan sessionSpan, Runnable runnable) { + wrap(sessionSpan, () -> { + runnable.run(); + return null; + }); + } + + private AssertingSpan newSpan(SleuthSessionSpan sessionCreateSpan) { + return AssertingSpan.of(sessionCreateSpan, this.tracer.nextSpan()).name(sessionCreateSpan.getName()); + } + + @Override + public void save(Session session) { + wrap(SleuthSessionSpan.SESSION_SAVE_SPAN, () -> this.delegate.save(session)); + } + + @Override + public Session findById(String id) { + return wrap(SleuthSessionSpan.SESSION_FIND_SPAN, () -> this.delegate.findById(id)); + } + + @Override + public void deleteById(String id) { + wrap(SleuthSessionSpan.SESSION_DELETE_SPAN, () -> this.delegate.deleteById(id)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceSessionRepositoryAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceSessionRepositoryAspect.java new file mode 100644 index 000000000..32ab521b8 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/session/TraceSessionRepositoryAspect.java @@ -0,0 +1,143 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.session; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; + +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.ReactiveSessionRepository; +import org.springframework.session.SessionRepository; +import org.springframework.util.ReflectionUtils; + +/** + * Aspect around {@link SessionRepository} and {@link ReactiveSessionRepository} method + * execution. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Aspect +public class TraceSessionRepositoryAspect { + + private static final Log log = LogFactory.getLog(TraceSessionRepositoryAspect.class); + + private final Tracer tracer; + + private final CurrentTraceContext currentTraceContext; + + public TraceSessionRepositoryAspect(Tracer tracer, CurrentTraceContext currentTraceContext) { + this.tracer = tracer; + this.currentTraceContext = currentTraceContext; + } + + // RedisIndexedSessionRepository + @Around("execution(public * org.springframework.session.SessionRepository.*(..))") + public Object wrapSessionRepository(ProceedingJoinPoint pjp) throws Throwable { + SessionRepository target = (SessionRepository) pjp.getTarget(); + if (target instanceof TraceSessionRepository) { + return pjp.proceed(); + } + target = wrapSessionRepository(target); + return callMethodOnWrappedObject(pjp, target); + } + + private SessionRepository wrapSessionRepository(SessionRepository target) { + if (target instanceof FindByIndexNameSessionRepository) { + return new TraceFindByIndexNameSessionRepository(this.tracer, (FindByIndexNameSessionRepository) target); + } + return new TraceSessionRepository(this.tracer, target); + } + + private Object callMethodOnWrappedObject(ProceedingJoinPoint pjp, T target) throws Throwable { + Method method = getMethod(pjp, target); + if (method != null) { + if (log.isDebugEnabled()) { + log.debug("Found a corresponding method on the trace representation [" + method + "]"); + } + return method.invoke(target, pjp.getArgs()); + } + if (log.isDebugEnabled()) { + log.debug("Method [" + pjp.getSignature().getName() + + "] not found on the trace representation. Will run the original one."); + } + return pjp.proceed(); + } + + @Around("execution(public * org.springframework.session.ReactiveSessionRepository.*(..))") + public Object wrapReactiveSessionRepository(ProceedingJoinPoint pjp) throws Throwable { + ReactiveSessionRepository target = (ReactiveSessionRepository) pjp.getTarget(); + if (target instanceof TraceReactiveSessionRepository) { + return pjp.proceed(); + } + target = new TraceReactiveSessionRepository(this.tracer, this.currentTraceContext, target); + return callMethodOnWrappedObject(pjp, target); + } + + private Method getMethod(ProceedingJoinPoint pjp, Object tracingWrapper) { + MethodSignature signature = (MethodSignature) pjp.getSignature(); + Method method = signature.getMethod(); + Method foundMethodOnTracingWrapper = ReflectionUtils.findMethod(tracingWrapper.getClass(), method.getName(), + method.getParameterTypes()); + if (foundMethodOnTracingWrapper != null) { + if (log.isDebugEnabled()) { + log.debug("Found an exact match for method execution [" + foundMethodOnTracingWrapper + "]"); + } + return foundMethodOnTracingWrapper; + } + Method[] uniquePublicDeclaredMethodsOnTracingWrapper = ReflectionUtils + .getUniqueDeclaredMethods(tracingWrapper.getClass(), m -> Modifier.isPublic(m.getModifiers())); + if (uniquePublicDeclaredMethodsOnTracingWrapper.length == 0) { + return null; + } + if (log.isTraceEnabled()) { + log.trace("Will pick one of the unique declared methods [" + + Arrays.toString(uniquePublicDeclaredMethodsOnTracingWrapper) + "] that has a name [" + + method.getName() + "]"); + } + Object[] argsOnOriginalObject = pjp.getArgs(); + return Arrays.stream(uniquePublicDeclaredMethodsOnTracingWrapper) + .filter(m -> m.getName().equals(method.getName()) + && paramsAreOfSameTyperInherited(argsOnOriginalObject, m.getParameterTypes())) + .findFirst().orElse(null); + } + + private boolean paramsAreOfSameTyperInherited(Object[] argsOnOriginalObject, Class[] typeOnTracingWrapper) { + if (argsOnOriginalObject.length != typeOnTracingWrapper.length) { + return false; + } + for (int i = 0; i < argsOnOriginalObject.length; i++) { + Class argType = argsOnOriginalObject[i].getClass(); + Class typeOnWrapper = typeOnTracingWrapper[i]; + if (!typeOnWrapper.isAssignableFrom(argType)) { + return false; + } + } + return true; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/session/TraceFindByIndexNameSessionRepositoryTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/session/TraceFindByIndexNameSessionRepositoryTests.java new file mode 100644 index 000000000..98f36b2a1 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/session/TraceFindByIndexNameSessionRepositoryTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.session; + +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.sleuth.tracer.SimpleSpan; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.SessionRepository; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class TraceFindByIndexNameSessionRepositoryTests extends TraceSessionRepositoryTests { + + FindByIndexNameSessionRepository delegate = mock(FindByIndexNameSessionRepository.class); + + TraceFindByIndexNameSessionRepository traceSessionRepository = new TraceFindByIndexNameSessionRepository( + this.simpleTracer, this.delegate); + + @Test + void should_trace_session_find_by_principle_name() { + this.traceSessionRepository.findByPrincipalName("foo"); + + verify(this.delegate).findByPrincipalName(any()); + SimpleSpan lastSpan = this.simpleTracer.getLastSpan(); + then(lastSpan.name).isEqualTo(SleuthSessionSpan.SESSION_FIND_SPAN.getName()); + } + + @Test + void should_trace_session_find_by_index_name_value() { + this.traceSessionRepository.findByIndexNameAndIndexValue("indexName1", "indexValue1"); + + verify(this.delegate).findByIndexNameAndIndexValue(any(), any()); + SimpleSpan lastSpan = this.simpleTracer.getLastSpan(); + then(lastSpan.name).isEqualTo(SleuthSessionSpan.SESSION_FIND_SPAN.getName()); + then(lastSpan.tags).containsEntry(SleuthSessionSpan.Tags.INDEX_NAME.getKey(), "indexName1"); + } + + @Override + SessionRepository sessionRepositoryDelegate() { + return this.delegate; + } + + @Override + SessionRepository sessionRepository() { + return this.traceSessionRepository; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/session/TraceReactiveSessionRepositoryTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/session/TraceReactiveSessionRepositoryTests.java new file mode 100644 index 000000000..64e22a363 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/session/TraceReactiveSessionRepositoryTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.session; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.sleuth.tracer.SimpleCurrentTraceContext; +import org.springframework.cloud.sleuth.tracer.SimpleSpan; +import org.springframework.cloud.sleuth.tracer.SimpleTracer; +import org.springframework.session.ReactiveSessionRepository; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class TraceReactiveSessionRepositoryTests { + + SimpleCurrentTraceContext simpleCurrentTraceContext = new SimpleCurrentTraceContext(); + + SimpleTracer simpleTracer = new SimpleTracer() { + @Override + public SimpleSpan nextSpan() { + SimpleSpan span = super.nextSpan(); + simpleCurrentTraceContext.traceContext = span.context(); + return span; + } + }; + + ReactiveSessionRepository delegate = mock(ReactiveSessionRepository.class); + + ReactiveSessionRepository traceSessionRepository = new TraceReactiveSessionRepository(this.simpleTracer, + this.simpleCurrentTraceContext, delegate); + + @Test + void should_trace_session_creation() { + given(this.delegate.createSession()).willReturn(Mono.empty()); + + this.traceSessionRepository.createSession().block(Duration.ofMillis(10)); + + verify(this.delegate).createSession(); + then(this.simpleTracer.getLastSpan().name).isEqualTo(SleuthSessionSpan.SESSION_CREATE_SPAN.getName()); + } + + @Test + void should_trace_session_save() { + given(this.delegate.save(any())).willReturn(Mono.empty()); + + this.traceSessionRepository.save(null).block(Duration.ofMillis(10)); + + verify(this.delegate).save(any()); + then(this.simpleTracer.getLastSpan().name).isEqualTo(SleuthSessionSpan.SESSION_SAVE_SPAN.getName()); + } + + @Test + void should_trace_session_find_by_id() { + given(this.delegate.findById(any())).willReturn(Mono.empty()); + + this.traceSessionRepository.findById("").block(Duration.ofMillis(10)); + + verify(this.delegate).findById(any()); + then(this.simpleTracer.getLastSpan().name).isEqualTo(SleuthSessionSpan.SESSION_FIND_SPAN.getName()); + } + + @Test + void should_trace_session_delete_by_id() { + given(this.delegate.deleteById(any())).willReturn(Mono.empty()); + + this.traceSessionRepository.deleteById("").block(Duration.ofMillis(10)); + + verify(this.delegate).deleteById(any()); + then(this.simpleTracer.getLastSpan().name).isEqualTo(SleuthSessionSpan.SESSION_DELETE_SPAN.getName()); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/session/TraceSessionRepositoryTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/session/TraceSessionRepositoryTests.java new file mode 100644 index 000000000..dac9d30f2 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/session/TraceSessionRepositoryTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.session; + +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.sleuth.tracer.SimpleTracer; +import org.springframework.session.SessionRepository; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class TraceSessionRepositoryTests { + + SimpleTracer simpleTracer = new SimpleTracer(); + + SessionRepository delegate = mock(SessionRepository.class); + + SessionRepository traceSessionRepository = new TraceSessionRepository(this.simpleTracer, delegate); + + @Test + void should_trace_session_creation() { + sessionRepository().createSession(); + + verify(sessionRepositoryDelegate()).createSession(); + then(this.simpleTracer.getLastSpan().name).isEqualTo(SleuthSessionSpan.SESSION_CREATE_SPAN.getName()); + } + + @Test + void should_trace_session_save() { + sessionRepository().save(null); + + verify(sessionRepositoryDelegate()).save(any()); + then(this.simpleTracer.getLastSpan().name).isEqualTo(SleuthSessionSpan.SESSION_SAVE_SPAN.getName()); + } + + @Test + void should_trace_session_find_by_id() { + sessionRepository().findById(""); + + verify(sessionRepositoryDelegate()).findById(any()); + then(this.simpleTracer.getLastSpan().name).isEqualTo(SleuthSessionSpan.SESSION_FIND_SPAN.getName()); + } + + @Test + void should_trace_session_delete_by_id() { + sessionRepository().deleteById(""); + + verify(sessionRepositoryDelegate()).deleteById(any()); + then(this.simpleTracer.getLastSpan().name).isEqualTo(SleuthSessionSpan.SESSION_DELETE_SPAN.getName()); + } + + SessionRepository sessionRepository() { + return this.traceSessionRepository; + } + + SessionRepository sessionRepositoryDelegate() { + return this.delegate; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java index ac609ac27..c4213fd4b 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java @@ -104,4 +104,11 @@ public class SimpleSpan implements Span { return this; } + @Override + public String toString() { + return "SimpleSpan{" + "tags=" + tags + ", started=" + started + ", ended=" + ended + ", throwable=" + throwable + + ", remoteServiceName='" + remoteServiceName + '\'' + ", spanKind=" + spanKind + ", events=" + events + + ", name='" + name + '\'' + '}'; + } + } diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java index f373f8588..300ff616a 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java @@ -22,6 +22,7 @@ import brave.sampler.Sampler; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; @@ -74,8 +75,9 @@ public class MultipleHopsIntegrationTests } @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration(exclude = { MongoAutoConfiguration.class, QuartzAutoConfiguration.class, - JmxAutoConfiguration.class, R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class }) + @EnableAutoConfiguration( + exclude = { MongoAutoConfiguration.class, QuartzAutoConfiguration.class, JmxAutoConfiguration.class, + R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) static class Config { @Bean diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java index b7d5b2105..f4a41ed5c 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java @@ -36,6 +36,7 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration; import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -168,8 +169,9 @@ public class JmsTracingConfigurationTest { } @Configuration(proxyBeanMethods = false) -@EnableAutoConfiguration(exclude = { KafkaAutoConfiguration.class, MongoAutoConfiguration.class, - QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class }) +@EnableAutoConfiguration( + exclude = { KafkaAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, + R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) class JmsTestTracingConfiguration { } diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java index 734e096d3..a51599cfd 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java @@ -23,6 +23,7 @@ import brave.test.TestSpanHandler; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; @@ -33,7 +34,7 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.web.client.RestTemplate; @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class, - R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class }) + R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) @ImportResource("classpath:beans/applicationContext.xml") @EnableIntegration @EnableAsync From 3e8813266516fac047dea0ed67c30f347048776d Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 9 Jun 2021 17:51:03 +0200 Subject: [PATCH 46/59] Making tests less brittle --- .../instrument/rsocket/TraceRSocketTests.java | 750 +++++++++--------- 1 file changed, 374 insertions(+), 376 deletions(-) diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java index 0a2a2411f..e41ffcfa3 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java @@ -1,376 +1,374 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.rsocket; - -import java.net.URI; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingDeque; - -import io.rsocket.frame.FrameType; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.context.ContextView; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.TraceContext; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.exporter.FinishedSpan; -import org.springframework.cloud.sleuth.test.TestSpanHandler; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.rsocket.RSocketRequester; -import org.springframework.messaging.rsocket.RSocketRequester.Builder; -import org.springframework.messaging.rsocket.RSocketStrategies; -import org.springframework.stereotype.Controller; -import org.springframework.util.MimeType; - -import static org.assertj.core.api.BDDAssertions.then; - -public abstract class TraceRSocketTests { - - public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; - - @Test - public void should_instrument_responder() throws Exception { - // setup - ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) - .web(WebApplicationType.REACTIVE) - .properties("server.port=0", "spring.rsocket.server.transport=websocket", - "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", - "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", - "management.security.enabled=false") - .run(); - final TestSpanHandler spans = context.getBean(TestSpanHandler.class); - final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); - final TestController controller2 = context.getBean(TestController.class); - final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); - - final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); - - final RSocketRequester rSocketRequester = rsocketRequesterBuilder - .websocket(URI.create("ws://localhost:" + port + "/rsocket")); - - // REQUEST FNF - whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); - - FrameType receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST FNF - whenNonSampledRequestFnfIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - whenNonSampledRequestResponseIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - whenNonSampledRequestStreamIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - whenNonSampledRequestChannelIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // cleanup - context.close(); - } - - protected String expectedTraceId() { - return EXPECTED_TRACE_ID; - } - - protected String expectedSpanId() { - return EXPECTED_TRACE_ID; - } - - @Test - public void should_instrument_requester_and_responder() throws Exception { - // setup - ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) - .web(WebApplicationType.REACTIVE) - .properties("server.port=0", "spring.rsocket.server.transport=websocket", - "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", - "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", - "management.security.enabled=false") - .run(); - - final org.springframework.cloud.sleuth.Tracer tracer = context - .getBean(org.springframework.cloud.sleuth.Tracer.class); - final TestSpanHandler spans = context.getBean(TestSpanHandler.class); - final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); - final TestController controller2 = context.getBean(TestController.class); - - final Builder rsocketRequesterBuilder = context.getBean(Builder.class); - - final RSocketRequester rSocketRequester = rsocketRequesterBuilder - .websocket(URI.create("ws://localhost:" + port + "/rsocket")); - - // REQUEST FNF - final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); - whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) - .doFinally(signalType -> nextSpanFnf.end()).block(); - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); - whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) - .doFinally(signalType -> nextSpanRR.end()).block(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); - whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) - .doFinally(signalType -> nextSpanRS.end()).blockLast(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); - whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) - .doFinally(signalType -> nextSpanRC.end()).blockLast(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); - spans.clear(); - controller2.reset(); - - // cleanup - context.close(); - } - - protected abstract Class testConfiguration(); - - private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { - then(spans).hasSize(1); - // TODO: Preferred option would be : [api.c2.{name}] - FinishedSpan span = spans.get(0); - then(span.getName()).isEqualTo(frameType.name() + " " + path); - then(span.getTags()).containsEntry("messaging.controller.class", - "org.springframework.cloud.sleuth.instrument.rsocket.TraceRSocketTests$TestController"); - then(span.getTags()).containsKey("messaging.controller.method"); - } - - private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { - return requester.route(path).send(); - } - - private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { - return requester.route(path).retrieveMono(String.class); - } - - private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { - return requester.route(path).retrieveFlux(String.class); - } - - private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { - return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); - } - - private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { - requester.route("api.c2.fnf").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).send().block(); - } - - private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { - requester.route("api.c2.rr").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).retrieveMono(String.class).block(); - } - - private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { - requester.route("api.c2.rs").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).retrieveFlux(String.class).blockLast(); - } - - private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { - requester.route("api.c2.rc").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); - } - - private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { - // then(spans).isEmpty(); // FIXME: does not work for request case - then(controller2.getSpan()).isNotNull(); - then(controller2.getSpan().context().traceId()).isEqualTo(expectedTraceId); - } - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration - static class MyConfig { - - @Bean - TestController controller(Tracer tracer) { - return new TestController(tracer); - } - - } - - @Controller - @MessageMapping("api.c2") - static class TestController { - - final Tracer tracer; - - Span span; - - ContextView interceptedContext; - - BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); - - TestController(Tracer tracer) { - this.tracer = tracer; - } - - BlockingQueue getReceivedFrames() { - return this.receivedFrames; - } - - Span getSpan() { - return this.span; - } - - void reset() { - this.span = null; - } - - @MessageMapping("fnf") - Mono testFnf() { - - this.span = this.tracer.currentSpan(); - - return Mono.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_FNF); - return Mono.empty(); - }); - } - - @MessageMapping("rr") - Mono testRR() { - this.span = this.tracer.currentSpan(); - - return Mono.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_RESPONSE); - return Mono.just("response"); - }); - } - - @MessageMapping("rs") - Flux testRS() { - this.span = this.tracer.currentSpan(); - - return Flux.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_STREAM); - return Flux.just("stream"); - }); - } - - @MessageMapping("rc") - Flux testRC(@Payload Flux inbound) { - this.span = this.tracer.currentSpan(); - - return Flux.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_CHANNEL); - return inbound; - }); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import java.net.URI; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; + +import io.rsocket.frame.FrameType; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketRequester.Builder; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.stereotype.Controller; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.BDDAssertions.then; + +public abstract class TraceRSocketTests { + + public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; + + @Test + public void should_instrument_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); + + final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); + + FrameType receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST FNF + whenNonSampledRequestFnfIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenNonSampledRequestResponseIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenNonSampledRequestStreamIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenNonSampledRequestChannelIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + protected String expectedTraceId() { + return EXPECTED_TRACE_ID; + } + + protected String expectedSpanId() { + return EXPECTED_TRACE_ID; + } + + @Test + public void should_instrument_requester_and_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + + final org.springframework.cloud.sleuth.Tracer tracer = context + .getBean(org.springframework.cloud.sleuth.Tracer.class); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + + final Builder rsocketRequesterBuilder = context.getBean(Builder.class); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) + .doFinally(signalType -> nextSpanFnf.end()).block(); + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) + .doFinally(signalType -> nextSpanRR.end()).block(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) + .doFinally(signalType -> nextSpanRS.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) + .doFinally(signalType -> nextSpanRC.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + protected abstract Class testConfiguration(); + + private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { + String expectedName = frameType.name() + " " + path; + FinishedSpan span = spans.reportedSpans().stream().filter(finished -> expectedName.equals(finished.getName())).findFirst().orElseThrow(() -> new AssertionError("Span with name [" + expectedName + "] not found")); + then(span.getTags()).containsEntry("messaging.controller.class", + "org.springframework.cloud.sleuth.instrument.rsocket.TraceRSocketTests$TestController"); + then(span.getTags()).containsKey("messaging.controller.method"); + } + + private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { + return requester.route(path).send(); + } + + private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveMono(String.class); + } + + private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveFlux(String.class); + } + + private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { + return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); + } + + private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { + requester.route("api.c2.fnf").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).send().block(); + } + + private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { + requester.route("api.c2.rr").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveMono(String.class).block(); + } + + private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { + requester.route("api.c2.rs").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveFlux(String.class).blockLast(); + } + + private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { + requester.route("api.c2.rc").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); + } + + private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { + // then(spans).isEmpty(); // FIXME: does not work for request case + then(controller2.getSpan()).isNotNull(); + then(controller2.getSpan().context().traceId()).isEqualTo(expectedTraceId); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + static class MyConfig { + + @Bean + TestController controller(Tracer tracer) { + return new TestController(tracer); + } + + } + + @Controller + @MessageMapping("api.c2") + static class TestController { + + final Tracer tracer; + + Span span; + + ContextView interceptedContext; + + BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); + + TestController(Tracer tracer) { + this.tracer = tracer; + } + + BlockingQueue getReceivedFrames() { + return this.receivedFrames; + } + + Span getSpan() { + return this.span; + } + + void reset() { + this.span = null; + } + + @MessageMapping("fnf") + Mono testFnf() { + + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_FNF); + return Mono.empty(); + }); + } + + @MessageMapping("rr") + Mono testRR() { + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_RESPONSE); + return Mono.just("response"); + }); + } + + @MessageMapping("rs") + Flux testRS() { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_STREAM); + return Flux.just("stream"); + }); + } + + @MessageMapping("rc") + Flux testRC(@Payload Flux inbound) { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_CHANNEL); + return inbound; + }); + } + + } + +} From 17ba1b18d57496006b79508adcd509227fd745d6 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 10 Jun 2021 15:46:04 +0200 Subject: [PATCH 47/59] Adds Cassandra support (#1974) Co-authored-by: Mark Paluch --- docs/src/main/asciidoc/_configprops.adoc | 1 + docs/src/main/asciidoc/_spans.adoc | 25 +++ docs/src/main/asciidoc/integrations.adoc | 7 + .../cloud/sleuth/docs/AssertingSpan.java | 6 + .../sleuth/docs/AssertingSpanBuilder.java | 6 + spring-cloud-sleuth-autoconfigure/pom.xml | 10 + .../TraceCassandraAutoConfiguration.java | 60 ++++++ ...aceCassandraReactiveAutoConfiguration.java | 56 +++++ .../TraceCqlSessionBeanPostProcessor.java | 58 +++++ ...TraceReactiveSessionBeanPostProcessor.java | 57 +++++ ...itional-spring-configuration-metadata.json | 6 + .../main/resources/META-INF/spring.factories | 2 + ...eRpcAutoConfigurationIntegrationTests.java | 3 +- .../instrument/web/client/WebClientTests.java | 3 +- ...aceAsyncDefaultAutoConfigurationTests.java | 3 +- .../TraceCassandraAutoConfigurationTests.java | 55 +++++ ...ssandraReactiveAutoConfigurationTests.java | 51 +++++ .../TraceQuartzAutoConfigurationTest.java | 3 +- .../BraveWebClientAutoConfigurationTests.java | 3 +- .../instrument/web/client/GH846Tests.java | 3 +- .../client/TraceWebClientDisabledTests.java | 3 +- .../zipkin2/ZipkinSamplerTests.java | 3 +- .../src/test/resources/application.yml | 2 +- spring-cloud-sleuth-instrumentation/pom.xml | 10 + .../cassandra/CassandraSpanCustomizer.java | 35 +++ .../cassandra/CassandraSpanSupplier.java | 37 ++++ .../cassandra/SleuthCassandraSpan.java | 108 ++++++++++ .../instrument/cassandra/TraceCqlSession.java | 44 ++++ .../TraceCqlSessionBuilderCustomizer.java | 37 ++++ .../cassandra/TraceCqlSessionInterceptor.java | 166 +++++++++++++++ .../cassandra/TraceReactiveSession.java | 161 ++++++++++++++ .../cassandra/TraceRequestTracker.java | 135 ++++++++++++ .../instrument/cassandra/TraceStatement.java | 147 +++++++++++++ .../TraceListenerStrategySpanCustomizer.java | 1 + .../instrument/reactor/ReactorSleuth.java | 42 +++- .../servlet/HttpServletRequestWrapper.java | 6 +- .../servlet/HttpServletResponseWrapper.java | 3 +- ...TraceCqlSessionBuilderCustomizerTests.java | 38 ++++ .../TraceCqlSessionInterceptorTests.java | 108 ++++++++++ .../cassandra/TraceReactiveSessionTests.java | 199 ++++++++++++++++++ .../cassandra/TraceRequestTrackerTests.java | 194 +++++++++++++++++ .../tracer/SimpleCurrentTraceContext.java | 9 + .../cloud/sleuth/tracer/SimpleSpan.java | 11 + .../baggage/MultipleHopsIntegrationTests.java | 7 +- .../JmsTracingConfigurationTest.java | 7 +- .../issue_943/HelloSpringIntegration.java | 4 +- 46 files changed, 1916 insertions(+), 19 deletions(-) create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCqlSessionBeanPostProcessor.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceReactiveSessionBeanPostProcessor.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfigurationTests.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfigurationTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanCustomizer.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanSupplier.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/SleuthCassandraSpan.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSession.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizer.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptor.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSession.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTracker.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceStatement.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizerTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptorTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSessionTests.java create mode 100644 spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTrackerTests.java diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 3425dd37b..5f575c7e0 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -10,6 +10,7 @@ |spring.sleuth.baggage.remote-fields | | List of fields that are referenced the same in-process as it is on the wire. For example, the field "x-vcap-request-id" would be set as-is including the prefix. |spring.sleuth.baggage.tag-fields | | List of fields that should automatically become tags. |spring.sleuth.batch.enabled | `true` | Enable Spring Batch instrumentation. +|spring.sleuth.cassandra.enabled | `true` | Enable Cassandra instrumentation. |spring.sleuth.circuitbreaker.enabled | `true` | Enable Spring Cloud CircuitBreaker instrumentation. |spring.sleuth.config.server.enabled | `true` | Enable Spring Cloud Config Server instrumentation. |spring.sleuth.deployer.enabled | `true` | Enable Spring Cloud Deployer instrumentation. diff --git a/docs/src/main/asciidoc/_spans.adoc b/docs/src/main/asciidoc/_spans.adoc index 2ded3abf3..bca12b758 100644 --- a/docs/src/main/asciidoc/_spans.adoc +++ b/docs/src/main/asciidoc/_spans.adoc @@ -85,6 +85,31 @@ Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.in |batch.step.type|Type of the Spring Batch job. |=== +=== Cassandra Span + +> Span created around CqlSession executions. + +**Span name** `%s` - since it contains `%s`, the name is dynamic and will be resolved at runtime. + +Fully qualified name of the enclosing class `org.springframework.cloud.sleuth.instrument.cassandra.SleuthCassandraSpan` + +IMPORTANT: All tags and events must be prefixed with `cassandra.` prefix! + +.Tag Keys +|=== +|Name | Description +|cassandra.cql|A tag containing Cassandra CQL. +|cassandra.keyspace|Name of the Cassandra keyspace. +|cassandra.node[%s].error|A tag containing error that occurred for the given node. (since the name contains `%s` the final value will be resolved at runtime) +|=== + +.Event Values +|=== +|Name | Description +|cassandra.node.error|Set whenever an error occurred for the given node. +|cassandra.node.success|Set when a success occurred for the session processing. +|=== + === Circuit Breaker Function Span > Span created when we wrap a Function passed to the CircuitBreaker. as fallback. diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index a0d24f277..9e1cb711d 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -665,6 +665,13 @@ This feature is available for all tracer implementations. We're adding an instrumented Tomcat's `Valve` that originates the span. In order to disable this instrumentation set `spring.sleuth.web.tomcat.enabled` to `false`. +[[sleuth-cassandra-integration]] +== Spring Data Cassandra + +This feature is available for all tracer implementations. + +We're instrumenting Casandra's `CqlSession` and `ReactiveSession` interfaces and we're providing our own implementation of the `RequestTracker`. +In order to disable this instrumentation set `spring.sleuth.cassandra.enabled` to `false`. [[sleuth-jdbc-integration]] == Spring JDBC diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java index d4ad3b7ff..0f9f47f23 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java @@ -132,6 +132,12 @@ public interface AssertingSpan extends Span { return this; } + @Override + default Span remoteIpAndPort(String ip, int port) { + getDelegate().remoteIpAndPort(ip, port); + return this; + } + /** * @param documentedSpan span configuration * @param span span to wrap in assertions diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java index 0363928d2..de951bdc4 100644 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java @@ -93,6 +93,12 @@ public interface AssertingSpanBuilder extends Span.Builder { return this; } + @Override + default Span.Builder remoteIpAndPort(String ip, int port) { + getDelegate().remoteIpAndPort(ip, port); + return this; + } + @Override default AssertingSpanBuilder setParent(TraceContext context) { getDelegate().setParent(context); diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index de8c4d1b1..cecaa8ace 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -123,6 +123,16 @@ spring-cloud-starter-task true + + org.springframework.boot + spring-boot-starter-data-cassandra + true + + + org.springframework.boot + spring-boot-starter-data-cassandra-reactive + true + org.springframework.cloud spring-cloud-deployer-spi diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfiguration.java new file mode 100644 index 000000000..ca89e870a --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.cassandra.TraceCqlSessionBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Cassandra. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(Tracer.class) +@ConditionalOnProperty(value = "spring.sleuth.cassandra.enabled", matchIfMissing = true) +@AutoConfigureAfter(BraveAutoConfiguration.class) +@AutoConfigureBefore(CassandraAutoConfiguration.class) +@ConditionalOnClass(CqlSession.class) +public class TraceCassandraAutoConfiguration { + + @Bean + static TraceCqlSessionBeanPostProcessor traceCqlSessionBeanPostProcessor(BeanFactory beanFactory) { + return new TraceCqlSessionBeanPostProcessor(beanFactory); + } + + @Bean + TraceCqlSessionBuilderCustomizer traceCqlSessionBuilderCustomizer() { + return new TraceCqlSessionBuilderCustomizer(); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfiguration.java new file mode 100644 index 000000000..331fe53b4 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.cassandra; + +import reactor.core.publisher.Flux; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.ReactiveSession; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Cassandra. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(Tracer.class) +@ConditionalOnClass({ ReactiveSession.class, Flux.class }) +@ConditionalOnProperty(value = "spring.sleuth.cassandra.enabled", matchIfMissing = true) +@AutoConfigureAfter(BraveAutoConfiguration.class) +@AutoConfigureBefore({ CassandraAutoConfiguration.class, CassandraReactiveDataAutoConfiguration.class }) +public class TraceCassandraReactiveAutoConfiguration { + + @Bean + static TraceReactiveSessionBeanPostProcessor traceReactiveSessionBeanPostProcessor(BeanFactory beanFactory) { + return new TraceReactiveSessionBeanPostProcessor(beanFactory); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCqlSessionBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCqlSessionBeanPostProcessor.java new file mode 100644 index 000000000..26cb74abc --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCqlSessionBeanPostProcessor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.sleuth.instrument.cassandra.TraceCqlSession; + +/** + * {@link BeanPostProcessor} to wrap a {@link CqlSession} instance into its trace + * representation. + * + * @author Marcin Grzejszczak + * @author Mark Paluch + * @since 3.1.0 + */ +public class TraceCqlSessionBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + public TraceCqlSessionBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof CqlSession) { + return create((CqlSession) bean); + } + return bean; + } + + private CqlSession create(CqlSession session) { + return TraceCqlSession.create(session, this.beanFactory); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceReactiveSessionBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceReactiveSessionBeanPostProcessor.java new file mode 100644 index 000000000..ddf6e74d0 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceReactiveSessionBeanPostProcessor.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.cassandra; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.cloud.sleuth.instrument.cassandra.TraceReactiveSession; +import org.springframework.data.cassandra.ReactiveSession; + +/** + * {@link BeanPostProcessor} to wrap a + * {@link org.springframework.data.cassandra.ReactiveSession} instance into its trace + * representation. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceReactiveSessionBeanPostProcessor implements BeanPostProcessor { + + private final BeanFactory beanFactory; + + public TraceReactiveSessionBeanPostProcessor(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof ReactiveSession) { + return create((ReactiveSession) bean); + } + return bean; + } + + private ReactiveSession create(ReactiveSession session) { + return TraceReactiveSession.create(session, this.beanFactory); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 1cfec09f1..fac014bc6 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -191,6 +191,12 @@ "description": "Enable Spring Session instrumentation.", "defaultValue": true }, + { + "name": "spring.sleuth.cassandra.enabled", + "type": "java.lang.Boolean", + "description": "Enable Cassandra instrumentation.", + "defaultValue": true + }, { "name": "spring.sleuth.vault.enabled", "type": "java.lang.Boolean", diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index f859d669d..13decf094 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -6,6 +6,8 @@ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncAutoConfi org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncCustomAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncDefaultAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.batch.TraceBatchAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.cassandra.TraceCassandraAutoConfiguration,\ +org.springframework.cloud.sleuth.autoconfig.instrument.cassandra.TraceCassandraReactiveAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.config.TraceSpringCloudConfigAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.circuitbreaker.TraceCircuitBreakerAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.deployer.TraceDeployerAutoConfiguration,\ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java index 730c7a278..b245cfcaa 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/rpc/BraveRpcAutoConfigurationIntegrationTests.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -63,7 +64,7 @@ public class BraveRpcAutoConfigurationIntegrationTests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) @Configuration(proxyBeanMethods = false) public static class Config { diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java index c5fcf7908..5d07ef4ad 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/instrument/web/client/WebClientTests.java @@ -41,6 +41,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; @@ -152,7 +153,7 @@ public class WebClientTests { @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, - MongoAutoConfiguration.class, MongoDataAutoConfiguration.class }) + CassandraAutoConfiguration.class, MongoAutoConfiguration.class, MongoDataAutoConfiguration.class }) @DisableSecurity public static class TestConfiguration { diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java index d96ce17c4..ca1dce937 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/async/TraceAsyncDefaultAutoConfigurationTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -54,7 +55,7 @@ public class TraceAsyncDefaultAutoConfigurationTests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) static class Config { @Bean diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfigurationTests.java new file mode 100644 index 000000000..bcf141da1 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraAutoConfigurationTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.cloud.sleuth.instrument.cassandra.TraceCqlSessionBuilderCustomizer; + +class TraceCassandraAutoConfigurationTests { + + ApplicationContextRunner runner = new ApplicationContextRunner() + .withPropertyValues("spring.sleuth.noop.enabled=true").withConfiguration( + AutoConfigurations.of(TraceNoOpAutoConfiguration.class, TraceCassandraAutoConfiguration.class)); + + @Test + void should_register_cassandra_tracing_beans() { + runner.run(context -> BDDAssertions.then(context).hasSingleBean(TraceCqlSessionBeanPostProcessor.class) + .hasSingleBean(TraceCqlSessionBuilderCustomizer.class)); + } + + @Test + void should_not_register_cassandra_tracing_beans_when_cassandra_not_present() { + runner.withClassLoader(new FilteredClassLoader(CqlSession.class)) + .run(context -> BDDAssertions.then(context).doesNotHaveBean(TraceCqlSessionBeanPostProcessor.class) + .doesNotHaveBean(TraceCqlSessionBuilderCustomizer.class)); + } + + @Test + void should_not_register_cassandra_tracing_beans_when_cassandra_tracing_disabled() { + runner.withPropertyValues("spring.sleuth.cassandra.enabled=false") + .run(context -> BDDAssertions.then(context).doesNotHaveBean(TraceCqlSessionBeanPostProcessor.class) + .doesNotHaveBean(TraceCqlSessionBuilderCustomizer.class)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfigurationTests.java new file mode 100644 index 000000000..68b77b955 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/cassandra/TraceCassandraReactiveAutoConfigurationTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.instrument.cassandra; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.sleuth.autoconfig.TraceNoOpAutoConfiguration; +import org.springframework.data.cassandra.ReactiveSession; + +class TraceCassandraReactiveAutoConfigurationTests { + + ApplicationContextRunner runner = new ApplicationContextRunner() + .withPropertyValues("spring.sleuth.noop.enabled=true").withConfiguration(AutoConfigurations + .of(TraceNoOpAutoConfiguration.class, TraceCassandraReactiveAutoConfiguration.class)); + + @Test + void should_register_cassandra_tracing_beans() { + runner.run(context -> BDDAssertions.then(context).hasSingleBean(TraceReactiveSessionBeanPostProcessor.class)); + } + + @Test + void should_not_register_cassandra_tracing_beans_when_cassandra_not_present() { + runner.withClassLoader(new FilteredClassLoader(ReactiveSession.class)).run( + context -> BDDAssertions.then(context).doesNotHaveBean(TraceReactiveSessionBeanPostProcessor.class)); + } + + @Test + void should_not_register_cassandra_tracing_beans_when_cassandra_tracing_disabled() { + runner.withPropertyValues("spring.sleuth.cassandra.enabled=false").run( + context -> BDDAssertions.then(context).doesNotHaveBean(TraceReactiveSessionBeanPostProcessor.class)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java index 8ce3abc66..7265b4e5c 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/quartz/TraceQuartzAutoConfigurationTest.java @@ -26,6 +26,7 @@ import org.springframework.boot.actuate.autoconfigure.security.servlet.Managemen import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -120,7 +121,7 @@ public class TraceQuartzAutoConfigurationTest { exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }, + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }, excludeName = "org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration") public static class EnableAutoConfig { diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java index 016442269..2e0c9f37a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/BraveWebClientAutoConfigurationTests.java @@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -124,7 +125,7 @@ public class BraveWebClientAutoConfigurationTests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) static class Config { // custom builder diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java index 0801a617d..900b3c447 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/GH846Tests.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -58,7 +59,7 @@ public class GH846Tests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) static class App { @Bean diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java index 07c6b8e7b..35c3d536a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/web/client/TraceWebClientDisabledTests.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -47,7 +48,7 @@ public class TraceWebClientDisabledTests { @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) public static class Config { } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java index b3061be84..34103f05a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/zipkin2/ZipkinSamplerTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; @@ -58,7 +59,7 @@ public class ZipkinSamplerTests { exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, - R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }, + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }, excludeName = "org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration") static class TestConfig { diff --git a/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml b/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml index e5b38daa4..151a645e7 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml +++ b/spring-cloud-sleuth-autoconfigure/src/test/resources/application.yml @@ -1,3 +1,3 @@ logging.level.org.springframework.cloud: DEBUG -spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration, org.springframework.cloud.gateway.config.GatewayAutoConfiguration, org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration, org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration, org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration, org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration, org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration, org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration, org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration +spring.autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration, org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration, org.springframework.cloud.gateway.config.GatewayAutoConfiguration, org.springframework.cloud.gateway.config.GatewayClassPathWarningAutoConfiguration, org.springframework.cloud.gateway.config.GatewayMetricsAutoConfiguration, org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration, org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration, org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration, org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration, org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration, org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration, org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index fd7a5aa4d..4af8bedbc 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -167,6 +167,16 @@ spring-cloud-starter-task true + + org.springframework.boot + spring-boot-starter-data-cassandra + true + + + org.springframework.boot + spring-boot-starter-data-cassandra-reactive + true + org.springframework.cloud spring-cloud-deployer-spi diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanCustomizer.java new file mode 100644 index 000000000..6d7b98d14 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanCustomizer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.cassandra; + +/** + * Provides additional customization for a Cassandra span. Used internally - do not + * implement. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface CassandraSpanCustomizer { + + /** + * Provides additional customization for a Cassandra span. + * @param defaultName default name of the span + */ + void customizeSpan(String defaultName); + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanSupplier.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanSupplier.java new file mode 100644 index 000000000..ab5115a78 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/CassandraSpanSupplier.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.cassandra; + +import org.springframework.cloud.sleuth.Span; + +/** + * Returns the Cassandra span. + * + * Used internally - do not implement. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface CassandraSpanSupplier { + + /** + * @return span + */ + Span getSpan(); + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/SleuthCassandraSpan.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/SleuthCassandraSpan.java new file mode 100644 index 000000000..d0752df51 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/SleuthCassandraSpan.java @@ -0,0 +1,108 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.cassandra; + +import org.springframework.cloud.sleuth.docs.DocumentedSpan; +import org.springframework.cloud.sleuth.docs.EventValue; +import org.springframework.cloud.sleuth.docs.TagKey; + +enum SleuthCassandraSpan implements DocumentedSpan { + + /** + * Span created around CqlSession executions. + */ + CASSANDRA_SPAN { + @Override + public String getName() { + return "%s"; + } + + @Override + public TagKey[] getTagKeys() { + return Tags.values(); + } + + @Override + public EventValue[] getEvents() { + return Events.values(); + } + + @Override + public String prefix() { + return "cassandra."; + } + }; + + enum Tags implements TagKey { + + /** + * Name of the Cassandra keyspace. + */ + KEYSPACE_NAME { + @Override + public String getKey() { + return "cassandra.keyspace"; + } + }, + + /** + * A tag containing error that occurred for the given node. + */ + NODE_ERROR_TAG { + @Override + public String getKey() { + return "cassandra.node[%s].error"; + } + }, + + /** + * A tag containing Cassandra CQL. + */ + CQL_TAG { + @Override + public String getKey() { + return "cassandra.cql"; + } + }, + + } + + enum Events implements EventValue { + + /** + * Set whenever an error occurred for the given node. + */ + NODE_ERROR { + @Override + public String getValue() { + return "cassandra.node.error"; + } + }, + + /** + * Set when a success occurred for the session processing. + */ + NODE_SUCCESS { + @Override + public String getValue() { + return "cassandra.node.success"; + } + } + + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSession.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSession.java new file mode 100644 index 000000000..0c1f0f48a --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSession.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.cassandra; + +import com.datastax.oss.driver.api.core.CqlSession; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanFactory; + +/** + * Factory to create a {@link TraceCqlSession}. + * + * @author Mark Paluch + * @since 3.1.0 + */ +public final class TraceCqlSession { + + private TraceCqlSession() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + public static CqlSession create(CqlSession session, BeanFactory beanFactory) { + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setTarget(session); + proxyFactory.addAdvice(new TraceCqlSessionInterceptor(session, beanFactory)); + proxyFactory.addInterface(CqlSession.class); + return (CqlSession) proxyFactory.getProxy(); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizer.java new file mode 100644 index 000000000..2420d27c8 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.cassandra; + +import com.datastax.oss.driver.api.core.CqlSessionBuilder; + +import org.springframework.boot.autoconfigure.cassandra.CqlSessionBuilderCustomizer; + +/** + * Adds tracing mechanism to the {@link CqlSessionBuilder}. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceCqlSessionBuilderCustomizer implements CqlSessionBuilderCustomizer { + + @Override + public void customize(CqlSessionBuilder cqlSessionBuilder) { + cqlSessionBuilder.withRequestTracker(TraceRequestTracker.INSTANCE); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptor.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptor.java new file mode 100644 index 000000000..d06972e37 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptor.java @@ -0,0 +1,166 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.cassandra; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; +import org.springframework.cloud.sleuth.internal.ContextUtil; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +/** + * A {@link MethodInterceptor} that wraps calls around {@link CqlSession} in a trace + * representation. This interceptor wraps statements for {@code execute} and + * {@code prepare} (including their asynchronous variants) only. Graph and reactive + * {@link CqlSession} method remain called as-is. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +class TraceCqlSessionInterceptor implements MethodInterceptor { + + private static final Log log = LogFactory.getLog(TraceCqlSessionInterceptor.class); + + private final CqlSession delegate; + + private final BeanFactory beanFactory; + + private Tracer tracer; + + private CurrentTraceContext currentTraceContext; + + TraceCqlSessionInterceptor(CqlSession delegate, BeanFactory beanFactory) { + this.delegate = delegate; + this.beanFactory = beanFactory; + } + + @Nullable + @Override + public Object invoke(@NonNull MethodInvocation invocation) throws Throwable { + Method method = invocation.getMethod(); + Object[] args = invocation.getArguments(); + if (isContextUnusable()) { + return invocation.proceed(); + } + if (method.getName().equals("execute")) { + if (args.length > 0) { + return tracedCall(createStatement(args), "execute", this.delegate::execute); + } + } + if (method.getName().equals("executeAsync")) { + if (args.length > 0) { + return tracedCall(createStatement(args), "executeAsync", this.delegate::executeAsync); + } + } + if (method.getName().equals("prepare")) { + if (args.length > 0) { + return tracedCall(createStatement(args), "prepare", + statement -> this.delegate.prepare((SimpleStatement) statement)); + } + } + if (method.getName().equals("prepareAsync")) { + if (args.length > 0) { + return tracedCall(createStatement(args), "prepareAsync", + statement -> this.delegate.prepareAsync((SimpleStatement) statement)); + } + } + return invocation.proceed(); + } + + private static Statement createStatement(Object[] args) { + if (args[0] instanceof Statement) { + return (Statement) (args[0]); + } + else if (args[0] instanceof String && args.length == 1) { + return SimpleStatement.newInstance((String) args[0]); + } + else if (args[0] instanceof String && args.length == 2) { + String query = (String) args[0]; + return args[1] instanceof Map ? SimpleStatement.newInstance(query, (Map) args[1]) + : SimpleStatement.newInstance(query, (Object[]) args[1]); + } + throw new IllegalArgumentException(String.format("Unsupported arguments %s", Arrays.toString(args))); + } + + boolean isContextUnusable() { + return ContextUtil.isContextUnusable(this.beanFactory); + } + + private Object tracedCall(Statement statement, String defaultSpanName, Function, Object> function) { + Span span = cassandraClientSpan(); + Statement proxied = TraceStatement.isTraceStatement(statement) ? statement + : TraceStatement.createProxy(span, statement); + ((CassandraSpanCustomizer) proxied).customizeSpan(defaultSpanName); + try (CurrentTraceContext.Scope ws = currentTraceContext().maybeScope(span.context())) { + log.debug("Will execute statement"); + return function.apply(proxied); + } + } + + private Span cassandraClientSpan() { + return cassandraClientSpan(tracer().spanBuilder(), getSessionName(), getKeyspace()); + } + + static Span cassandraClientSpan(Span.Builder builder, String sessionName, Optional keyspace) { + return AssertingSpanBuilder.of(SleuthCassandraSpan.CASSANDRA_SPAN, builder).kind(Span.Kind.CLIENT) + .remoteServiceName("cassandra-" + sessionName) + .tag(SleuthCassandraSpan.Tags.KEYSPACE_NAME, keyspace.map(CqlIdentifier::asInternal).orElse("unknown")) + .start(); + } + + String getSessionName() { + return this.delegate.getContext().getSessionName(); + } + + Optional getKeyspace() { + return this.delegate.getKeyspace(); + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = this.beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + + private CurrentTraceContext currentTraceContext() { + if (this.currentTraceContext == null) { + this.currentTraceContext = this.beanFactory.getBean(CurrentTraceContext.class); + } + return this.currentTraceContext; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSession.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSession.java new file mode 100644 index 000000000..dd94e0ec3 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSession.java @@ -0,0 +1,161 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.cassandra; + +import java.util.Map; +import java.util.Optional; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; +import org.springframework.data.cassandra.ReactiveResultSet; +import org.springframework.data.cassandra.ReactiveSession; + +/** + * Tracing variant of {@link ReactiveSession}. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class TraceReactiveSession implements ReactiveSession { + + private final ReactiveSession delegate; + + private final BeanFactory beanFactory; + + private Tracer tracer; + + private CurrentTraceContext currentTraceContext; + + TraceReactiveSession(ReactiveSession delegate, BeanFactory beanFactory) { + this.delegate = delegate; + this.beanFactory = beanFactory; + } + + /** + * Factory method for creation of a {@link TraceReactiveSession}. + * @param session reactive session + * @param beanFactory bean factory + * @return traced representation of a {@link ReactiveSession}. + */ + public static ReactiveSession create(ReactiveSession session, BeanFactory beanFactory) { + return new TraceReactiveSession(session, beanFactory); + } + + @Override + public boolean isClosed() { + return this.delegate.isClosed(); + } + + @Override + public DriverContext getContext() { + return this.delegate.getContext(); + } + + @Override + public Mono execute(String cql) { + return execute(SimpleStatement.newInstance(cql)); + } + + @Override + public Mono execute(String cql, Object... objects) { + return execute(SimpleStatement.newInstance(cql, objects)); + } + + @Override + public Mono execute(String cql, Map map) { + return execute(SimpleStatement.newInstance(cql, map)); + } + + @Override + public Mono execute(Statement statement) { + return Mono.deferContextual(contextView -> { + Span span = ReactorSleuth.spanFromContext(tracer(), currentTraceContext(), contextView); + return this.delegate.execute(proxiedStatement(span, statement, "execute")); + }).contextWrite(context -> ReactorSleuth.putSpanInScope(tracer(), context, createSpan(context))); + } + + @Override + public Mono prepare(String cql) { + return prepare(SimpleStatement.newInstance(cql)); + } + + @Override + public Mono prepare(SimpleStatement statement) { + return Mono.deferContextual(contextView -> { + Span span = ReactorSleuth.spanFromContext(tracer(), currentTraceContext(), contextView); + return this.delegate.prepare((SimpleStatement) proxiedStatement(span, statement, "prepare")); + }).contextWrite(context -> ReactorSleuth.putSpanInScope(tracer(), context, createSpan(context))); + } + + private Statement proxiedStatement(Span span, Statement statement, String defaultName) { + Statement proxied = TraceStatement.createProxy(span, statement); + ((CassandraSpanCustomizer) proxied).customizeSpan(defaultName); + return proxied; + } + + @Override + public void close() { + this.delegate.close(); + } + + private Span createSpan(ContextView contextView) { + return TraceCqlSessionInterceptor.cassandraClientSpan(spanBuilder(contextView), getContext().getSessionName(), + Optional.empty() /* todo @since 3.2.2 */); + } + + private Span.Builder spanBuilder(ContextView contextView) { + Span.Builder spanBuilder = tracer().spanBuilder(); + if (contextView.hasKey(TraceContext.class)) { + return spanBuilder.setParent(contextView.get(TraceContext.class)); + } + else if (contextView.hasKey(Span.class)) { + return spanBuilder.setParent(contextView.get(Span.class).context()); + } + Span span = tracer().currentSpan(); + if (span != null) { + return spanBuilder.setParent(span.context()); + } + return spanBuilder; + } + + private CurrentTraceContext currentTraceContext() { + if (this.currentTraceContext == null) { + this.currentTraceContext = beanFactory.getBean(CurrentTraceContext.class); + } + return this.currentTraceContext; + } + + private Tracer tracer() { + if (this.tracer == null) { + this.tracer = beanFactory.getBean(Tracer.class); + } + return this.tracer; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTracker.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTracker.java new file mode 100644 index 000000000..5b32056ce --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTracker.java @@ -0,0 +1,135 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.cassandra; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.docs.AssertingSpan; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +import static org.springframework.cloud.sleuth.instrument.cassandra.SleuthCassandraSpan.CASSANDRA_SPAN; +import static org.springframework.cloud.sleuth.instrument.cassandra.SleuthCassandraSpan.Events.NODE_ERROR; +import static org.springframework.cloud.sleuth.instrument.cassandra.SleuthCassandraSpan.Events.NODE_SUCCESS; +import static org.springframework.cloud.sleuth.instrument.cassandra.SleuthCassandraSpan.Tags.NODE_ERROR_TAG; + +/** + * Trace implementation of the {@link RequestTracker}. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +enum TraceRequestTracker implements RequestTracker { + + INSTANCE; + + private static final Log log = LogFactory.getLog(TraceRequestTracker.class); + + @Override + public void onSuccess(@NonNull Request request, long latencyNanos, @NonNull DriverExecutionProfile executionProfile, + @NonNull Node node, @NonNull String requestLogPrefix) { + if (request instanceof CassandraSpanSupplier) { + Span span = ((CassandraSpanSupplier) request).getSpan(); + if (log.isDebugEnabled()) { + log.debug("Closing span [" + span + "]"); + } + span.end(); + } + } + + @Override + public void onError(@NonNull Request request, @NonNull Throwable error, long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, @Nullable Node node, @NonNull String requestLogPrefix) { + if (request instanceof CassandraSpanSupplier) { + Span span = ((CassandraSpanSupplier) request).getSpan(); + span.error(error); + if (log.isDebugEnabled()) { + log.debug("Closing span [" + span + "]"); + } + span.end(); + } + } + + @Override + public void onNodeError(@NonNull Request request, @NonNull Throwable error, long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, @NonNull Node node, @NonNull String requestLogPrefix) { + if (request instanceof CassandraSpanSupplier) { + AssertingSpan span = AssertingSpan.of(CASSANDRA_SPAN, ((CassandraSpanSupplier) request).getSpan()); + span.event(NODE_ERROR); + span.tag(String.format(NODE_ERROR_TAG.getKey(), node.getEndPoint()), error.toString()); + tryAddingRemoteIpAndPort(node, span); + if (log.isDebugEnabled()) { + log.debug("Marking node error for [" + span + "]"); + } + } + } + + @Override + public void onNodeSuccess(@NonNull Request request, long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, @NonNull Node node, @NonNull String requestLogPrefix) { + if (request instanceof CassandraSpanSupplier) { + AssertingSpan span = AssertingSpan.of(CASSANDRA_SPAN, ((CassandraSpanSupplier) request).getSpan()); + span.event(NODE_SUCCESS); + tryAddingRemoteIpAndPort(node, span); + if (log.isDebugEnabled()) { + log.debug("Marking node success for [" + span + "]"); + } + } + } + + @Override + public void onSessionReady(@NonNull Session session) { + } + + @Override + public void close() throws Exception { + + } + + private void tryAddingRemoteIpAndPort(Node node, Span span) { + try { + SocketAddress socketAddress = node.getEndPoint().resolve(); + String host; + int port; + if (socketAddress instanceof InetSocketAddress) { + InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + host = inetSocketAddress.getHostString(); + port = inetSocketAddress.getPort(); + } + else { + host = socketAddress.toString(); + port = 0; + } + span.remoteIpAndPort(host, port); + } + catch (Exception e) { + log.debug("Exception occurred while trying to set ip and port", e); + } + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceStatement.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceStatement.java new file mode 100644 index 000000000..91a036878 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceStatement.java @@ -0,0 +1,147 @@ +/* + * Copyright 2018-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.cassandra; + +import java.util.StringJoiner; + +import javax.annotation.Nonnull; + +import com.datastax.oss.driver.api.core.cql.BatchStatement; +import com.datastax.oss.driver.api.core.cql.BatchableStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.docs.AssertingSpan; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; + +/** + * Trace implementation of a {@link Statement}. + * + * @author Mark Paluch + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +final class TraceStatement implements MethodInterceptor { + + private final Span span; + + private Statement delegate; + + private TraceStatement(Span span, Statement delegate) { + this.span = span; + this.delegate = delegate; + } + + /** + * Creates a proxy with {@link TraceStatement} attached to it. + * @param span current span + * @param target target to proxy + * @param type of target + * @return proxied object with trace advice + */ + @SuppressWarnings("unchecked") + public static T createProxy(Span span, T target) { + ProxyFactory factory = new ProxyFactory(ClassUtils.getAllInterfaces(target)); + factory.addInterface(CassandraSpanCustomizer.class); + factory.addInterface(CassandraSpanSupplier.class); + factory.setTarget(target); + factory.addAdvice(new TraceStatement(span, (Statement) target)); + return (T) factory.getProxy(); + } + + /** + * @param statement target statement to inspect + * @return whether the given {@code statement} is a traced statement wrapper + */ + public static boolean isTraceStatement(Statement statement) { + return statement instanceof CassandraSpanCustomizer && statement instanceof CassandraSpanSupplier; + } + + /** + * @return stored span + */ + public Span getSpan() { + return this.span; + } + + /** + * Tries to parse the CQL query or provides the default name. + * @param defaultName if there's not query + * @return span name + */ + public String getSpanName(String defaultName) { + String query = getCql(); + if (query.indexOf(' ') > -1) { + return query.substring(0, query.indexOf(' ')); + } + return defaultName; + } + + private String getCql() { + String query = ""; + if (this.delegate instanceof SimpleStatement) { + query = getQuery(this.delegate); + } + else if (this.delegate instanceof BoundStatement) { + query = getQuery(this.delegate); + } + else if (this.delegate instanceof BatchStatement) { + StringJoiner joiner = new StringJoiner(";"); + for (BatchableStatement bs : (BatchStatement) this.delegate) { + joiner.add(getQuery(bs)); + } + query = joiner.toString(); + } + return query; + } + + private static String getQuery(Statement statement) { + if (statement instanceof SimpleStatement) { + return ((SimpleStatement) statement).getQuery(); + } + else if (statement instanceof BoundStatement) { + return ((BoundStatement) statement).getPreparedStatement().getQuery(); + } + return ""; + } + + @Nullable + @Override + public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable { + if (invocation.getMethod().getName().equals("getSpan")) { + return this.span; + } + if (invocation.getMethod().getName().equals("customizeSpan")) { + AssertingSpan.of(SleuthCassandraSpan.CASSANDRA_SPAN, this.span) + .name(getSpanName((String) invocation.getArguments()[0])) + .tag(SleuthCassandraSpan.Tags.CQL_TAG, getCql()); + return null; + } + Object result = invocation.proceed(); + if (result instanceof Statement) { + this.delegate = (Statement) result; + } + return result; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java index 52fe0ae6b..4e592d673 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.java @@ -30,6 +30,7 @@ public interface TraceListenerStrategySpanCustomizer /** * Customizes the client database span. + * @param dataSource data source for which we're building the span * @param spanBuilder span builder */ void customizeConnectionSpan(T dataSource, Span.Builder spanBuilder); diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java index 8b5e99e3f..3ed8f2faf 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorSleuth.java @@ -35,6 +35,7 @@ import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; import reactor.util.context.Context; +import reactor.util.context.ContextView; import org.springframework.cloud.sleuth.CurrentTraceContext; import org.springframework.cloud.sleuth.Span; @@ -544,6 +545,7 @@ public abstract class ReactorSleuth { * Updates the Reactor context with tracing information. Creates a new span if there * is no current span. Creates a child span if there was an entry in the context * already. + * @param tracer tracer * @param currentTraceContext current trace context * @param context Reactor context * @param childSpanName child span name when there is no span in context @@ -572,11 +574,49 @@ public abstract class ReactorSleuth { return putSpanInScope(tracer, context, span); } - private static Context putSpanInScope(Tracer tracer, Context context, Span span) { + /** + * Puts the provided span in scope and in Reactor context. + * @param tracer tracer + * @param context Reactor context + * @param span span to put in Reactor context + * @return mutated context + */ + public static Context putSpanInScope(Tracer tracer, Context context, Span span) { return context.put(Span.class, span).put(TraceContext.class, span.context()).put(Tracer.SpanInScope.class, tracer.withSpan(span)); } + /** + * Retrieves span from Reactor context. + * @param tracer tracer + * @param currentTraceContext current trace context + * @param context context view + * @return span from Reactor context or creates a new one if missing + */ + public static Span spanFromContext(Tracer tracer, CurrentTraceContext currentTraceContext, ContextView context) { + Span span = context.getOrDefault(Span.class, null); + if (span != null) { + if (log.isDebugEnabled()) { + log.debug("Found a span in reactor context [" + span + "]"); + } + return span; + } + TraceContext traceContext = context.getOrDefault(TraceContext.class, null); + if (traceContext != null) { + try (CurrentTraceContext.Scope scope = currentTraceContext.maybeScope(traceContext)) { + if (log.isDebugEnabled()) { + log.debug("Found a trace context in reactor context [" + traceContext + "]"); + } + return tracer.currentSpan(); + } + } + Span newSpan = tracer.nextSpan().start(); + if (log.isDebugEnabled()) { + log.debug("No span was found - will create a new one [" + newSpan + "]"); + } + return newSpan; + } + } class SleuthContextOperator implements Subscription, CoreSubscriber, Scannable { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletRequestWrapper.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletRequestWrapper.java index 8fe1bc235..a692c6d5f 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletRequestWrapper.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletRequestWrapper.java @@ -33,7 +33,11 @@ import org.springframework.lang.Nullable; */ public class HttpServletRequestWrapper implements HttpServerRequest { - /** @since 5.10 */ + /** + * Wraps the request in a tracing representation. + * @param request http request + * @return wrapped request + */ public static HttpServerRequest create(HttpServletRequest request) { return new HttpServletRequestWrapper(request); } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletResponseWrapper.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletResponseWrapper.java index 3d70cbd80..de4e30b86 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletResponseWrapper.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/web/servlet/HttpServletResponseWrapper.java @@ -36,8 +36,9 @@ public class HttpServletResponseWrapper implements HttpServerResponse { // not final for inner // subtype /** + * Returns the trace representation of a response. * @param caught an exception caught serving the request. - * @since 5.10 + * @return wrapped response */ public static HttpServerResponse create(@Nullable HttpServletRequest request, HttpServletResponse response, @Nullable Throwable caught) { diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizerTests.java new file mode 100644 index 000000000..ab5f3725a --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionBuilderCustomizerTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.cassandra; + +import com.datastax.oss.driver.api.core.CqlSessionBuilder; +import org.junit.jupiter.api.Test; + +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +class TraceCqlSessionBuilderCustomizerTests { + + @Test + void should_register_trace_request_tracker() { + TraceCqlSessionBuilderCustomizer customizer = new TraceCqlSessionBuilderCustomizer(); + CqlSessionBuilder builder = mock(CqlSessionBuilder.class); + + customizer.customize(builder); + + then(builder).should().withRequestTracker(isA(TraceRequestTracker.class)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptorTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptorTests.java new file mode 100644 index 000000000..fcba0a9b0 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceCqlSessionInterceptorTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.cassandra; + +import java.util.Optional; +import java.util.function.BiConsumer; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.AsyncCqlSession; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.SyncCqlSession; +import org.junit.jupiter.api.Test; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.cloud.sleuth.tracer.SimpleCurrentTraceContext; +import org.springframework.cloud.sleuth.tracer.SimpleSpan; +import org.springframework.cloud.sleuth.tracer.SimpleTracer; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +class TraceCqlSessionInterceptorTests { + + SimpleTracer simpleTracer = new SimpleTracer(); + + SimpleCurrentTraceContext simpleCurrentTraceContext = SimpleCurrentTraceContext.withTracer(this.simpleTracer); + + BeanFactory beanFactory = beanFactory(); + + @Test + void should_register_a_span_for_execute() { + assertThatTracingWorks(SyncCqlSession::execute); + } + + @Test + void should_register_a_span_for_executeAsync() { + assertThatTracingWorks(AsyncCqlSession::executeAsync); + } + + @Test + void should_register_a_span_for_prepare() { + assertThatTracingWorks(SyncCqlSession::prepare); + } + + private void assertThatTracingWorks(BiConsumer consumer) { + CqlSession session = proxied(mock(CqlSession.class)); + SimpleStatement statement = mock(SimpleStatement.class); + given(statement.getQuery()) + .willReturn("Insert into University.Student(RollNo,Name,dept,Semester) values(2,'Michael','CS', 2);"); + + consumer.accept(session, statement); + + SimpleSpan span = this.simpleTracer.getLastSpan(); + then(span).isNotNull(); + then(span.tags).containsKeys(SleuthCassandraSpan.Tags.CQL_TAG.getKey(), + SleuthCassandraSpan.Tags.KEYSPACE_NAME.getKey()); + then(span.remoteServiceName).isNotBlank(); + } + + private BeanFactory beanFactory() { + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); + beanFactory.addBean("tracer", this.simpleTracer); + beanFactory.addBean("currentTraceContext", this.simpleCurrentTraceContext); + return beanFactory; + } + + private CqlSession proxied(CqlSession session) { + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setTarget(session); + proxyFactory.addAdvice(new TraceCqlSessionInterceptor(session, this.beanFactory) { + @Override + boolean isContextUnusable() { + return false; + } + + @Override + String getSessionName() { + return "test"; + } + + @Override + Optional getKeyspace() { + return Optional.empty(); + } + }); + proxyFactory.addInterface(CqlSession.class); + return (CqlSession) proxyFactory.getProxy(); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSessionTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSessionTests.java new file mode 100644 index 000000000..20f6f44f1 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceReactiveSessionTests.java @@ -0,0 +1,199 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.cassandra; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; +import com.datastax.oss.driver.api.core.auth.AuthProvider; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.throttling.RequestThrottler; +import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy; +import com.datastax.oss.driver.api.core.ssl.SslEngineFactory; +import com.datastax.oss.driver.api.core.time.TimestampGenerator; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.cloud.sleuth.tracer.SimpleCurrentTraceContext; +import org.springframework.cloud.sleuth.tracer.SimpleSpan; +import org.springframework.cloud.sleuth.tracer.SimpleTracer; +import org.springframework.data.cassandra.ReactiveSession; +import org.springframework.lang.NonNull; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +class TraceReactiveSessionTests { + + SimpleTracer simpleTracer = new SimpleTracer(); + + SimpleCurrentTraceContext simpleCurrentTraceContext = SimpleCurrentTraceContext.withTracer(this.simpleTracer); + + @Test + void should_register_a_span_for_execute() { + ReactiveSession session = mock(ReactiveSession.class); + given(session.execute(any(Statement.class))).willReturn(Mono.empty()); + SimpleStatement statement = mock(SimpleStatement.class); + given(statement.getQuery()) + .willReturn("Insert into University.Student(RollNo,Name,dept,Semester) values(2,'Michael','CS', 2);"); + + new TraceReactiveSession(session, beanFactory()) { + @Override + public DriverContext getContext() { + return driverContext(); + } + }.execute(statement).block(Duration.ofMillis(10)); + + SimpleSpan span = this.simpleTracer.getLastSpan(); + then(span).isNotNull(); + then(span.tags).containsKeys(SleuthCassandraSpan.Tags.CQL_TAG.getKey(), + SleuthCassandraSpan.Tags.KEYSPACE_NAME.getKey()); + then(span.remoteServiceName).isNotBlank(); + } + + private BeanFactory beanFactory() { + StaticListableBeanFactory beanFactory = new StaticListableBeanFactory(); + beanFactory.addBean("tracer", this.simpleTracer); + beanFactory.addBean("currentTraceContext", this.simpleCurrentTraceContext); + return beanFactory; + } + + private DriverContext driverContext() { + return new DriverContext() { + @NonNull + @Override + public String getSessionName() { + return "session"; + } + + @NonNull + @Override + public DriverConfig getConfig() { + return null; + } + + @NonNull + @Override + public DriverConfigLoader getConfigLoader() { + return null; + } + + @NonNull + @Override + public Map getLoadBalancingPolicies() { + return null; + } + + @NonNull + @Override + public Map getRetryPolicies() { + return null; + } + + @NonNull + @Override + public Map getSpeculativeExecutionPolicies() { + return null; + } + + @NonNull + @Override + public TimestampGenerator getTimestampGenerator() { + return null; + } + + @NonNull + @Override + public ReconnectionPolicy getReconnectionPolicy() { + return null; + } + + @NonNull + @Override + public AddressTranslator getAddressTranslator() { + return null; + } + + @NonNull + @Override + public Optional getAuthProvider() { + return Optional.empty(); + } + + @NonNull + @Override + public Optional getSslEngineFactory() { + return Optional.empty(); + } + + @NonNull + @Override + public RequestTracker getRequestTracker() { + return null; + } + + @NonNull + @Override + public RequestThrottler getRequestThrottler() { + return null; + } + + @NonNull + @Override + public NodeStateListener getNodeStateListener() { + return null; + } + + @NonNull + @Override + public SchemaChangeListener getSchemaChangeListener() { + return null; + } + + @NonNull + @Override + public ProtocolVersion getProtocolVersion() { + return null; + } + + @NonNull + @Override + public CodecRegistry getCodecRegistry() { + return null; + } + }; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTrackerTests.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTrackerTests.java new file mode 100644 index 000000000..ada2dcc3c --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/cassandra/TraceRequestTrackerTests.java @@ -0,0 +1,194 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.cassandra; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Map; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.session.Request; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.tracer.SimpleSpan; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.BDDAssertions.then; + +class TraceRequestTrackerTests { + + @Test + void should_end_span_on_success() { + TraceRequestTracker traceRequestTracker = TraceRequestTracker.INSTANCE; + SimpleSpan span = new SimpleSpan(); + + traceRequestTracker.onSuccess(new MyRequest(span), 1L, BDDMockito.mock(DriverExecutionProfile.class), + BDDMockito.mock(Node.class), ""); + + then(span.ended).isTrue(); + } + + @Test + void should_end_span_on_error() { + TraceRequestTracker traceRequestTracker = TraceRequestTracker.INSTANCE; + SimpleSpan span = new SimpleSpan(); + + traceRequestTracker.onError(new MyRequest(span), new IllegalStateException("Foo"), 1L, + BDDMockito.mock(DriverExecutionProfile.class), BDDMockito.mock(Node.class), ""); + + then(span.ended).isTrue(); + then(span.throwable).isNotNull(); + } + + @Test + void should_customize_span_on_node_error() { + TraceRequestTracker traceRequestTracker = TraceRequestTracker.INSTANCE; + SimpleSpan span = new SimpleSpan(); + Node node = BDDMockito.mock(Node.class); + BDDMockito.given(node.getEndPoint()).willReturn(endpoint()); + + traceRequestTracker.onNodeError(new MyRequest(span), new IllegalStateException("Foo"), 1L, + BDDMockito.mock(DriverExecutionProfile.class), node, ""); + + then(span.events).contains(SleuthCassandraSpan.Events.NODE_ERROR.getValue()); + then(span.tags).containsEntry("cassandra.node[localhost/127.0.0.1:1234].error", + "java.lang.IllegalStateException: Foo"); + then(span.ip).isNotEmpty(); + then(span.port).isPositive(); + } + + @Test + void should_customize_span_on_node_success() { + TraceRequestTracker traceRequestTracker = TraceRequestTracker.INSTANCE; + SimpleSpan span = new SimpleSpan(); + Node node = BDDMockito.mock(Node.class); + BDDMockito.given(node.getEndPoint()).willReturn(endpoint()); + + traceRequestTracker.onNodeSuccess(new MyRequest(span), 1L, BDDMockito.mock(DriverExecutionProfile.class), node, + ""); + + then(span.events).contains(SleuthCassandraSpan.Events.NODE_SUCCESS.getValue()); + then(span.ip).isNotEmpty(); + then(span.port).isPositive(); + } + + private EndPoint endpoint() { + return new EndPoint() { + @NonNull + @Override + public SocketAddress resolve() { + return new InetSocketAddress("localhost", 1234); + } + + @NonNull + @Override + public String asMetricPrefix() { + return ""; + } + + @Override + public String toString() { + return resolve().toString(); + } + }; + } + +} + +class MyRequest implements Request, CassandraSpanSupplier { + + private final Span span; + + MyRequest(Span span) { + this.span = span; + } + + @Nullable + @Override + public String getExecutionProfileName() { + return null; + } + + @Nullable + @Override + public DriverExecutionProfile getExecutionProfile() { + return null; + } + + @Nullable + @Override + public CqlIdentifier getKeyspace() { + return null; + } + + @Nullable + @Override + public CqlIdentifier getRoutingKeyspace() { + return null; + } + + @Nullable + @Override + public ByteBuffer getRoutingKey() { + return null; + } + + @Nullable + @Override + public Token getRoutingToken() { + return null; + } + + @NonNull + @Override + public Map getCustomPayload() { + return null; + } + + @Nullable + @Override + public Boolean isIdempotent() { + return null; + } + + @Nullable + @Override + public Duration getTimeout() { + return null; + } + + @Nullable + @Override + public Node getNode() { + return null; + } + + @Override + public Span getSpan() { + return this.span; + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleCurrentTraceContext.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleCurrentTraceContext.java index 6c171a1d2..0c3acd5f9 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleCurrentTraceContext.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleCurrentTraceContext.java @@ -72,4 +72,13 @@ public class SimpleCurrentTraceContext implements CurrentTraceContext { return delegate; } + public static SimpleCurrentTraceContext withTracer(SimpleTracer simpleTracer) { + return new SimpleCurrentTraceContext() { + @Override + public TraceContext context() { + return simpleTracer.currentSpan() != null ? simpleTracer.currentSpan().context() : null; + } + }; + } + } diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java index c4213fd4b..7df1f7035 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/SimpleSpan.java @@ -48,6 +48,10 @@ public class SimpleSpan implements Span { public String name; + public String ip; + + public int port; + @Override public boolean isNoop() { return true; @@ -88,6 +92,13 @@ public class SimpleSpan implements Span { return this; } + @Override + public Span remoteIpAndPort(String ip, int port) { + this.ip = ip; + this.port = port; + return this; + } + @Override public void end() { this.ended = true; diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java index 300ff616a..8292a235f 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-baggage-tests/src/test/java/org/springframework/cloud/sleuth/brave/baggage/MultipleHopsIntegrationTests.java @@ -21,6 +21,7 @@ import brave.baggage.BaggagePropagationConfig; import brave.sampler.Sampler; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration; @@ -75,9 +76,9 @@ public class MultipleHopsIntegrationTests } @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration( - exclude = { MongoAutoConfiguration.class, QuartzAutoConfiguration.class, JmxAutoConfiguration.class, - R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + @EnableAutoConfiguration(exclude = { MongoAutoConfiguration.class, QuartzAutoConfiguration.class, + JmxAutoConfiguration.class, R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, + RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) static class Config { @Bean diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java index f4a41ed5c..d6912f647 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/JmsTracingConfigurationTest.java @@ -35,6 +35,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration; @@ -169,9 +170,9 @@ public class JmsTracingConfigurationTest { } @Configuration(proxyBeanMethods = false) -@EnableAutoConfiguration( - exclude = { KafkaAutoConfiguration.class, MongoAutoConfiguration.class, QuartzAutoConfiguration.class, - R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) +@EnableAutoConfiguration(exclude = { KafkaAutoConfiguration.class, MongoAutoConfiguration.class, + QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, + RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) class JmsTestTracingConfiguration { } diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java index a51599cfd..5e449776b 100644 --- a/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java +++ b/tests/brave/spring-cloud-sleuth-instrumentation-messaging-tests/src/test/java/org/springframework/cloud/sleuth/instrument/messaging/issues/issue_943/HelloSpringIntegration.java @@ -22,6 +22,7 @@ import brave.test.TestSpanHandler; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; @@ -34,7 +35,8 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.web.client.RestTemplate; @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class, - R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class }) + R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, + CassandraAutoConfiguration.class }) @ImportResource("classpath:beans/applicationContext.xml") @EnableIntegration @EnableAsync From ca59f9429bbb8a8d9bf71b89078fe784c9cb9da8 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 10 Jun 2021 15:53:41 +0200 Subject: [PATCH 48/59] Polish --- .../instrument/rsocket/TraceRSocketTests.java | 749 +++++++++--------- 1 file changed, 375 insertions(+), 374 deletions(-) diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java index e41ffcfa3..ef66fcff3 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java @@ -1,374 +1,375 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.rsocket; - -import java.net.URI; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingDeque; - -import io.rsocket.frame.FrameType; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.context.ContextView; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.TraceContext; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.exporter.FinishedSpan; -import org.springframework.cloud.sleuth.test.TestSpanHandler; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.rsocket.RSocketRequester; -import org.springframework.messaging.rsocket.RSocketRequester.Builder; -import org.springframework.messaging.rsocket.RSocketStrategies; -import org.springframework.stereotype.Controller; -import org.springframework.util.MimeType; - -import static org.assertj.core.api.BDDAssertions.then; - -public abstract class TraceRSocketTests { - - public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; - - @Test - public void should_instrument_responder() throws Exception { - // setup - ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) - .web(WebApplicationType.REACTIVE) - .properties("server.port=0", "spring.rsocket.server.transport=websocket", - "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", - "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", - "management.security.enabled=false") - .run(); - final TestSpanHandler spans = context.getBean(TestSpanHandler.class); - final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); - final TestController controller2 = context.getBean(TestController.class); - final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); - - final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); - - final RSocketRequester rSocketRequester = rsocketRequesterBuilder - .websocket(URI.create("ws://localhost:" + port + "/rsocket")); - - // REQUEST FNF - whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); - - FrameType receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST FNF - whenNonSampledRequestFnfIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - whenNonSampledRequestResponseIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - whenNonSampledRequestStreamIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - whenNonSampledRequestChannelIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // cleanup - context.close(); - } - - protected String expectedTraceId() { - return EXPECTED_TRACE_ID; - } - - protected String expectedSpanId() { - return EXPECTED_TRACE_ID; - } - - @Test - public void should_instrument_requester_and_responder() throws Exception { - // setup - ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) - .web(WebApplicationType.REACTIVE) - .properties("server.port=0", "spring.rsocket.server.transport=websocket", - "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", - "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", - "management.security.enabled=false") - .run(); - - final org.springframework.cloud.sleuth.Tracer tracer = context - .getBean(org.springframework.cloud.sleuth.Tracer.class); - final TestSpanHandler spans = context.getBean(TestSpanHandler.class); - final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); - final TestController controller2 = context.getBean(TestController.class); - - final Builder rsocketRequesterBuilder = context.getBean(Builder.class); - - final RSocketRequester rSocketRequester = rsocketRequesterBuilder - .websocket(URI.create("ws://localhost:" + port + "/rsocket")); - - // REQUEST FNF - final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); - whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) - .doFinally(signalType -> nextSpanFnf.end()).block(); - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); - whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) - .doFinally(signalType -> nextSpanRR.end()).block(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); - whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) - .doFinally(signalType -> nextSpanRS.end()).blockLast(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); - whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) - .doFinally(signalType -> nextSpanRC.end()).blockLast(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); - spans.clear(); - controller2.reset(); - - // cleanup - context.close(); - } - - protected abstract Class testConfiguration(); - - private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { - String expectedName = frameType.name() + " " + path; - FinishedSpan span = spans.reportedSpans().stream().filter(finished -> expectedName.equals(finished.getName())).findFirst().orElseThrow(() -> new AssertionError("Span with name [" + expectedName + "] not found")); - then(span.getTags()).containsEntry("messaging.controller.class", - "org.springframework.cloud.sleuth.instrument.rsocket.TraceRSocketTests$TestController"); - then(span.getTags()).containsKey("messaging.controller.method"); - } - - private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { - return requester.route(path).send(); - } - - private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { - return requester.route(path).retrieveMono(String.class); - } - - private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { - return requester.route(path).retrieveFlux(String.class); - } - - private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { - return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); - } - - private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { - requester.route("api.c2.fnf").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).send().block(); - } - - private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { - requester.route("api.c2.rr").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).retrieveMono(String.class).block(); - } - - private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { - requester.route("api.c2.rs").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).retrieveFlux(String.class).blockLast(); - } - - private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { - requester.route("api.c2.rc").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); - } - - private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { - // then(spans).isEmpty(); // FIXME: does not work for request case - then(controller2.getSpan()).isNotNull(); - then(controller2.getSpan().context().traceId()).isEqualTo(expectedTraceId); - } - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration - static class MyConfig { - - @Bean - TestController controller(Tracer tracer) { - return new TestController(tracer); - } - - } - - @Controller - @MessageMapping("api.c2") - static class TestController { - - final Tracer tracer; - - Span span; - - ContextView interceptedContext; - - BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); - - TestController(Tracer tracer) { - this.tracer = tracer; - } - - BlockingQueue getReceivedFrames() { - return this.receivedFrames; - } - - Span getSpan() { - return this.span; - } - - void reset() { - this.span = null; - } - - @MessageMapping("fnf") - Mono testFnf() { - - this.span = this.tracer.currentSpan(); - - return Mono.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_FNF); - return Mono.empty(); - }); - } - - @MessageMapping("rr") - Mono testRR() { - this.span = this.tracer.currentSpan(); - - return Mono.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_RESPONSE); - return Mono.just("response"); - }); - } - - @MessageMapping("rs") - Flux testRS() { - this.span = this.tracer.currentSpan(); - - return Flux.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_STREAM); - return Flux.just("stream"); - }); - } - - @MessageMapping("rc") - Flux testRC(@Payload Flux inbound) { - this.span = this.tracer.currentSpan(); - - return Flux.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_CHANNEL); - return inbound; - }); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import java.net.URI; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; + +import io.rsocket.frame.FrameType; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketRequester.Builder; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.stereotype.Controller; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.BDDAssertions.then; + +public abstract class TraceRSocketTests { + + public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; + + @Test + public void should_instrument_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); + + final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); + + FrameType receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST FNF + whenNonSampledRequestFnfIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenNonSampledRequestResponseIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenNonSampledRequestStreamIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenNonSampledRequestChannelIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + protected String expectedTraceId() { + return EXPECTED_TRACE_ID; + } + + protected String expectedSpanId() { + return EXPECTED_TRACE_ID; + } + + @Test + public void should_instrument_requester_and_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + + final org.springframework.cloud.sleuth.Tracer tracer = context + .getBean(org.springframework.cloud.sleuth.Tracer.class); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + + final Builder rsocketRequesterBuilder = context.getBean(Builder.class); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) + .doFinally(signalType -> nextSpanFnf.end()).block(); + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) + .doFinally(signalType -> nextSpanRR.end()).block(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) + .doFinally(signalType -> nextSpanRS.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) + .doFinally(signalType -> nextSpanRC.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + protected abstract Class testConfiguration(); + + private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { + String expectedName = frameType.name() + " " + path; + FinishedSpan span = spans.reportedSpans().stream().filter(finished -> expectedName.equals(finished.getName())) + .findFirst().orElseThrow(() -> new AssertionError("Span with name [" + expectedName + "] not found")); + then(span.getTags()).containsEntry("messaging.controller.class", + "org.springframework.cloud.sleuth.instrument.rsocket.TraceRSocketTests$TestController"); + then(span.getTags()).containsKey("messaging.controller.method"); + } + + private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { + return requester.route(path).send(); + } + + private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveMono(String.class); + } + + private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveFlux(String.class); + } + + private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { + return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); + } + + private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { + requester.route("api.c2.fnf").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).send().block(); + } + + private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { + requester.route("api.c2.rr").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveMono(String.class).block(); + } + + private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { + requester.route("api.c2.rs").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveFlux(String.class).blockLast(); + } + + private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { + requester.route("api.c2.rc").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); + } + + private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { + // then(spans).isEmpty(); // FIXME: does not work for request case + then(controller2.getSpan()).isNotNull(); + then(controller2.getSpan().context().traceId()).isEqualTo(expectedTraceId); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + static class MyConfig { + + @Bean + TestController controller(Tracer tracer) { + return new TestController(tracer); + } + + } + + @Controller + @MessageMapping("api.c2") + static class TestController { + + final Tracer tracer; + + Span span; + + ContextView interceptedContext; + + BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); + + TestController(Tracer tracer) { + this.tracer = tracer; + } + + BlockingQueue getReceivedFrames() { + return this.receivedFrames; + } + + Span getSpan() { + return this.span; + } + + void reset() { + this.span = null; + } + + @MessageMapping("fnf") + Mono testFnf() { + + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_FNF); + return Mono.empty(); + }); + } + + @MessageMapping("rr") + Mono testRR() { + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_RESPONSE); + return Mono.just("response"); + }); + } + + @MessageMapping("rs") + Flux testRS() { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_STREAM); + return Flux.just("stream"); + }); + } + + @MessageMapping("rc") + Flux testRC(@Payload Flux inbound) { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_CHANNEL); + return inbound; + }); + } + + } + +} From 9b580e2672d7947e1956ec612abf7876f527ed0e Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 10 Jun 2021 17:17:39 +0200 Subject: [PATCH 49/59] Made tests less brittle --- .../instrument/rsocket/TraceRSocketTests.java | 755 +++++++++--------- 1 file changed, 380 insertions(+), 375 deletions(-) diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java index ef66fcff3..176281fef 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java @@ -1,375 +1,380 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.rsocket; - -import java.net.URI; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingDeque; - -import io.rsocket.frame.FrameType; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.context.ContextView; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.TraceContext; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.exporter.FinishedSpan; -import org.springframework.cloud.sleuth.test.TestSpanHandler; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.rsocket.RSocketRequester; -import org.springframework.messaging.rsocket.RSocketRequester.Builder; -import org.springframework.messaging.rsocket.RSocketStrategies; -import org.springframework.stereotype.Controller; -import org.springframework.util.MimeType; - -import static org.assertj.core.api.BDDAssertions.then; - -public abstract class TraceRSocketTests { - - public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; - - @Test - public void should_instrument_responder() throws Exception { - // setup - ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) - .web(WebApplicationType.REACTIVE) - .properties("server.port=0", "spring.rsocket.server.transport=websocket", - "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", - "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", - "management.security.enabled=false") - .run(); - final TestSpanHandler spans = context.getBean(TestSpanHandler.class); - final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); - final TestController controller2 = context.getBean(TestController.class); - final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); - - final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); - - final RSocketRequester rSocketRequester = rsocketRequesterBuilder - .websocket(URI.create("ws://localhost:" + port + "/rsocket")); - - // REQUEST FNF - whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); - - FrameType receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST FNF - whenNonSampledRequestFnfIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - whenNonSampledRequestResponseIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - whenNonSampledRequestStreamIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - whenNonSampledRequestChannelIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // cleanup - context.close(); - } - - protected String expectedTraceId() { - return EXPECTED_TRACE_ID; - } - - protected String expectedSpanId() { - return EXPECTED_TRACE_ID; - } - - @Test - public void should_instrument_requester_and_responder() throws Exception { - // setup - ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) - .web(WebApplicationType.REACTIVE) - .properties("server.port=0", "spring.rsocket.server.transport=websocket", - "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", - "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", - "management.security.enabled=false") - .run(); - - final org.springframework.cloud.sleuth.Tracer tracer = context - .getBean(org.springframework.cloud.sleuth.Tracer.class); - final TestSpanHandler spans = context.getBean(TestSpanHandler.class); - final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); - final TestController controller2 = context.getBean(TestController.class); - - final Builder rsocketRequesterBuilder = context.getBean(Builder.class); - - final RSocketRequester rSocketRequester = rsocketRequesterBuilder - .websocket(URI.create("ws://localhost:" + port + "/rsocket")); - - // REQUEST FNF - final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); - whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) - .doFinally(signalType -> nextSpanFnf.end()).block(); - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); - whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) - .doFinally(signalType -> nextSpanRR.end()).block(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); - whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) - .doFinally(signalType -> nextSpanRS.end()).blockLast(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); - whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) - .doFinally(signalType -> nextSpanRC.end()).blockLast(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); - spans.clear(); - controller2.reset(); - - // cleanup - context.close(); - } - - protected abstract Class testConfiguration(); - - private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { - String expectedName = frameType.name() + " " + path; - FinishedSpan span = spans.reportedSpans().stream().filter(finished -> expectedName.equals(finished.getName())) - .findFirst().orElseThrow(() -> new AssertionError("Span with name [" + expectedName + "] not found")); - then(span.getTags()).containsEntry("messaging.controller.class", - "org.springframework.cloud.sleuth.instrument.rsocket.TraceRSocketTests$TestController"); - then(span.getTags()).containsKey("messaging.controller.method"); - } - - private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { - return requester.route(path).send(); - } - - private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { - return requester.route(path).retrieveMono(String.class); - } - - private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { - return requester.route(path).retrieveFlux(String.class); - } - - private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { - return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); - } - - private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { - requester.route("api.c2.fnf").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).send().block(); - } - - private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { - requester.route("api.c2.rr").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).retrieveMono(String.class).block(); - } - - private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { - requester.route("api.c2.rs").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).retrieveFlux(String.class).blockLast(); - } - - private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { - requester.route("api.c2.rc").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); - } - - private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { - // then(spans).isEmpty(); // FIXME: does not work for request case - then(controller2.getSpan()).isNotNull(); - then(controller2.getSpan().context().traceId()).isEqualTo(expectedTraceId); - } - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration - static class MyConfig { - - @Bean - TestController controller(Tracer tracer) { - return new TestController(tracer); - } - - } - - @Controller - @MessageMapping("api.c2") - static class TestController { - - final Tracer tracer; - - Span span; - - ContextView interceptedContext; - - BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); - - TestController(Tracer tracer) { - this.tracer = tracer; - } - - BlockingQueue getReceivedFrames() { - return this.receivedFrames; - } - - Span getSpan() { - return this.span; - } - - void reset() { - this.span = null; - } - - @MessageMapping("fnf") - Mono testFnf() { - - this.span = this.tracer.currentSpan(); - - return Mono.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_FNF); - return Mono.empty(); - }); - } - - @MessageMapping("rr") - Mono testRR() { - this.span = this.tracer.currentSpan(); - - return Mono.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_RESPONSE); - return Mono.just("response"); - }); - } - - @MessageMapping("rs") - Flux testRS() { - this.span = this.tracer.currentSpan(); - - return Flux.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_STREAM); - return Flux.just("stream"); - }); - } - - @MessageMapping("rc") - Flux testRC(@Payload Flux inbound) { - this.span = this.tracer.currentSpan(); - - return Flux.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_CHANNEL); - return inbound; - }); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import java.net.URI; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; + +import io.rsocket.frame.FrameType; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketRequester.Builder; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.stereotype.Controller; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.BDDAssertions.then; + +public abstract class TraceRSocketTests { + + public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; + + @Test + public void should_instrument_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); + + final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); + + FrameType receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST FNF + whenNonSampledRequestFnfIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenNonSampledRequestResponseIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenNonSampledRequestStreamIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenNonSampledRequestChannelIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + protected String expectedTraceId() { + return EXPECTED_TRACE_ID; + } + + protected String expectedSpanId() { + return EXPECTED_TRACE_ID; + } + + @Test + public void should_instrument_requester_and_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + + final org.springframework.cloud.sleuth.Tracer tracer = context + .getBean(org.springframework.cloud.sleuth.Tracer.class); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + + final Builder rsocketRequesterBuilder = context.getBean(Builder.class); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) + .doFinally(signalType -> nextSpanFnf.end()).block(); + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) + .doFinally(signalType -> nextSpanRR.end()).block(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) + .doFinally(signalType -> nextSpanRS.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) + .doFinally(signalType -> nextSpanRC.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + protected abstract Class testConfiguration(); + + private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { + String expectedName = frameType.name() + " " + path; + Awaitility.await().untilAsserted(() -> { + FinishedSpan span = spans.reportedSpans().stream().filter(finished -> expectedName.equals(finished.getName())) + .findFirst().orElseThrow(() -> new AssertionError("Span with name [" + expectedName + "] not found")); + then(span.getTags()).containsEntry("messaging.controller.class", + "org.springframework.cloud.sleuth.instrument.rsocket.TraceRSocketTests$TestController"); + then(span.getTags()).containsKey("messaging.controller.method"); + }); + } + + private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { + return requester.route(path).send(); + } + + private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveMono(String.class); + } + + private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveFlux(String.class); + } + + private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { + return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); + } + + private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { + requester.route("api.c2.fnf").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).send().block(); + } + + private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { + requester.route("api.c2.rr").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveMono(String.class).block(); + } + + private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { + requester.route("api.c2.rs").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveFlux(String.class).blockLast(); + } + + private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { + requester.route("api.c2.rc").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); + } + + private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { + // then(spans).isEmpty(); // FIXME: does not work for request case + Awaitility.await().untilAsserted(() -> { + then(controller2.getSpan()).isNotNull(); + then(controller2.getSpan().context().traceId()).isEqualTo(expectedTraceId); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + static class MyConfig { + + @Bean + TestController controller(Tracer tracer) { + return new TestController(tracer); + } + + } + + @Controller + @MessageMapping("api.c2") + static class TestController { + + final Tracer tracer; + + Span span; + + ContextView interceptedContext; + + BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); + + TestController(Tracer tracer) { + this.tracer = tracer; + } + + BlockingQueue getReceivedFrames() { + return this.receivedFrames; + } + + Span getSpan() { + return this.span; + } + + void reset() { + this.span = null; + } + + @MessageMapping("fnf") + Mono testFnf() { + + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_FNF); + return Mono.empty(); + }); + } + + @MessageMapping("rr") + Mono testRR() { + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_RESPONSE); + return Mono.just("response"); + }); + } + + @MessageMapping("rs") + Flux testRS() { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_STREAM); + return Flux.just("stream"); + }); + } + + @MessageMapping("rc") + Flux testRC(@Payload Flux inbound) { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_CHANNEL); + return inbound; + }); + } + + } + +} From 5bc8a29fe1a39e7b9d836a89f912c8d71ae94a53 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 10 Jun 2021 17:31:23 +0200 Subject: [PATCH 50/59] Updating Build to 3.1.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 00da89234..deb14f596 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ org.springframework.cloud spring-cloud-build - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT @@ -62,7 +62,7 @@ 1.8 1.8 1.8 - 3.0.3-SNAPSHOT + 3.1.0-SNAPSHOT 3.0.3-SNAPSHOT 3.0.3-SNAPSHOT 3.0.3-SNAPSHOT From 87ff5ca92b706ac1baacae8cab3ac8e0dd4af2ee Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 10 Jun 2021 17:31:48 +0200 Subject: [PATCH 51/59] Updating Build to 3.1.0-SNAPSHOT --- spring-cloud-sleuth-dependencies/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-sleuth-dependencies/pom.xml b/spring-cloud-sleuth-dependencies/pom.xml index 696cbadf2..b8899a8d0 100644 --- a/spring-cloud-sleuth-dependencies/pom.xml +++ b/spring-cloud-sleuth-dependencies/pom.xml @@ -22,7 +22,7 @@ spring-cloud-dependencies-parent org.springframework.cloud - 3.0.2 + 3.1.0-SNAPSHOT spring-cloud-sleuth-dependencies From 2493b176e539ebc54c72be95285f7e9e0f5e4c4d Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 16 Jun 2021 12:48:43 +0200 Subject: [PATCH 52/59] Actuator endpoint with spans (#1964) fixes #1879 --- docs/src/main/asciidoc/index.adoc | 19 +- docs/src/main/asciidoc/index.htmladoc | 19 +- docs/src/main/asciidoc/index.htmlsingleadoc | 1 - docs/src/main/asciidoc/index.pdfadoc | 14 +- docs/src/main/asciidoc/project-features.adoc | 9 + pom.xml | 14 +- .../cloud/sleuth/exporter/SpanReporter.java | 33 + spring-cloud-sleuth-autoconfigure/pom.xml | 1 - .../actuate/BraveFinishedSpanWriter.java | 46 ++ .../actuate/BufferingSpanReporter.java | 89 ++ .../actuate/FinishedSpanWriter.java | 40 + .../actuate/SleuthActuatorProperties.java | 43 + .../autoconfig/actuate/TextOutputFormat.java | 52 ++ .../TraceSleuthActuatorAutoConfiguration.java | 87 ++ .../actuate/TracesScrapeEndpoint.java | 71 ++ .../brave/BraveBridgeConfiguration.java | 10 +- .../main/resources/META-INF/spring.factories | 1 + .../actuate/BraveFinishedSpanWriterTests.java | 59 ++ .../actuate/BufferingSpanReporterTests.java | 42 + ...eSleuthActuatorAutoConfigurationTests.java | 48 ++ .../actuate/TracesScrapeEndpointTests.java | 54 ++ .../brave/bridge/CompositeSpanHandler.java | 21 +- .../cloud/sleuth/internal/ContextUtil.java | 2 +- tests/brave/pom.xml | 1 + .../pom.xml | 84 ++ ...atorAutoConfigurationIntegrationTests.java | 53 ++ .../src/test/resources/application.yml | 1 + ...atorAutoConfigurationIntegrationTests.java | 120 +++ .../instrument/rsocket/TraceRSocketTests.java | 761 +++++++++--------- 29 files changed, 1393 insertions(+), 402 deletions(-) mode change 120000 => 100644 docs/src/main/asciidoc/index.adoc mode change 120000 => 100644 docs/src/main/asciidoc/index.htmladoc delete mode 120000 docs/src/main/asciidoc/index.htmlsingleadoc mode change 120000 => 100644 docs/src/main/asciidoc/index.pdfadoc create mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/exporter/SpanReporter.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/BraveFinishedSpanWriter.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/BufferingSpanReporter.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/FinishedSpanWriter.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/SleuthActuatorProperties.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TextOutputFormat.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfiguration.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TracesScrapeEndpoint.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/BraveFinishedSpanWriterTests.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/BufferingSpanReporterTests.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationTests.java create mode 100644 spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TracesScrapeEndpointTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-actuator-tests/pom.xml create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-actuator-tests/src/test/java/org/springframework/cloud/sleuth/brave/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationIntegrationTests.java create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-actuator-tests/src/test/resources/application.yml create mode 100644 tests/common/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationIntegrationTests.java diff --git a/docs/src/main/asciidoc/index.adoc b/docs/src/main/asciidoc/index.adoc deleted file mode 120000 index 2ab5e96e8..000000000 --- a/docs/src/main/asciidoc/index.adoc +++ /dev/null @@ -1 +0,0 @@ -spring-cloud-sleuth.adoc \ No newline at end of file diff --git a/docs/src/main/asciidoc/index.adoc b/docs/src/main/asciidoc/index.adoc new file mode 100644 index 000000000..d34261b40 --- /dev/null +++ b/docs/src/main/asciidoc/index.adoc @@ -0,0 +1,18 @@ +[[spring-cloud-sleuth-reference-documentation]] += Spring Cloud Sleuth Reference Documentation +Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant + +:docinfo: shared +include::_attributes.adoc[] + +The reference documentation consists of the following sections: + +[horizontal] +<> :: Legal information. +<> :: About the Documentation, Getting Help, First Steps, and more. +<> :: Introducing {project-full-name}, Developing Your First {project-full-name}-based Application +<> :: {project-full-name} usage examples and workflows. +<> :: Span creation, context propagation, and more. +<> :: Add sampling, propagate remote tags, and more. +<> :: Instrumentation configuration, context propagation, and more. +<> :: Span definitions and configuration properties. diff --git a/docs/src/main/asciidoc/index.htmladoc b/docs/src/main/asciidoc/index.htmladoc deleted file mode 120000 index 2ab5e96e8..000000000 --- a/docs/src/main/asciidoc/index.htmladoc +++ /dev/null @@ -1 +0,0 @@ -spring-cloud-sleuth.adoc \ No newline at end of file diff --git a/docs/src/main/asciidoc/index.htmladoc b/docs/src/main/asciidoc/index.htmladoc new file mode 100644 index 000000000..d34261b40 --- /dev/null +++ b/docs/src/main/asciidoc/index.htmladoc @@ -0,0 +1,18 @@ +[[spring-cloud-sleuth-reference-documentation]] += Spring Cloud Sleuth Reference Documentation +Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant + +:docinfo: shared +include::_attributes.adoc[] + +The reference documentation consists of the following sections: + +[horizontal] +<> :: Legal information. +<> :: About the Documentation, Getting Help, First Steps, and more. +<> :: Introducing {project-full-name}, Developing Your First {project-full-name}-based Application +<> :: {project-full-name} usage examples and workflows. +<> :: Span creation, context propagation, and more. +<> :: Add sampling, propagate remote tags, and more. +<> :: Instrumentation configuration, context propagation, and more. +<> :: Span definitions and configuration properties. diff --git a/docs/src/main/asciidoc/index.htmlsingleadoc b/docs/src/main/asciidoc/index.htmlsingleadoc deleted file mode 120000 index f8d93d10b..000000000 --- a/docs/src/main/asciidoc/index.htmlsingleadoc +++ /dev/null @@ -1 +0,0 @@ -spring-cloud-sleuth.htmlsingleadoc \ No newline at end of file diff --git a/docs/src/main/asciidoc/index.pdfadoc b/docs/src/main/asciidoc/index.pdfadoc deleted file mode 120000 index d4beca5b9..000000000 --- a/docs/src/main/asciidoc/index.pdfadoc +++ /dev/null @@ -1 +0,0 @@ -spring-cloud-sleuth.pdfadoc \ No newline at end of file diff --git a/docs/src/main/asciidoc/index.pdfadoc b/docs/src/main/asciidoc/index.pdfadoc new file mode 100644 index 000000000..e1edcb891 --- /dev/null +++ b/docs/src/main/asciidoc/index.pdfadoc @@ -0,0 +1,13 @@ +[[spring-cloud-sleuth-reference-documentation]] += Spring Cloud Sleuth Reference Documentation +Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant + +include::_attributes.adoc[] + +include::legal.adoc[leveloffset=+1] +include::getting-started.adoc[leveloffset=+1] +include::using.adoc[leveloffset=+1] +include::project-features.adoc[leveloffset=+1] +include::howto.adoc[leveloffset=+1] +include::integrations.adoc[leveloffset=+1] +include::appendix.adoc[leveloffset=+1] diff --git a/docs/src/main/asciidoc/project-features.adoc b/docs/src/main/asciidoc/project-features.adoc index 4379349b2..f871283cd 100644 --- a/docs/src/main/asciidoc/project-features.adoc +++ b/docs/src/main/asciidoc/project-features.adoc @@ -485,6 +485,15 @@ Otherwise, your custom logback file does not properly read the property. A declarative format of representing span configuration was introduced via the `DocumentedSpan` abstraction. By analyzing Sleuth's source code an appendix with all span characteristics is created (including allowed tag keys and event names). You can check the <> for more information. +[[features-traces-actuator]] +== Traces Actuator Endpoint + +Spring Cloud Sleuth comes with a `traces` Actuator endpoint that can store finished spans. The endpoint can be queried either via an HTTP Get method to simply retrieve the list of stored spans or via HTTP Post method to retrieve the list and clear it. + +The size of the queue where the spans are stored can be configured via the `management.endpoint.traces.queue-size` property. + +Please read the https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#actuator[Spring Boot Actuator: Production-ready Features] section of the documentation to read more about the Actuator endpoints configuration options. + [[features-whats-next]] == What to Read Next diff --git a/pom.xml b/pom.xml index deb14f596..087ddb99a 100644 --- a/pom.xml +++ b/pom.xml @@ -63,14 +63,14 @@ 1.8 1.8 3.1.0-SNAPSHOT - 3.0.3-SNAPSHOT - 3.0.3-SNAPSHOT - 3.0.3-SNAPSHOT - 2.0.2-SNAPSHOT + 3.0.4-SNAPSHOT + 3.0.4-SNAPSHOT + 3.0.4-SNAPSHOT + 2.0.3-SNAPSHOT 3.1.3-SNAPSHOT - 3.1.3-SNAPSHOT - 3.0.3-SNAPSHOT - 3.0.3-SNAPSHOT + 3.1.4-SNAPSHOT + 3.0.4-SNAPSHOT + 3.0.4-SNAPSHOT 2.3.2-SNAPSHOT 2.5.1 5.13.2 diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/exporter/SpanReporter.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/exporter/SpanReporter.java new file mode 100644 index 000000000..0c0b20cc5 --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/exporter/SpanReporter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.exporter; + +/** + * An interface that allows to process spans after they got finished. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface SpanReporter { + + /** + * Reports the finished span. + * @param span a span that was ended and is ready to be reported. + */ + void report(FinishedSpan span); + +} diff --git a/spring-cloud-sleuth-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index cecaa8ace..aedbd9bf8 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -172,7 +172,6 @@ org.aspectj aspectjrt - org.springframework.boot spring-boot-starter-quartz diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/BraveFinishedSpanWriter.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/BraveFinishedSpanWriter.java new file mode 100644 index 000000000..0448dc7ab --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/BraveFinishedSpanWriter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import java.util.List; +import java.util.stream.Collectors; + +import brave.Tags; +import brave.handler.MutableSpanBytesEncoder; + +import org.springframework.cloud.sleuth.brave.bridge.BraveFinishedSpan; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.exporter.SpanReporter; + +/** + * A {@link SpanReporter} that buffers finished spans. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +class BraveFinishedSpanWriter implements FinishedSpanWriter { + + @Override + public String write(TextOutputFormat format, List spans) { + if (format == TextOutputFormat.CONTENT_TYPE_OPENZIPKIN_JSON_V2) { + return new String(MutableSpanBytesEncoder.zipkinJsonV2(Tags.ERROR) + .encodeList(spans.stream().map(BraveFinishedSpan::toBrave).collect(Collectors.toList()))); + } + return null; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/BufferingSpanReporter.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/BufferingSpanReporter.java new file mode 100644 index 000000000..331db905c --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/BufferingSpanReporter.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.boot.context.metrics.buffering.StartupTimeline; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.exporter.SpanReporter; + +/** + * A {@link SpanReporter} that buffers finished spans. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class BufferingSpanReporter implements SpanReporter { + + private final int capacity; + + private final AtomicInteger estimatedSize = new AtomicInteger(); + + final ConcurrentLinkedQueue spans = new ConcurrentLinkedQueue<>(); + + public BufferingSpanReporter(int capacity) { + this.capacity = capacity; + } + + /** + * Return a snapshot of currently buffered spans. + *

+ * This will not remove spans from the buffer, see {@link #drainFinishedSpans()} ()} + * for its counterpart. + * @return a snapshot of currently buffered spans. + */ + public List getFinishedSpans() { + return new ArrayList<>(this.spans); + } + + /** + * Return the {@link StartupTimeline timeline} by pulling spans from the buffer. + *

+ * This removes steps from the buffer, see {@link #getFinishedSpans()} for its + * read-only counterpart. + * @return buffered steps drained from the buffer. + */ + public List drainFinishedSpans() { + List events = new ArrayList<>(); + Iterator iterator = this.spans.iterator(); + while (iterator.hasNext()) { + events.add(iterator.next()); + iterator.remove(); + } + this.estimatedSize.set(0); + return events; + } + + @Override + public void report(FinishedSpan span) { + if (this.estimatedSize.get() < this.capacity) { + this.estimatedSize.incrementAndGet(); + this.spans.add(span); + } + else { + this.spans.poll(); + this.estimatedSize.decrementAndGet(); + report(span); + } + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/FinishedSpanWriter.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/FinishedSpanWriter.java new file mode 100644 index 000000000..4e40a55db --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/FinishedSpanWriter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import java.util.List; + +import org.springframework.cloud.sleuth.exporter.FinishedSpan; + +/** + * Writes finished spans in a provided format. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface FinishedSpanWriter { + + /** + * Writes the spans in a given format to String. + * @param format format in which spans should be stored + * @param spans spans to store + * @return string representation of spans or {@code null} if {@link TextOutputFormat} + * is not supported. + */ + T write(TextOutputFormat format, List spans); + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/SleuthActuatorProperties.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/SleuthActuatorProperties.java new file mode 100644 index 000000000..80d15b205 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/SleuthActuatorProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Settings for Sleuth actuator. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@ConfigurationProperties(prefix = "management.endpoint.traces") +public class SleuthActuatorProperties { + + /** + * Max capacity of the span queue. + */ + private int capacity = 10_000; + + public int getCapacity() { + return this.capacity; + } + + public void setCapacity(int capacity) { + this.capacity = capacity; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TextOutputFormat.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TextOutputFormat.java new file mode 100644 index 000000000..4e87ec6cb --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TextOutputFormat.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import org.springframework.boot.actuate.endpoint.Producible; +import org.springframework.http.MediaType; +import org.springframework.util.MimeType; + +/** + * A {@link Producible} enum for supported span outputs. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public enum TextOutputFormat implements Producible { + + /** + * OpenZipkin text. + */ + CONTENT_TYPE_OPENZIPKIN_JSON_V2(MediaType.APPLICATION_JSON), + + /** + * OTLP protobuf format. + */ + CONTENT_TYPE_OTLP_PROTOBUF(MediaType.parseMediaType("application/x-protobuf")); + + private final MimeType mimeType; + + TextOutputFormat(MimeType mimeType) { + this.mimeType = mimeType; + } + + @Override + public MimeType getProducedMimeType() { + return this.mimeType; + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfiguration.java new file mode 100644 index 000000000..8cfa0005f --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfiguration.java @@ -0,0 +1,87 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import brave.handler.SpanHandler; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.endpoint.Producible; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.autoconfig.brave.ConditionalOnBraveEnabled; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} for Sleuth actuator endpoint. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(value = "spring.sleuth.enabled", matchIfMissing = true) +@ConditionalOnAvailableEndpoint(endpoint = TracesScrapeEndpoint.class) +@AutoConfigureBefore(BraveAutoConfiguration.class) +@EnableConfigurationProperties(SleuthActuatorProperties.class) +@ConditionalOnClass(Producible.class) +public class TraceSleuthActuatorAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + BufferingSpanReporter sleuthBufferingSpanReporter(SleuthActuatorProperties sleuthActuatorProperties) { + return new BufferingSpanReporter(sleuthActuatorProperties.getCapacity()); + } + + @Bean + @ConditionalOnMissingBean + TracesScrapeEndpoint sleuthTracesScrapeEndpoint(BufferingSpanReporter bufferingSpanReporter, + FinishedSpanWriter finishedSpanWriter) { + return new TracesScrapeEndpoint(bufferingSpanReporter, finishedSpanWriter); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(brave.Tracer.class) + @ConditionalOnBraveEnabled + static class BraveActuatorConfiguration { + + @Bean + @ConditionalOnMissingBean + FinishedSpanWriter sleuthBraveFinishedSpanWriter() { + return new BraveFinishedSpanWriter(); + } + + /** + * We need to register at least one {@link SpanHandler} for sampling to hook in. + * If there are no span handlers all spans will be noop and no spans will get + * reported. + * @return a noop span handler + */ + @Bean + SpanHandler sleuthBraveCustomSpanHandler() { + return new SpanHandler() { + + }; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TracesScrapeEndpoint.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TracesScrapeEndpoint.java new file mode 100644 index 000000000..36eaa262c --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TracesScrapeEndpoint.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import java.util.List; + +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.WriteOperation; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.http.HttpStatus; +import org.springframework.lang.NonNull; + +/** + * {@link Endpoint @Endpoint} that outputs spans in a format that can be scraped by a + * collector. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@WebEndpoint(id = "traces") +public class TracesScrapeEndpoint { + + private final BufferingSpanReporter bufferingSpanReporter; + + private final FinishedSpanWriter finishedSpanWriter; + + public TracesScrapeEndpoint(BufferingSpanReporter bufferingSpanReporter, FinishedSpanWriter finishedSpanWriter) { + this.bufferingSpanReporter = bufferingSpanReporter; + this.finishedSpanWriter = finishedSpanWriter; + } + + @ReadOperation(producesFrom = TextOutputFormat.class) + public WebEndpointResponse spansSnapshot(TextOutputFormat format) { + List finishedSpans = this.bufferingSpanReporter.getFinishedSpans(); + return response(format, finishedSpans); + } + + @NonNull + private WebEndpointResponse response(TextOutputFormat format, List finishedSpans) { + Object spans = this.finishedSpanWriter.write(format, finishedSpans); + if (spans == null) { + return new WebEndpointResponse<>("The format [" + format.getProducedMimeType() + " ] is not supported", + HttpStatus.NOT_ACCEPTABLE.value()); + } + return new WebEndpointResponse<>(spans, format); + } + + @WriteOperation(producesFrom = TextOutputFormat.class) + public WebEndpointResponse spans(TextOutputFormat format) { + List finishedSpans = this.bufferingSpanReporter.drainFinishedSpans(); + return response(format, finishedSpans); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/brave/BraveBridgeConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/brave/BraveBridgeConfiguration.java index cefcd9574..81626fe1b 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/brave/BraveBridgeConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/brave/BraveBridgeConfiguration.java @@ -16,12 +16,14 @@ package org.springframework.cloud.sleuth.autoconfig.brave; +import java.util.ArrayList; import java.util.List; import brave.Tracing; import brave.handler.SpanHandler; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.sleuth.SpanCustomizer; @@ -35,10 +37,10 @@ import org.springframework.cloud.sleuth.brave.bridge.CompositePropagationFactory import org.springframework.cloud.sleuth.brave.bridge.CompositeSpanHandler; import org.springframework.cloud.sleuth.brave.propagation.PropagationFactorySupplier; import org.springframework.cloud.sleuth.exporter.SpanFilter; +import org.springframework.cloud.sleuth.exporter.SpanReporter; import org.springframework.cloud.sleuth.propagation.Propagator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.lang.Nullable; @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(SleuthPropagationProperties.class) @@ -75,8 +77,10 @@ class BraveBridgeConfiguration { // Name is important for sampling conditions @Bean(name = "traceCompositeSpanHandler") - SpanHandler compositeSpanHandler(@Nullable List exporters) { - return new CompositeSpanHandler(exporters); + SpanHandler compositeSpanHandler(ObjectProvider> exporters, + ObjectProvider> reporters) { + return new CompositeSpanHandler(exporters.getIfAvailable(ArrayList::new), + reporters.getIfAvailable(ArrayList::new)); } } diff --git a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index 13decf094..4c915d613 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,5 +1,6 @@ # Auto Configuration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.sleuth.autoconfig.actuate.TraceSleuthActuatorAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.kafka.TracingKafkaAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.kafka.TracingReactorKafkaAutoConfiguration,\ org.springframework.cloud.sleuth.autoconfig.instrument.async.TraceAsyncAutoConfiguration,\ diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/BraveFinishedSpanWriterTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/BraveFinishedSpanWriterTests.java new file mode 100644 index 000000000..4c1aab16d --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/BraveFinishedSpanWriterTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import java.util.Collections; + +import brave.handler.MutableSpan; +import brave.propagation.TraceContext; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.sleuth.brave.bridge.BraveFinishedSpan; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; + +import static org.assertj.core.api.BDDAssertions.then; + +class BraveFinishedSpanWriterTests { + + @Test + void should_convert_finished_spans_to_zipkin_json() { + FinishedSpan finishedSpan = new BraveFinishedSpan( + new MutableSpan(TraceContext.newBuilder().spanId(1L).traceId(2L).build(), null)); + + String json = new BraveFinishedSpanWriter().write(TextOutputFormat.CONTENT_TYPE_OPENZIPKIN_JSON_V2, + Collections.singletonList(finishedSpan)); + + then(json).isEqualTo("[{\"traceId\":\"0000000000000002\",\"id\":\"0000000000000001\"}]"); + } + + @Test + void should_not_support_any_other_format_than_openzipkin() { + FinishedSpan finishedSpan = new BraveFinishedSpan( + new MutableSpan(TraceContext.newBuilder().spanId(1L).traceId(2L).build(), null)); + + String json = new BraveFinishedSpanWriter().write(TextOutputFormat.CONTENT_TYPE_OTLP_PROTOBUF, + Collections.singletonList(finishedSpan)); + + then(json).isNull(); + } + + @Test + void should_return_null_when_format_not_supported() { + then(new BraveFinishedSpanWriter().write(null, Collections.emptyList())).isNull(); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/BufferingSpanReporterTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/BufferingSpanReporterTests.java new file mode 100644 index 000000000..bdd697e47 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/BufferingSpanReporterTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.sleuth.exporter.FinishedSpan; + +import static org.mockito.Mockito.mock; + +class BufferingSpanReporterTests { + + @Test + void should_overwrite_the_oldest_element_with_the_newest_when_queue_is_full() { + BufferingSpanReporter reporter = new BufferingSpanReporter(2); + FinishedSpan oldest = mock(FinishedSpan.class, "oldest"); + FinishedSpan second = mock(FinishedSpan.class, "second"); + FinishedSpan youngest = mock(FinishedSpan.class, "youngest"); + + reporter.report(oldest); + reporter.report(second); + reporter.report(youngest); + + BDDAssertions.then(reporter.spans).containsExactly(second, youngest); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationTests.java new file mode 100644 index 000000000..16bb57137 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +class TraceSleuthActuatorAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("management.endpoints.web.exposure.include=traces") + .withConfiguration(AutoConfigurations.of(TraceSleuthActuatorAutoConfiguration.class)); + + @Test + void should_register_actuator_when_management_endpoint_included() { + this.contextRunner.run(context -> BDDAssertions.then(context).hasSingleBean(TracesScrapeEndpoint.class)); + } + + @Test + void should_not_register_actuator_when_endpoint_disabled() { + this.contextRunner.withPropertyValues("management.endpoint.traces.enabled=false") + .run(context -> BDDAssertions.then(context).doesNotHaveBean(TracesScrapeEndpoint.class)); + } + + @Test + void should_not_register_actuator_when_sleuth_disabled() { + this.contextRunner.withPropertyValues("spring.sleuth.enabled=false") + .run(context -> BDDAssertions.then(context).doesNotHaveBean(TracesScrapeEndpoint.class)); + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TracesScrapeEndpointTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TracesScrapeEndpointTests.java new file mode 100644 index 000000000..3abc6f87b --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TracesScrapeEndpointTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.http.HttpStatus; +import org.springframework.lang.NonNull; + +import static org.assertj.core.api.BDDAssertions.then; + +class TracesScrapeEndpointTests { + + @Test + void should_return_not_acceptable_when_no_finished_span_writer_is_applicable() { + TracesScrapeEndpoint tracesScrapeEndpoint = new TracesScrapeEndpoint(bufferingSpanReporter(), + (format, spans) -> null); + + WebEndpointResponse response = tracesScrapeEndpoint + .spansSnapshot(TextOutputFormat.CONTENT_TYPE_OPENZIPKIN_JSON_V2); + + then(response.getStatus()).isEqualTo(HttpStatus.NOT_ACCEPTABLE.value()); + } + + @NonNull + private BufferingSpanReporter bufferingSpanReporter() { + return new BufferingSpanReporter(1) { + @Override + public List getFinishedSpans() { + return Collections.emptyList(); + } + }; + } + +} diff --git a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/CompositeSpanHandler.java b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/CompositeSpanHandler.java index beee3f488..7157057bc 100644 --- a/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/CompositeSpanHandler.java +++ b/spring-cloud-sleuth-brave/src/main/java/org/springframework/cloud/sleuth/brave/bridge/CompositeSpanHandler.java @@ -24,19 +24,23 @@ import brave.handler.SpanHandler; import brave.propagation.TraceContext; import org.springframework.cloud.sleuth.exporter.SpanFilter; +import org.springframework.cloud.sleuth.exporter.SpanReporter; /** - * Merges {@link SpanFilter}s into a {@link SpanHandler}. + * Merges {@link SpanFilter}s and {@link SpanReporter}s into a {@link SpanHandler}. * * @author Marcin Grzejszczak * @since 3.0.0 */ public class CompositeSpanHandler extends SpanHandler { - private final List exporters; + private final List filters; - public CompositeSpanHandler(List exporters) { - this.exporters = exporters == null ? Collections.emptyList() : exporters; + private final List reporters; + + public CompositeSpanHandler(List filters, List reporters) { + this.filters = filters == null ? Collections.emptyList() : filters; + this.reporters = reporters == null ? Collections.emptyList() : reporters; } @Override @@ -48,11 +52,16 @@ public class CompositeSpanHandler extends SpanHandler { if (!shouldProcess) { return false; } - return super.end(context, span, cause); + shouldProcess = super.end(context, span, cause); + if (!shouldProcess) { + return false; + } + this.reporters.forEach(r -> r.report(BraveFinishedSpan.fromBrave(span))); + return true; } private boolean shouldProcess(MutableSpan span) { - for (SpanFilter exporter : this.exporters) { + for (SpanFilter exporter : this.filters) { if (!exporter.isExportable(BraveFinishedSpan.fromBrave(span))) { return false; } diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/ContextUtil.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/ContextUtil.java index 861a9534f..e678cfe76 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/ContextUtil.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/internal/ContextUtil.java @@ -36,7 +36,7 @@ public final class ContextUtil { private static final Log log = LogFactory.getLog(ContextUtil.class); /** - * @param beanFactory bean facotry + * @param beanFactory bean factory * @return {@code true} when context is not ready to be used */ public static boolean isContextUnusable(BeanFactory beanFactory) { diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index e6e878707..9e87b6cb5 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -35,6 +35,7 @@ + spring-cloud-sleuth-instrumentation-actuator-tests spring-cloud-sleuth-instrumentation-annotation-tests spring-cloud-sleuth-instrumentation-async-tests spring-cloud-sleuth-instrumentation-baggage-tests diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-actuator-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-actuator-tests/pom.xml new file mode 100644 index 000000000..bcfbc15e4 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-actuator-tests/pom.xml @@ -0,0 +1,84 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-actuator-tests + jar + Spring Cloud Sleuth Brave Actuator Instrumentation Tests + Spring Cloud Sleuth Brave Actuator Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + + + + org.springframework.cloud + spring-cloud-sleuth-tests-common + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-test + + + io.zipkin.brave + brave-tests + + + org.awaitility + awaitility + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-actuator-tests/src/test/java/org/springframework/cloud/sleuth/brave/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationIntegrationTests.java b/tests/brave/spring-cloud-sleuth-instrumentation-actuator-tests/src/test/java/org/springframework/cloud/sleuth/brave/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationIntegrationTests.java new file mode 100644 index 000000000..e1ba95e2d --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-actuator-tests/src/test/java/org/springframework/cloud/sleuth/brave/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationIntegrationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.autoconfig.actuate; + +import brave.sampler.Sampler; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.ContextConfiguration; + +@SpringBootTest +@ContextConfiguration(classes = TraceSleuthActuatorAutoConfigurationIntegrationTests.Config.class) +public class TraceSleuthActuatorAutoConfigurationIntegrationTests extends + org.springframework.cloud.sleuth.autoconfig.actuate.TraceSleuthActuatorAutoConfigurationIntegrationTests { + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + TestSpanHandler testSpanHandlerSupplier(brave.test.TestSpanHandler testSpanHandler) { + return new BraveTestSpanHandler(testSpanHandler); + } + + @Bean + Sampler alwaysSampler() { + return Sampler.ALWAYS_SAMPLE; + } + + @Bean + brave.test.TestSpanHandler braveTestSpanHandler() { + return new brave.test.TestSpanHandler(); + } + + } + +} diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-actuator-tests/src/test/resources/application.yml b/tests/brave/spring-cloud-sleuth-instrumentation-actuator-tests/src/test/resources/application.yml new file mode 100644 index 000000000..be7f4fe29 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-actuator-tests/src/test/resources/application.yml @@ -0,0 +1 @@ +logging.level.org.springframework.cloud: DEBUG diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationIntegrationTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationIntegrationTests.java new file mode 100644 index 000000000..073957b00 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationIntegrationTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ContextConfiguration(classes = TraceSleuthActuatorAutoConfigurationIntegrationTests.TestConfig.class) +@TestPropertySource(properties = { "spring.jackson.serialization.indent_output=true", + "management.endpoints.web.exposure.include=*", "spring.jackson.default-property-inclusion=non_null" }) +public abstract class TraceSleuthActuatorAutoConfigurationIntegrationTests { + + protected MockMvc mockMvc; + + @Autowired + WebApplicationContext applicationContext; + + @Autowired + protected Tracer tracer; + + @Autowired + protected BufferingSpanReporter bufferingSpanReporter; + + @BeforeEach + void setup() throws InterruptedException { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.applicationContext).build(); + Span start = this.tracer.nextSpan().name("first").start(); + Thread.sleep(5); + Span second = this.tracer.nextSpan().name("second").start(); + Thread.sleep(10); + Span third = this.tracer.nextSpan().name("third").start(); + Thread.sleep(15); + third.end(); + second.end(); + start.end(); + } + + @Test + void tracesZipkinSnapshot() throws Exception { + tracesSnapshot(MediaType.APPLICATION_JSON, zipkinJsonBody()); + } + + @Test + void tracesZipkin() throws Exception { + traces(MediaType.APPLICATION_JSON, zipkinJsonBody()); + } + + protected void tracesSnapshot(MediaType contentType, ResultMatcher resultMatcher) throws Exception { + await().untilAsserted(() -> then(bufferedSpans()).isNotEmpty()); + + this.mockMvc.perform(get("/actuator/traces").accept(contentType)).andExpect(status().isOk()) + .andExpect(resultMatcher); + + then(bufferedSpans()).isNotEmpty(); + } + + protected void traces(MediaType acceptType, ResultMatcher resultMatcher) throws Exception { + await().untilAsserted(() -> then(bufferedSpans()).isNotEmpty()); + + this.mockMvc.perform(post("/actuator/traces").contentType(MediaType.APPLICATION_JSON).accept(acceptType)) + .andExpect(status().isOk()).andExpect(resultMatcher); + + then(bufferedSpans()).isEmpty(); + } + + protected ResultMatcher zipkinJsonBody() { + return content().string(allOf(containsString("\"name\":\"first\""), containsString("\"name\":\"second\""), + containsString("\"name\":\"third\""))); + } + + protected ConcurrentLinkedQueue bufferedSpans() { + return this.bufferingSpanReporter.spans; + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + static class TestConfig { + + } + +} diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java index 176281fef..691591fca 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java @@ -1,380 +1,381 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.instrument.rsocket; - -import java.net.URI; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingDeque; - -import io.rsocket.frame.FrameType; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.util.context.ContextView; - -import org.springframework.boot.WebApplicationType; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.sleuth.Span; -import org.springframework.cloud.sleuth.TraceContext; -import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.exporter.FinishedSpan; -import org.springframework.cloud.sleuth.test.TestSpanHandler; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.env.Environment; -import org.springframework.messaging.handler.annotation.MessageMapping; -import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.rsocket.RSocketRequester; -import org.springframework.messaging.rsocket.RSocketRequester.Builder; -import org.springframework.messaging.rsocket.RSocketStrategies; -import org.springframework.stereotype.Controller; -import org.springframework.util.MimeType; - -import static org.assertj.core.api.BDDAssertions.then; - -public abstract class TraceRSocketTests { - - public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; - - @Test - public void should_instrument_responder() throws Exception { - // setup - ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) - .web(WebApplicationType.REACTIVE) - .properties("server.port=0", "spring.rsocket.server.transport=websocket", - "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", - "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", - "management.security.enabled=false") - .run(); - final TestSpanHandler spans = context.getBean(TestSpanHandler.class); - final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); - final TestController controller2 = context.getBean(TestController.class); - final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); - - final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); - - final RSocketRequester rSocketRequester = rsocketRequesterBuilder - .websocket(URI.create("ws://localhost:" + port + "/rsocket")); - - // REQUEST FNF - whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); - - FrameType receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); - - receivedFrame = controller2.getReceivedFrames().take(); - thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); - spans.clear(); - controller2.reset(); - - // REQUEST FNF - whenNonSampledRequestFnfIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - whenNonSampledRequestResponseIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - whenNonSampledRequestStreamIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - whenNonSampledRequestChannelIsSent(rSocketRequester); - controller2.getReceivedFrames().take(); - // then - thenNoSpanWasReported(spans, controller2, expectedTraceId()); - spans.clear(); - controller2.reset(); - - // cleanup - context.close(); - } - - protected String expectedTraceId() { - return EXPECTED_TRACE_ID; - } - - protected String expectedSpanId() { - return EXPECTED_TRACE_ID; - } - - @Test - public void should_instrument_requester_and_responder() throws Exception { - // setup - ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) - .web(WebApplicationType.REACTIVE) - .properties("server.port=0", "spring.rsocket.server.transport=websocket", - "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", - "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", - "management.security.enabled=false") - .run(); - - final org.springframework.cloud.sleuth.Tracer tracer = context - .getBean(org.springframework.cloud.sleuth.Tracer.class); - final TestSpanHandler spans = context.getBean(TestSpanHandler.class); - final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); - final TestController controller2 = context.getBean(TestController.class); - - final Builder rsocketRequesterBuilder = context.getBean(Builder.class); - - final RSocketRequester rSocketRequester = rsocketRequesterBuilder - .websocket(URI.create("ws://localhost:" + port + "/rsocket")); - - // REQUEST FNF - final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); - whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) - .doFinally(signalType -> nextSpanFnf.end()).block(); - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST RESPONSE - final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); - whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) - .doFinally(signalType -> nextSpanRR.end()).block(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST STREAM - final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); - whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) - .doFinally(signalType -> nextSpanRS.end()).blockLast(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); - spans.clear(); - controller2.reset(); - - // REQUEST CHANNEL - final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); - whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") - .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) - .doFinally(signalType -> nextSpanRC.end()).blockLast(); - - controller2.getReceivedFrames().take(); - thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); - spans.clear(); - controller2.reset(); - - // cleanup - context.close(); - } - - protected abstract Class testConfiguration(); - - private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { - String expectedName = frameType.name() + " " + path; - Awaitility.await().untilAsserted(() -> { - FinishedSpan span = spans.reportedSpans().stream().filter(finished -> expectedName.equals(finished.getName())) - .findFirst().orElseThrow(() -> new AssertionError("Span with name [" + expectedName + "] not found")); - then(span.getTags()).containsEntry("messaging.controller.class", - "org.springframework.cloud.sleuth.instrument.rsocket.TraceRSocketTests$TestController"); - then(span.getTags()).containsKey("messaging.controller.method"); - }); - } - - private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { - return requester.route(path).send(); - } - - private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { - return requester.route(path).retrieveMono(String.class); - } - - private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { - return requester.route(path).retrieveFlux(String.class); - } - - private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { - return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); - } - - private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { - requester.route("api.c2.fnf").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).send().block(); - } - - private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { - requester.route("api.c2.rr").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).retrieveMono(String.class).block(); - } - - private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { - requester.route("api.c2.rs").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).retrieveFlux(String.class).blockLast(); - } - - private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { - requester.route("api.c2.rc").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { - @Override - public String toString() { - return "b3"; - } - }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); - } - - private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { - // then(spans).isEmpty(); // FIXME: does not work for request case - Awaitility.await().untilAsserted(() -> { - then(controller2.getSpan()).isNotNull(); - then(controller2.getSpan().context().traceId()).isEqualTo(expectedTraceId); - }); - } - - @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration - static class MyConfig { - - @Bean - TestController controller(Tracer tracer) { - return new TestController(tracer); - } - - } - - @Controller - @MessageMapping("api.c2") - static class TestController { - - final Tracer tracer; - - Span span; - - ContextView interceptedContext; - - BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); - - TestController(Tracer tracer) { - this.tracer = tracer; - } - - BlockingQueue getReceivedFrames() { - return this.receivedFrames; - } - - Span getSpan() { - return this.span; - } - - void reset() { - this.span = null; - } - - @MessageMapping("fnf") - Mono testFnf() { - - this.span = this.tracer.currentSpan(); - - return Mono.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_FNF); - return Mono.empty(); - }); - } - - @MessageMapping("rr") - Mono testRR() { - this.span = this.tracer.currentSpan(); - - return Mono.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_RESPONSE); - return Mono.just("response"); - }); - } - - @MessageMapping("rs") - Flux testRS() { - this.span = this.tracer.currentSpan(); - - return Flux.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_STREAM); - return Flux.just("stream"); - }); - } - - @MessageMapping("rc") - Flux testRC(@Payload Flux inbound) { - this.span = this.tracer.currentSpan(); - - return Flux.deferContextual(c -> { - interceptedContext = c; - receivedFrames.offer(FrameType.REQUEST_CHANNEL); - return inbound; - }); - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.rsocket; + +import java.net.URI; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; + +import io.rsocket.frame.FrameType; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.ContextView; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.TraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.rsocket.RSocketRequester; +import org.springframework.messaging.rsocket.RSocketRequester.Builder; +import org.springframework.messaging.rsocket.RSocketStrategies; +import org.springframework.stereotype.Controller; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.BDDAssertions.then; + +public abstract class TraceRSocketTests { + + public static final String EXPECTED_TRACE_ID = "b919095138aa4c6e"; + + @Test + public void should_instrument_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + final RSocketStrategies strategies = context.getBean(RSocketStrategies.class); + + final Builder rsocketRequesterBuilder = RSocketRequester.builder().rsocketStrategies(strategies); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf").block(); + + FrameType receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.fnf", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr").block(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rr", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rs", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc").blockLast(); + + receivedFrame = controller2.getReceivedFrames().take(); + thenSpanWasReportedWithTags(spans, "api.c2.rc", receivedFrame); + spans.clear(); + controller2.reset(); + + // REQUEST FNF + whenNonSampledRequestFnfIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + whenNonSampledRequestResponseIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + whenNonSampledRequestStreamIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + whenNonSampledRequestChannelIsSent(rSocketRequester); + controller2.getReceivedFrames().take(); + // then + thenNoSpanWasReported(spans, controller2, expectedTraceId()); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + protected String expectedTraceId() { + return EXPECTED_TRACE_ID; + } + + protected String expectedSpanId() { + return EXPECTED_TRACE_ID; + } + + @Test + public void should_instrument_requester_and_responder() throws Exception { + // setup + ConfigurableApplicationContext context = new SpringApplicationBuilder(MyConfig.class, testConfiguration()) + .web(WebApplicationType.REACTIVE) + .properties("server.port=0", "spring.rsocket.server.transport=websocket", + "spring.rsocket.server.mapping-path=/rsocket", "spring.jmx.enabled=false", + "spring.application.name=TraceRSocketTests", "security.basic.enabled=false", + "management.security.enabled=false") + .run(); + + final org.springframework.cloud.sleuth.Tracer tracer = context + .getBean(org.springframework.cloud.sleuth.Tracer.class); + final TestSpanHandler spans = context.getBean(TestSpanHandler.class); + final int port = context.getBean(Environment.class).getProperty("local.server.port", Integer.class); + final TestController controller2 = context.getBean(TestController.class); + + final Builder rsocketRequesterBuilder = context.getBean(Builder.class); + + final RSocketRequester rSocketRequester = rsocketRequesterBuilder + .websocket(URI.create("ws://localhost:" + port + "/rsocket")); + + // REQUEST FNF + final org.springframework.cloud.sleuth.Span nextSpanFnf = tracer.nextSpan().start(); + whenRequestFnFIsSent(rSocketRequester, "api.c2.fnf") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanFnf.context())) + .doFinally(signalType -> nextSpanFnf.end()).block(); + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanFnf.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST RESPONSE + final org.springframework.cloud.sleuth.Span nextSpanRR = tracer.nextSpan().start(); + whenRequestResponseIsSent(rSocketRequester, "api.c2.rr") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRR.context())) + .doFinally(signalType -> nextSpanRR.end()).block(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRR.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST STREAM + final org.springframework.cloud.sleuth.Span nextSpanRS = tracer.nextSpan().start(); + whenRequestStreamIsSent(rSocketRequester, "api.c2.rs") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRS.context())) + .doFinally(signalType -> nextSpanRS.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRS.context().traceId()); + spans.clear(); + controller2.reset(); + + // REQUEST CHANNEL + final org.springframework.cloud.sleuth.Span nextSpanRC = tracer.nextSpan().start(); + whenRequestChannelIsSent(rSocketRequester, "api.c2.rc") + .contextWrite(ctx -> ctx.put(TraceContext.class, nextSpanRC.context())) + .doFinally(signalType -> nextSpanRC.end()).blockLast(); + + controller2.getReceivedFrames().take(); + thenNoSpanWasReported(spans, controller2, nextSpanRC.context().traceId()); + spans.clear(); + controller2.reset(); + + // cleanup + context.close(); + } + + protected abstract Class testConfiguration(); + + private void thenSpanWasReportedWithTags(TestSpanHandler spans, String path, FrameType frameType) { + String expectedName = frameType.name() + " " + path; + Awaitility.await().untilAsserted(() -> { + FinishedSpan span = spans.reportedSpans().stream() + .filter(finished -> expectedName.equals(finished.getName())).findFirst() + .orElseThrow(() -> new AssertionError("Span with name [" + expectedName + "] not found")); + then(span.getTags()).containsEntry("messaging.controller.class", + "org.springframework.cloud.sleuth.instrument.rsocket.TraceRSocketTests$TestController"); + then(span.getTags()).containsKey("messaging.controller.method"); + }); + } + + private Mono whenRequestFnFIsSent(RSocketRequester requester, String path) { + return requester.route(path).send(); + } + + private Mono whenRequestResponseIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveMono(String.class); + } + + private Flux whenRequestStreamIsSent(RSocketRequester requester, String path) { + return requester.route(path).retrieveFlux(String.class); + } + + private Flux whenRequestChannelIsSent(RSocketRequester requester, String path) { + return requester.route(path).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class); + } + + private void whenNonSampledRequestFnfIsSent(RSocketRequester requester) { + requester.route("api.c2.fnf").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).send().block(); + } + + private void whenNonSampledRequestResponseIsSent(RSocketRequester requester) { + requester.route("api.c2.rr").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveMono(String.class).block(); + } + + private void whenNonSampledRequestStreamIsSent(RSocketRequester requester) { + requester.route("api.c2.rs").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).retrieveFlux(String.class).blockLast(); + } + + private void whenNonSampledRequestChannelIsSent(RSocketRequester requester) { + requester.route("api.c2.rc").metadata(expectedTraceId() + "-" + expectedSpanId() + "-0", new MimeType("b3") { + @Override + public String toString() { + return "b3"; + } + }).data(Flux.fromArray(new String[] { "test1", "test2" })).retrieveFlux(String.class).blockLast(); + } + + private void thenNoSpanWasReported(TestSpanHandler spans, TestController controller2, String expectedTraceId) { + // then(spans).isEmpty(); // FIXME: does not work for request case + Awaitility.await().untilAsserted(() -> { + then(controller2.getSpan()).isNotNull(); + then(controller2.getSpan().context().traceId()).isEqualTo(expectedTraceId); + }); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + static class MyConfig { + + @Bean + TestController controller(Tracer tracer) { + return new TestController(tracer); + } + + } + + @Controller + @MessageMapping("api.c2") + static class TestController { + + final Tracer tracer; + + Span span; + + ContextView interceptedContext; + + BlockingQueue receivedFrames = new LinkedBlockingDeque<>(); + + TestController(Tracer tracer) { + this.tracer = tracer; + } + + BlockingQueue getReceivedFrames() { + return this.receivedFrames; + } + + Span getSpan() { + return this.span; + } + + void reset() { + this.span = null; + } + + @MessageMapping("fnf") + Mono testFnf() { + + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_FNF); + return Mono.empty(); + }); + } + + @MessageMapping("rr") + Mono testRR() { + this.span = this.tracer.currentSpan(); + + return Mono.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_RESPONSE); + return Mono.just("response"); + }); + } + + @MessageMapping("rs") + Flux testRS() { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_STREAM); + return Flux.just("stream"); + }); + } + + @MessageMapping("rc") + Flux testRC(@Payload Flux inbound) { + this.span = this.tracer.currentSpan(); + + return Flux.deferContextual(c -> { + interceptedContext = c; + receivedFrames.offer(FrameType.REQUEST_CHANNEL); + return inbound; + }); + } + + } + +} From 75b40bb88486f4ece11ecea519cf7782b1360c5e Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 16 Jun 2021 12:52:42 +0200 Subject: [PATCH 53/59] Polished the docs --- docs/src/main/asciidoc/index.adoc | 19 +------------------ docs/src/main/asciidoc/index.htmladoc | 18 ------------------ docs/src/main/asciidoc/index.pdfadoc | 13 ------------- 3 files changed, 1 insertion(+), 49 deletions(-) delete mode 100644 docs/src/main/asciidoc/index.htmladoc delete mode 100644 docs/src/main/asciidoc/index.pdfadoc diff --git a/docs/src/main/asciidoc/index.adoc b/docs/src/main/asciidoc/index.adoc index d34261b40..dd1a0a718 100644 --- a/docs/src/main/asciidoc/index.adoc +++ b/docs/src/main/asciidoc/index.adoc @@ -1,18 +1 @@ -[[spring-cloud-sleuth-reference-documentation]] -= Spring Cloud Sleuth Reference Documentation -Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant - -:docinfo: shared -include::_attributes.adoc[] - -The reference documentation consists of the following sections: - -[horizontal] -<> :: Legal information. -<> :: About the Documentation, Getting Help, First Steps, and more. -<> :: Introducing {project-full-name}, Developing Your First {project-full-name}-based Application -<> :: {project-full-name} usage examples and workflows. -<> :: Span creation, context propagation, and more. -<> :: Add sampling, propagate remote tags, and more. -<> :: Instrumentation configuration, context propagation, and more. -<> :: Span definitions and configuration properties. +include::spring-cloud-sleuth.adoc[] diff --git a/docs/src/main/asciidoc/index.htmladoc b/docs/src/main/asciidoc/index.htmladoc deleted file mode 100644 index d34261b40..000000000 --- a/docs/src/main/asciidoc/index.htmladoc +++ /dev/null @@ -1,18 +0,0 @@ -[[spring-cloud-sleuth-reference-documentation]] -= Spring Cloud Sleuth Reference Documentation -Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant - -:docinfo: shared -include::_attributes.adoc[] - -The reference documentation consists of the following sections: - -[horizontal] -<> :: Legal information. -<> :: About the Documentation, Getting Help, First Steps, and more. -<> :: Introducing {project-full-name}, Developing Your First {project-full-name}-based Application -<> :: {project-full-name} usage examples and workflows. -<> :: Span creation, context propagation, and more. -<> :: Add sampling, propagate remote tags, and more. -<> :: Instrumentation configuration, context propagation, and more. -<> :: Span definitions and configuration properties. diff --git a/docs/src/main/asciidoc/index.pdfadoc b/docs/src/main/asciidoc/index.pdfadoc deleted file mode 100644 index e1edcb891..000000000 --- a/docs/src/main/asciidoc/index.pdfadoc +++ /dev/null @@ -1,13 +0,0 @@ -[[spring-cloud-sleuth-reference-documentation]] -= Spring Cloud Sleuth Reference Documentation -Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant - -include::_attributes.adoc[] - -include::legal.adoc[leveloffset=+1] -include::getting-started.adoc[leveloffset=+1] -include::using.adoc[leveloffset=+1] -include::project-features.adoc[leveloffset=+1] -include::howto.adoc[leveloffset=+1] -include::integrations.adoc[leveloffset=+1] -include::appendix.adoc[leveloffset=+1] From 7c4d8b147983762f2eb47096ce9b76affbb74447 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 18 Jun 2021 07:30:49 +0200 Subject: [PATCH 54/59] Adds support for Kotlin Coroutines (#1977) * Added Kotlin Coroutine support * Added docs, polished code and added one more case for reactor context --- .editorconfig | 61 +++--- docs/pom.xml | 2 +- docs/src/main/asciidoc/integrations.adoc | 7 + pom.xml | 2 + .../actuate/FinishedSpanWriter.java | 80 ++++---- .../TraceSleuthActuatorAutoConfiguration.java | 174 +++++++++--------- ...eSleuthActuatorAutoConfigurationTests.java | 96 +++++----- spring-cloud-sleuth-instrumentation/pom.xml | 92 +++++++++ .../kotlin/KotlinContextElement.java | 92 +++++++++ .../instrument/kotlin/asContextElement.kt | 69 +++++++ .../kotlin/AsContextElementKtTests.kt | 111 +++++++++++ tests/brave/pom.xml | 1 + .../pom.xml | 170 +++++++++++++++++ .../SleuthCoroutinesApplicationTests.kt | 48 +++++ tests/common/pom.xml | 81 ++++++++ .../SleuthCoroutinesApplicationTests.kt | 110 +++++++++++ 16 files changed, 992 insertions(+), 204 deletions(-) create mode 100644 spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/KotlinContextElement.java create mode 100644 spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/asContextElement.kt create mode 100644 spring-cloud-sleuth-instrumentation/src/test/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/AsContextElementKtTests.kt create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/pom.xml create mode 100644 tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/src/test/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt create mode 100644 tests/common/src/main/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt diff --git a/.editorconfig b/.editorconfig index ffe385c6d..3240ec9da 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,28 +1,33 @@ -root = true - -[*] -end_of_line = crlf -insert_final_newline = true - -[*.java] -indent_style = tab -indent_size = 4 -continuation_indent_size = 8 - -[*.groovy] -indent_style = tab -indent_size = 4 -continuation_indent_size = 8 - -[*.xml] -indent_style = tab -indent_size = 4 -continuation_indent_size = 8 - -[*.yml] -indent_style = space -indent_size = 2 - -[*.yaml] -indent_style = space -indent_size = 2 +root = true + +[*] +end_of_line = crlf +insert_final_newline = true + +[*.java] +indent_style = tab +indent_size = 4 +continuation_indent_size = 8 + +[*.kt] +indent_style = tab +indent_size = 4 +continuation_indent_size = 8 + +[*.groovy] +indent_style = tab +indent_size = 4 +continuation_indent_size = 8 + +[*.xml] +indent_style = tab +indent_size = 4 +continuation_indent_size = 8 + +[*.yml] +indent_style = space +indent_size = 2 + +[*.yaml] +indent_style = space +indent_size = 2 diff --git a/docs/pom.xml b/docs/pom.xml index 2481231ef..fd336d69f 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -67,7 +67,7 @@ org.codehaus.mojo build-helper-maven-plugin - 3.0.0 + ${build-helper-maven-plugin.version} generate-sources diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 9e1cb711d..3b6e2ca9e 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -729,3 +729,10 @@ This feature is available for all tracer implementations. We're instrumenting the `Session` repositories that wraps all operations in a span. In order to disable this instrumentation set `spring.sleuth.session.enabled` to `false`. + +[[sleuth-kotlin-integration]] +== Kotlin Coroutines + +This feature is available for all tracer implementations. + +We're adding Kotlin Coroutines that allow you to retrieve the current span via the `Tracer` bean. You can either pass the bean to the Kotlin Coroutine context via `Tracer.asContextElement()` method execution or if you have Reactor Kotlin Coroutine integration on the classpath, we will retrieve it from Reactor's context. To retrieve the current span you can call the `currentSpan()` method within the Kotlin Coroutine. diff --git a/pom.xml b/pom.xml index 087ddb99a..fc67df95b 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,7 @@ 1.7 10.0.6 2.8.0 + 1.5.10 true @@ -98,6 +99,7 @@ 0.21.3 0.14.1 1.15.3 + 3.2.0 diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/FinishedSpanWriter.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/FinishedSpanWriter.java index 4e40a55db..c174467cc 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/FinishedSpanWriter.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/FinishedSpanWriter.java @@ -1,40 +1,40 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.actuate; - -import java.util.List; - -import org.springframework.cloud.sleuth.exporter.FinishedSpan; - -/** - * Writes finished spans in a provided format. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -public interface FinishedSpanWriter { - - /** - * Writes the spans in a given format to String. - * @param format format in which spans should be stored - * @param spans spans to store - * @return string representation of spans or {@code null} if {@link TextOutputFormat} - * is not supported. - */ - T write(TextOutputFormat format, List spans); - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import java.util.List; + +import org.springframework.cloud.sleuth.exporter.FinishedSpan; + +/** + * Writes finished spans in a provided format. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public interface FinishedSpanWriter { + + /** + * Writes the spans in a given format to String. + * @param format format in which spans should be stored + * @param spans spans to store + * @return string representation of spans or {@code null} if {@link TextOutputFormat} + * is not supported. + */ + T write(TextOutputFormat format, List spans); + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfiguration.java index 8cfa0005f..03add3b2d 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfiguration.java @@ -1,87 +1,87 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.actuate; - -import brave.handler.SpanHandler; - -import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; -import org.springframework.boot.actuate.endpoint.Producible; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; -import org.springframework.cloud.sleuth.autoconfig.brave.ConditionalOnBraveEnabled; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration - * Auto-configuration} for Sleuth actuator endpoint. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -@Configuration(proxyBeanMethods = false) -@ConditionalOnProperty(value = "spring.sleuth.enabled", matchIfMissing = true) -@ConditionalOnAvailableEndpoint(endpoint = TracesScrapeEndpoint.class) -@AutoConfigureBefore(BraveAutoConfiguration.class) -@EnableConfigurationProperties(SleuthActuatorProperties.class) -@ConditionalOnClass(Producible.class) -public class TraceSleuthActuatorAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - BufferingSpanReporter sleuthBufferingSpanReporter(SleuthActuatorProperties sleuthActuatorProperties) { - return new BufferingSpanReporter(sleuthActuatorProperties.getCapacity()); - } - - @Bean - @ConditionalOnMissingBean - TracesScrapeEndpoint sleuthTracesScrapeEndpoint(BufferingSpanReporter bufferingSpanReporter, - FinishedSpanWriter finishedSpanWriter) { - return new TracesScrapeEndpoint(bufferingSpanReporter, finishedSpanWriter); - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(brave.Tracer.class) - @ConditionalOnBraveEnabled - static class BraveActuatorConfiguration { - - @Bean - @ConditionalOnMissingBean - FinishedSpanWriter sleuthBraveFinishedSpanWriter() { - return new BraveFinishedSpanWriter(); - } - - /** - * We need to register at least one {@link SpanHandler} for sampling to hook in. - * If there are no span handlers all spans will be noop and no spans will get - * reported. - * @return a noop span handler - */ - @Bean - SpanHandler sleuthBraveCustomSpanHandler() { - return new SpanHandler() { - - }; - } - - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import brave.handler.SpanHandler; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; +import org.springframework.boot.actuate.endpoint.Producible; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.cloud.sleuth.autoconfig.brave.ConditionalOnBraveEnabled; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} for Sleuth actuator endpoint. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(value = "spring.sleuth.enabled", matchIfMissing = true) +@ConditionalOnAvailableEndpoint(endpoint = TracesScrapeEndpoint.class) +@AutoConfigureBefore(BraveAutoConfiguration.class) +@EnableConfigurationProperties(SleuthActuatorProperties.class) +@ConditionalOnClass(Producible.class) +public class TraceSleuthActuatorAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + BufferingSpanReporter sleuthBufferingSpanReporter(SleuthActuatorProperties sleuthActuatorProperties) { + return new BufferingSpanReporter(sleuthActuatorProperties.getCapacity()); + } + + @Bean + @ConditionalOnMissingBean + TracesScrapeEndpoint sleuthTracesScrapeEndpoint(BufferingSpanReporter bufferingSpanReporter, + FinishedSpanWriter finishedSpanWriter) { + return new TracesScrapeEndpoint(bufferingSpanReporter, finishedSpanWriter); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(brave.Tracer.class) + @ConditionalOnBraveEnabled + static class BraveActuatorConfiguration { + + @Bean + @ConditionalOnMissingBean + FinishedSpanWriter sleuthBraveFinishedSpanWriter() { + return new BraveFinishedSpanWriter(); + } + + /** + * We need to register at least one {@link SpanHandler} for sampling to hook in. + * If there are no span handlers all spans will be noop and no spans will get + * reported. + * @return a noop span handler + */ + @Bean + SpanHandler sleuthBraveCustomSpanHandler() { + return new SpanHandler() { + + }; + } + + } + +} diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationTests.java index 16bb57137..2f675933c 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/actuate/TraceSleuthActuatorAutoConfigurationTests.java @@ -1,48 +1,48 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth.autoconfig.actuate; - -import org.assertj.core.api.BDDAssertions; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -class TraceSleuthActuatorAutoConfigurationTests { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withPropertyValues("management.endpoints.web.exposure.include=traces") - .withConfiguration(AutoConfigurations.of(TraceSleuthActuatorAutoConfiguration.class)); - - @Test - void should_register_actuator_when_management_endpoint_included() { - this.contextRunner.run(context -> BDDAssertions.then(context).hasSingleBean(TracesScrapeEndpoint.class)); - } - - @Test - void should_not_register_actuator_when_endpoint_disabled() { - this.contextRunner.withPropertyValues("management.endpoint.traces.enabled=false") - .run(context -> BDDAssertions.then(context).doesNotHaveBean(TracesScrapeEndpoint.class)); - } - - @Test - void should_not_register_actuator_when_sleuth_disabled() { - this.contextRunner.withPropertyValues("spring.sleuth.enabled=false") - .run(context -> BDDAssertions.then(context).doesNotHaveBean(TracesScrapeEndpoint.class)); - } - -} +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.autoconfig.actuate; + +import org.assertj.core.api.BDDAssertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +class TraceSleuthActuatorAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("management.endpoints.web.exposure.include=traces") + .withConfiguration(AutoConfigurations.of(TraceSleuthActuatorAutoConfiguration.class)); + + @Test + void should_register_actuator_when_management_endpoint_included() { + this.contextRunner.run(context -> BDDAssertions.then(context).hasSingleBean(TracesScrapeEndpoint.class)); + } + + @Test + void should_not_register_actuator_when_endpoint_disabled() { + this.contextRunner.withPropertyValues("management.endpoint.traces.enabled=false") + .run(context -> BDDAssertions.then(context).doesNotHaveBean(TracesScrapeEndpoint.class)); + } + + @Test + void should_not_register_actuator_when_sleuth_disabled() { + this.contextRunner.withPropertyValues("spring.sleuth.enabled=false") + .run(context -> BDDAssertions.then(context).doesNotHaveBean(TracesScrapeEndpoint.class)); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index 4af8bedbc..471bae1db 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -212,6 +212,26 @@ spring-session-data-redis true + + io.projectreactor.kotlin + reactor-kotlin-extensions + true + + + org.jetbrains.kotlin + kotlin-reflect + true + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + true + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + true + p6spy @@ -246,6 +266,78 @@ + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + add-source + generate-sources + + add-source + + + + src/main/kotlin + + + + + add-test-source + generate-test-sources + + add-test-source + + + + src/test/kotlin + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + -Xjsr305=strict + + + spring + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + fast diff --git a/spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/KotlinContextElement.java b/spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/KotlinContextElement.java new file mode 100644 index 000000000..c1b9129bb --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/KotlinContextElement.java @@ -0,0 +1,92 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kotlin; + +import kotlin.coroutines.CoroutineContext; +import kotlin.jvm.functions.Function2; +import kotlinx.coroutines.ThreadContextElement; +import org.jetbrains.annotations.Nullable; + +import org.springframework.cloud.sleuth.Span; +import org.springframework.cloud.sleuth.SpanAndScope; +import org.springframework.cloud.sleuth.Tracer; + +/** + * {@link ThreadContextElement} for synchronizing a {@link SpanAndScope} across coroutine + * suspension and resumption. + * + * Inspired by OpenTelemetry's KotlinContextElement. + * + * @since 3.1.0 + */ +class KotlinContextElement implements ThreadContextElement { + + static final CoroutineContext.Key KEY = new CoroutineContext.Key() { + }; + + private final Span span; + + private final Tracer tracer; + + KotlinContextElement(Tracer tracer) { + this.tracer = tracer; + this.span = tracer.currentSpan(); + } + + Span getSpan() { + return this.span; + } + + @Override + public CoroutineContext.Key getKey() { + return KEY; + } + + @Override + @SuppressWarnings("MustBeClosedChecker") + public SpanAndScope updateThreadContext(CoroutineContext coroutineContext) { + Tracer.SpanInScope spanInScope = this.tracer.withSpan(this.span); + return new SpanAndScope(this.span, spanInScope); + } + + @Override + public void restoreThreadContext(CoroutineContext coroutineContext, SpanAndScope spanAndScope) { + spanAndScope.close(); + } + + @Override + public CoroutineContext plus(CoroutineContext coroutineContext) { + return CoroutineContext.DefaultImpls.plus(this, coroutineContext); + } + + @Override + public R fold(R initial, Function2 operation) { + return CoroutineContext.Element.DefaultImpls.fold(this, initial, operation); + } + + @Nullable + @Override + public E get(CoroutineContext.Key key) { + return CoroutineContext.Element.DefaultImpls.get(this, key); + } + + @Override + public CoroutineContext minusKey(CoroutineContext.Key key) { + return CoroutineContext.Element.DefaultImpls.minusKey(this, key); + } + +} diff --git a/spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/asContextElement.kt b/spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/asContextElement.kt new file mode 100644 index 000000000..9c0641e38 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/asContextElement.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kotlin + +import kotlinx.coroutines.reactor.ReactorContext +import org.springframework.cloud.sleuth.CurrentTraceContext +import org.springframework.cloud.sleuth.Span +import org.springframework.cloud.sleuth.TraceContext +import org.springframework.cloud.sleuth.Tracer +import org.springframework.util.ClassUtils +import kotlin.coroutines.CoroutineContext +import kotlin.reflect.jvm.internal.impl.load.kotlin.KotlinClassFinder + +/** + * Returns a [CoroutineContext] which will make this [Context] current when resuming a coroutine + * and restores the previous [Context] on suspension. + * + * Inspired by OpenTelemetry's asContextElement. + * @since 3.1.0 + */ +fun Tracer.asContextElement(): CoroutineContext { + return KotlinContextElement(this) +} + +/** + * Returns the [Span] in this [CoroutineContext] if present, or null otherwise. + * + * Inspired by OpenTelemetry's asContextElement. + * @since 3.1.0 + */ +fun CoroutineContext.currentSpan(): Span? { + val element = get(KotlinContextElement.KEY) + if (element is KotlinContextElement) { + return element.span + } + if (!ClassUtils.isPresent("kotlinx.coroutines.reactor.ReactorContext", null)) { + return null + } + val reactorContext = get(ReactorContext.Key) + if (reactorContext != null) { + if (reactorContext.context.hasKey(Span::class.java)) { + return reactorContext.context.get(Span::class.java) + } + else if (reactorContext.context.hasKey(TraceContext::class.java) && reactorContext.context.hasKey(Tracer::class.java) && reactorContext.context.hasKey(CurrentTraceContext::class.java)) { + val traceContext = reactorContext.context.get(TraceContext::class.java) + reactorContext.context.get(CurrentTraceContext::class.java).maybeScope(traceContext).use { + return reactorContext.context.get(Tracer::class.java).currentSpan() + } + } + else if (reactorContext.context.hasKey(Tracer::class.java)) { + return reactorContext.context.get(Tracer::class.java).currentSpan() + } + } + return null +} diff --git a/spring-cloud-sleuth-instrumentation/src/test/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/AsContextElementKtTests.kt b/spring-cloud-sleuth-instrumentation/src/test/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/AsContextElementKtTests.kt new file mode 100644 index 000000000..ca25386c7 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/kotlin/org/springframework/cloud/sleuth/instrument/kotlin/AsContextElementKtTests.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.instrument.kotlin + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.reactor.ReactorContext +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.BDDAssertions.then +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.FilteredClassLoader +import org.springframework.cloud.sleuth.CurrentTraceContext +import org.springframework.cloud.sleuth.Span +import org.springframework.cloud.sleuth.TraceContext +import org.springframework.cloud.sleuth.Tracer +import org.springframework.cloud.sleuth.tracer.SimpleCurrentTraceContext +import org.springframework.cloud.sleuth.tracer.SimpleTracer +import reactor.util.context.Context + +internal class AsContextElementKtTests { + + @Test + fun `should return current span from context`(): Unit = runBlocking { + val simpleTracer = SimpleTracer() + val nextSpan = simpleTracer.nextSpan().start() + var spanInGlobalScopeLaunch: Span? = null + var spanInGlobalScopeAsync: Span? = null + val asContextElement = simpleTracer.asContextElement() + + GlobalScope.launch(asContextElement) { + spanInGlobalScopeLaunch = coroutineContext.currentSpan() + } + GlobalScope.async(asContextElement) { + spanInGlobalScopeAsync = coroutineContext.currentSpan() + }.await() + + then(spanInGlobalScopeLaunch).isSameAs(nextSpan) + then(spanInGlobalScopeAsync).isSameAs(nextSpan) + } + + @Test + fun `should return span from coroutine context when KotlinContextElement present`(): Unit = runBlocking { + val simpleTracer = SimpleTracer() + val nextSpan = simpleTracer.nextSpan().start() + val element = KotlinContextElement(simpleTracer) + + then(element.currentSpan()).isSameAs(nextSpan) + } + + @Test + fun `should return null from coroutine context when KotlinContextElement and Reactor extensions are missing`(): Unit = runBlocking { + val contextClassLoader = Thread.currentThread().contextClassLoader + try { + Thread.currentThread().contextClassLoader = FilteredClassLoader("kotlinx.coroutines.reactor.ReactorContext") + then(coroutineContext.currentSpan()).isNull() + } finally { + Thread.currentThread().contextClassLoader = contextClassLoader + } + } + + @Test + fun `should return Span from Reactor extensions when KotlinContextElement missing`(): Unit = runBlocking { + val simpleTracer = SimpleTracer() + val nextSpan = simpleTracer.nextSpan().start() + val reactorContext = ReactorContext(Context.of(Span::class.java, nextSpan)) + + then(reactorContext.currentSpan()).isSameAs(nextSpan); + } + + @Test + fun `should return Span from Reactor extensions CurrentTraceContext when KotlinContextElement missing and there is TraceContext in Reactor context`(): Unit = runBlocking { + val currentTraceContext = SimpleCurrentTraceContext() + val simpleTracer = SimpleTracer() + val nextSpan = simpleTracer.nextSpan().start() + val reactorContext = ReactorContext(Context.of(Tracer::class.java, simpleTracer, CurrentTraceContext::class.java, currentTraceContext, TraceContext::class.java, nextSpan.context())) + + then(reactorContext.currentSpan()).isSameAs(nextSpan); + } + + @Test + fun `should return Span from Reactor extensions Tracer when KotlinContextElement missing and there is no Span in context`(): Unit = runBlocking { + val simpleTracer = SimpleTracer() + val nextSpan = simpleTracer.nextSpan().start() + val reactorContext = ReactorContext(Context.of(Tracer::class.java, simpleTracer)) + + then(reactorContext.currentSpan()).isSameAs(nextSpan); + } + + @Test + fun `should return null when no span is found`(): Unit = runBlocking { + val reactorContext = ReactorContext(Context.empty()) + + then(reactorContext.currentSpan()).isNull() + } + +} diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index 9e87b6cb5..50083a144 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -48,6 +48,7 @@ spring-cloud-sleuth-instrumentation-grpc-tests spring-cloud-sleuth-instrumentation-jdbc-tests spring-cloud-sleuth-instrumentation-kafka-tests + spring-cloud-sleuth-instrumentation-kotlin-tests spring-cloud-sleuth-instrumentation-lettuce-tests spring-cloud-sleuth-instrumentation-messaging-tests spring-cloud-sleuth-instrumentation-mvc-tests diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/pom.xml new file mode 100644 index 000000000..0dc78f359 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/pom.xml @@ -0,0 +1,170 @@ + + + + + 4.0.0 + + spring-cloud-sleuth-instrumentation-kotlin-tests + jar + Spring Cloud Sleuth Brave Kotlin Instrumentation Tests + Spring Cloud Sleuth Brave Kotlin Instrumentation Tests + + + org.springframework.cloud + spring-cloud-sleuth-tests-brave + 3.1.0-SNAPSHOT + .. + + + + true + + + + + + + maven-deploy-plugin + + true + + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + add-source + generate-sources + + add-source + + + + src/main/kotlin + + + + + add-test-source + generate-test-sources + + add-test-source + + + + src/test/kotlin + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + -Xjsr305=strict + + + spring + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + + + org.springframework.cloud + spring-cloud-sleuth-tests-common + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-test + + + io.zipkin.brave + brave-tests + + + org.awaitility + awaitility + + + + io.projectreactor.kotlin + reactor-kotlin-extensions + true + + + org.jetbrains.kotlin + kotlin-reflect + true + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + true + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + true + + + + + diff --git a/tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/src/test/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt b/tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/src/test/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt new file mode 100644 index 000000000..6a33b677a --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kotlin-tests/src/test/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.brave.kotlin + +import brave.sampler.Sampler +import brave.test.TestSpanHandler +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.cloud.sleuth.brave.BraveTestSpanHandler +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.test.context.ContextConfiguration + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ContextConfiguration +class SleuthCoroutinesApplicationTests : org.springframework.cloud.sleuth.kotlin.SleuthCoroutinesApplicationTests() { + + @Configuration(proxyBeanMethods = false) + internal class Config { + @Bean + fun testSpanHandlerSupplier(testSpanHandler: TestSpanHandler?): org.springframework.cloud.sleuth.test.TestSpanHandler { + return BraveTestSpanHandler(testSpanHandler) + } + + @Bean + fun alwaysSampler(): Sampler { + return Sampler.ALWAYS_SAMPLE + } + + @Bean + fun braveTestSpanHandler(): TestSpanHandler { + return TestSpanHandler() + } + } +} diff --git a/tests/common/pom.xml b/tests/common/pom.xml index 46fe1655d..8f52be66c 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -260,6 +260,87 @@ + + io.projectreactor.kotlin + reactor-kotlin-extensions + true + + + org.jetbrains.kotlin + kotlin-reflect + true + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + true + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + true + + + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + add-source + generate-sources + + add-source + + + + src/main/kotlin + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + -Xjsr305=strict + + + spring + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + diff --git a/tests/common/src/main/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt b/tests/common/src/main/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt new file mode 100644 index 000000000..213148959 --- /dev/null +++ b/tests/common/src/main/kotlin/org/springframework/cloud/sleuth/kotlin/SleuthCoroutinesApplicationTests.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.sleuth.kotlin + +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.BDDAssertions.then +import org.junit.jupiter.api.Test +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.boot.web.server.LocalServerPort +import org.springframework.cloud.sleuth.Span +import org.springframework.cloud.sleuth.Tracer +import org.springframework.cloud.sleuth.instrument.kotlin.asContextElement +import org.springframework.cloud.sleuth.instrument.kotlin.currentSpan +import org.springframework.context.annotation.Bean +import org.springframework.test.context.ContextConfiguration +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.client.RestTemplate +import kotlin.coroutines.coroutineContext + +@ContextConfiguration(classes = [SleuthCoroutinesApplicationTests.ControllerTestConfig::class]) +abstract class SleuthCoroutinesApplicationTests { + + @LocalServerPort + var serverPort: Int = 0 + + @Autowired + lateinit var tracer: Tracer + + @Autowired + lateinit var webController: WebController + + @Autowired + lateinit var restTemplate: RestTemplate + + @Test + fun should_pass_tracing_context_within_coroutines(): Unit = runBlocking { + val nextSpan = tracer.nextSpan() + val traceId = nextSpan.context().traceId() + tracer.withSpan(nextSpan.start()).use { withSpan -> + val response = restTemplate.getForObject(applicationUrl(), String::class.java) + // span in the controller is set + then(webController.spanInController).isNotNull + then(webController.spanInController?.context()?.traceId()).isEqualTo(traceId) + // span in global scope launch is set + then(webController.spanInGlobalScopeLaunch).isNotNull + then(webController.spanInGlobalScopeLaunch?.context()?.traceId()).isEqualTo(traceId) + // span in global async scope is set + then(webController.spanInGlobalScopeAsync).isNotNull + then(webController.spanInGlobalScopeAsync?.context()?.traceId()).isEqualTo(traceId) + // trace id got propagated + then(response).isEqualTo(traceId) + } + } + + private fun applicationUrl() = "http://localhost:$serverPort/hello" + + @TestConfiguration + @EnableAutoConfiguration + internal class ControllerTestConfig { + @Bean + fun restTemplate(): RestTemplate = RestTemplate() + + @Bean + fun webController(tracer: Tracer): WebController = WebController(tracer) + } +} + +@RestController +class WebController(val tracer: Tracer) { + val log: Logger = LoggerFactory.getLogger(this::class.java) + var spanInController: Span? = null + var spanInGlobalScopeLaunch: Span? = null + var spanInGlobalScopeAsync: Span? = null + + @GetMapping("/hello") + suspend fun hello(): String? { + spanInController = coroutineContext.currentSpan() + GlobalScope.launch(tracer.asContextElement()) { + log.info("in Coroutines context (launch) - current span {}", coroutineContext.currentSpan()) + spanInGlobalScopeLaunch = coroutineContext.currentSpan() + } + GlobalScope.async(tracer.asContextElement()) { + log.info("in Coroutines context (async)- current span {}", coroutineContext.currentSpan()) + spanInGlobalScopeAsync = coroutineContext.currentSpan() + }.await() + return coroutineContext.currentSpan()?.context()?.traceId() + } +} From f7eb39891720675e0fa25d0fea5d773839b59cc3 Mon Sep 17 00:00:00 2001 From: Chintan Radia Date: Fri, 18 Jun 2021 13:06:28 +0530 Subject: [PATCH 55/59] Fix not able to override datasource-proxy log-level (#1976) * Fix datasource-proxy log-level is not working - Aligned default values of `Query` & `SlowQuery` in `TraceJdbcProperties.DataSourceProxyProperties` and `DataSourceProxyProperties` - Add code to copy properties of `Query` and `SlowQuery` in `DataSourceProxyConfiguration` - Add test case to check if `log-level` can be override or not. Fixes gh-1973 * Disable query log listener by default - By default `QueryLogListener` and `LoggingQueryLogListener` will be disabled, dev need to enable it by using properties - By default `Slf4j` will be enlabled for query logging so by adding below property slow query logging will be enabled `spring.sleuth.jdbc.datasource-proxy.slow-query.enable-logging=true` this property will register `SLF4JSlowQueryListener` * Add docs for how-to enable data-proxy query logs --- docs/src/main/asciidoc/_configprops.adoc | 1 + docs/src/main/asciidoc/integrations.adoc | 3 + .../jdbc/DataSourceProxyConfiguration.java | 3 + .../instrument/jdbc/TraceJdbcProperties.java | 2 +- .../ProxyDataSourceConfigurationTests.java | 66 ++++++++++++++++--- .../jdbc/DataSourceProxyProperties.java | 4 +- 6 files changed, 67 insertions(+), 12 deletions(-) diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 5f575c7e0..6df436b44 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -31,6 +31,7 @@ |spring.sleuth.jdbc.datasource-proxy.query.enable-logging | `false` | Enable logging all queries to the log. |spring.sleuth.jdbc.datasource-proxy.query.log-level | `DEBUG` | Severity of query logger. |spring.sleuth.jdbc.datasource-proxy.query.logger-name | | Name of query logger. +|spring.sleuth.jdbc.datasource-proxy.slow-query.enable-logging | `false` | Enable logging slow queries to the log. |spring.sleuth.jdbc.datasource-proxy.slow-query.log-level | `WARN` | Severity of slow query logger. |spring.sleuth.jdbc.datasource-proxy.slow-query.logger-name | | Name of slow query logger. |spring.sleuth.jdbc.datasource-proxy.slow-query.threshold | `300` | Number of seconds to consider query as slow. diff --git a/docs/src/main/asciidoc/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 3b6e2ca9e..21588a64c 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -720,6 +720,9 @@ Please check the <> page under `spring.sleuth.j You can configure P6Spy manually using one of available configuration methods. For more information please refer to the http://p6spy.readthedocs.io/en/latest/configandusage.html[P6Spy Configuration Guide]. +By default logging queries will be disabled, set `spring.sleuth.jdbc.datasource-proxy.slow-query.enable-logging` to `true` to enable logging slow queries +and set `spring.sleuth.jdbc.datasource-proxy.query.enable-logging` to `true` to enable logging all queries. + In order to disable this instrumentation set `spring.sleuth.jdbc.enabled` to `false`. [[sleuth-session-integration]] diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java index 876efc38c..1ef0051e8 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java @@ -50,6 +50,7 @@ import org.springframework.context.annotation.Bean; * {@link QueryTransformer}. * * @author Arthur Gavlyukovskiy + * @author Chintan Radia */ @ConditionalOnClass(ProxyDataSource.class) @ConditionalOnProperty(name = "spring.sleuth.jdbc.datasource-proxy.enabled", havingValue = "true", @@ -84,6 +85,8 @@ class DataSourceProxyConfiguration { DataSourceProxyProperties props = new DataSourceProxyProperties(); BeanUtils.copyProperties(originalProxy, props); props.setLogging(DataSourceProxyProperties.DataSourceProxyLogging.valueOf(originalProxy.getLogging().name())); + BeanUtils.copyProperties(originalProxy.getQuery(), props.getQuery()); + BeanUtils.copyProperties(originalProxy.getSlowQuery(), props.getSlowQuery()); return props; } diff --git a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java index 32d4360b9..a813b8e1a 100644 --- a/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java @@ -253,7 +253,7 @@ public class TraceJdbcProperties { */ private long threshold = 300; - boolean isEnableLogging() { + public boolean isEnableLogging() { return enableLogging; } diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java index 844f34a23..cb7fdad1e 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java @@ -29,6 +29,7 @@ import net.ttddyy.dsproxy.listener.logging.CommonsQueryLoggingListener; import net.ttddyy.dsproxy.listener.logging.CommonsSlowQueryListener; import net.ttddyy.dsproxy.listener.logging.JULQueryLoggingListener; import net.ttddyy.dsproxy.listener.logging.JULSlowQueryListener; +import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; import net.ttddyy.dsproxy.listener.logging.SLF4JQueryLoggingListener; import net.ttddyy.dsproxy.listener.logging.SLF4JSlowQueryListener; import net.ttddyy.dsproxy.listener.logging.SystemOutQueryLoggingListener; @@ -53,6 +54,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.filter; class ProxyDataSourceConfigurationTests { @@ -65,7 +67,24 @@ class ProxyDataSourceConfigurationTests { .withClassLoader(new FilteredClassLoader("com.p6spy")); @Test - void testRegisterLogAndSlowQueryLogByDefaultToSlf4j() { + void testNotRegisterLogAndSlowQueryLogByDefaultToSlf4j() { + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(chainListener.getListeners()).extracting("class").doesNotContain(SLF4JSlowQueryListener.class); + assertThat(chainListener.getListeners()).extracting("class") + .doesNotContain(SLF4JQueryLoggingListener.class); + }); + } + + @Test + void testRegisterLogAndSlowQueryLogByDefaultToSlf4jWhenLoggingIsEnabled() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.datasource-proxy.slow-query.enable-logging=true", + "spring.sleuth.jdbc.datasource-proxy.query.enable-logging=true"); + contextRunner.run(context -> { DataSource dataSource = context.getBean(DataSource.class); ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) @@ -78,8 +97,10 @@ class ProxyDataSourceConfigurationTests { @Test void testRegisterLogAndSlowQueryLogByUsingSlf4j() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.datasource-proxy.logging=slf4j"); + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.datasource-proxy.logging=slf4j", + "spring.sleuth.jdbc.datasource-proxy.slow-query.enable-logging=true", + "spring.sleuth.jdbc.datasource-proxy.query.enable-logging=true"); contextRunner.run(context -> { DataSource dataSource = context.getBean(DataSource.class); @@ -93,8 +114,10 @@ class ProxyDataSourceConfigurationTests { @Test void testRegisterLogAndSlowQueryLogUsingSystemOut() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.datasource-proxy.logging=sysout"); + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.datasource-proxy.logging=sysout", + "spring.sleuth.jdbc.datasource-proxy.slow-query.enable-logging=true", + "spring.sleuth.jdbc.datasource-proxy.query.enable-logging=true"); contextRunner.run(context -> { DataSource dataSource = context.getBean(DataSource.class); @@ -108,8 +131,10 @@ class ProxyDataSourceConfigurationTests { @Test void testRegisterLogAndSlowQueryLogUsingJUL() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.datasourceProxy.logging=jul"); + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.datasourceProxy.logging=jul", + "spring.sleuth.jdbc.datasource-proxy.slow-query.enable-logging=true", + "spring.sleuth.jdbc.datasource-proxy.query.enable-logging=true"); contextRunner.run(context -> { DataSource dataSource = context.getBean(DataSource.class); @@ -123,8 +148,10 @@ class ProxyDataSourceConfigurationTests { @Test void testRegisterLogAndSlowQueryLogUsingApacheCommons() { - ApplicationContextRunner contextRunner = this.contextRunner - .withPropertyValues("spring.sleuth.jdbc.datasourceProxy.logging=commons"); + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.datasourceProxy.logging=commons", + "spring.sleuth.jdbc.datasource-proxy.slow-query.enable-logging=true", + "spring.sleuth.jdbc.datasource-proxy.query.enable-logging=true"); contextRunner.run(context -> { DataSource dataSource = context.getBean(DataSource.class); @@ -193,6 +220,27 @@ class ProxyDataSourceConfigurationTests { }); } + @Test + void testLogLevelIsCustomizableForLogAndSlowQueryInSlf4j() { + ApplicationContextRunner contextRunner = this.contextRunner.withPropertyValues( + "spring.sleuth.jdbc.datasource-proxy.logging=slf4j", + "spring.sleuth.jdbc.datasource-proxy.query.enable-logging=true", + "spring.sleuth.jdbc.datasource-proxy.slow-query.enable-logging=true", + "spring.sleuth.jdbc.datasource-proxy.slow-query.log-level=INFO", + "spring.sleuth.jdbc.datasource-proxy.query.log-level=INFO"); + + contextRunner.run(context -> { + DataSource dataSource = context.getBean(DataSource.class); + ProxyDataSource proxyDataSource = (ProxyDataSource) ((DataSourceWrapper) dataSource) + .getDecoratedDataSource(); + ChainListener chainListener = proxyDataSource.getProxyConfig().getQueryListener(); + assertThat(filter(chainListener.getListeners()).with("class").equalsTo(SLF4JQueryLoggingListener.class) + .with("logLevel").equalsTo(SLF4JLogLevel.INFO).get()).hasSize(1); + assertThat(filter(chainListener.getListeners()).with("class").equalsTo(SLF4JSlowQueryListener.class) + .with("logLevel").equalsTo(SLF4JLogLevel.INFO).get()).hasSize(1); + }); + } + @Configuration static class CustomDataSourceProxyConfiguration { diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyProperties.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyProperties.java index be8a40839..1a3954f3b 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyProperties.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyProperties.java @@ -113,7 +113,7 @@ public class DataSourceProxyProperties { /** * Enable logging all queries to the log. */ - private boolean enableLogging = true; + private boolean enableLogging = false; /** * Name of query logger. @@ -164,7 +164,7 @@ public class DataSourceProxyProperties { /** * Enable logging slow queries to the log. */ - private boolean enableLogging = true; + private boolean enableLogging = false; /** * Name of slow query logger. From d353e6655639ba46477c21cbd846fe2d3ed4f560 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 24 Jun 2021 17:49:57 +0200 Subject: [PATCH 56/59] Removed an unused class --- .../cloud/sleuth/WithThreadLocalSpan.java | 91 ---------------- ...itional-spring-configuration-metadata.json | 100 ------------------ .../rsocket/TracingResponderRSocketProxy.java | 8 +- 3 files changed, 1 insertion(+), 198 deletions(-) delete mode 100644 spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/WithThreadLocalSpan.java delete mode 100644 spring-cloud-sleuth-api/src/main/resources/META-INF/additional-spring-configuration-metadata.json diff --git a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/WithThreadLocalSpan.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/WithThreadLocalSpan.java deleted file mode 100644 index 89aec40f8..000000000 --- a/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/WithThreadLocalSpan.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2013-2021 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.sleuth; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.lang.Nullable; - -/** - * Represents a {@link Span} stored in thread local. - * - * @author Marcin Grzejszczak - * @since 3.1.0 - */ -public interface WithThreadLocalSpan { - - /** - * Logger. - */ - Log log = LogFactory.getLog(WithThreadLocalSpan.class); - - /** - * Sets the span in thread local scope. - * @param span span to put in thread local - */ - default void setSpanInScope(Span span) { - getThreadLocalSpan().set(span); - if (log.isDebugEnabled()) { - log.debug("Put span in scope " + span); - } - } - - /** - * Finishes the thread local span. - * @param error potential error to be stored in span - */ - default void finishSpan(@Nullable Throwable error) { - SpanAndScope spanAndScope = takeSpanFromThreadLocal(); - if (spanAndScope == null) { - return; - } - Span span = spanAndScope.getSpan(); - Tracer.SpanInScope scope = spanAndScope.getScope(); - if (span.isNoop()) { - if (log.isDebugEnabled()) { - log.debug("Span " + span + " is noop - will stope the scope"); - } - scope.close(); - return; - } - if (error != null) { // an error occurred, adding error to span - span.error(error); - } - if (log.isDebugEnabled()) { - log.debug("Will finish the span and its corresponding scope " + span); - } - span.end(); - scope.close(); - } - - /** - * Takes a span from thread local and restores the previous one if present. - * @return span from a thread local span - */ - default SpanAndScope takeSpanFromThreadLocal() { - SpanAndScope span = getThreadLocalSpan().get(); - if (log.isDebugEnabled()) { - log.debug("Took span [" + span + "] from thread local"); - } - getThreadLocalSpan().remove(); - return span; - } - - ThreadLocalSpan getThreadLocalSpan(); - -} diff --git a/spring-cloud-sleuth-api/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-sleuth-api/src/main/resources/META-INF/additional-spring-configuration-metadata.json deleted file mode 100644 index 828f8064d..000000000 --- a/spring-cloud-sleuth-api/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "properties": [ - { - "name": "spring.sleuth.integration.enabled", - "type": "java.lang.Boolean", - "description": "Enable Spring Integration sleuth instrumentation.", - "defaultValue": true - }, - { - "name": "spring.sleuth.integration.websockets.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing for WebSockets.", - "defaultValue": true - }, - { - "name": "spring.sleuth.async.enabled", - "type": "java.lang.Boolean", - "description": "Enable instrumenting async related components so that the tracing information is passed between threads.", - "defaultValue": true - }, - { - "name": "spring.sleuth.async.configurer.enabled", - "type": "java.lang.Boolean", - "description": "Enable default AsyncConfigurer.", - "defaultValue": true - }, - { - "name": "spring.sleuth.feign.enabled", - "type": "java.lang.Boolean", - "description": "Enable span information propagation when using Feign.", - "defaultValue": true - }, - { - "name": "spring.sleuth.feign.processor.enabled", - "type": "java.lang.Boolean", - "description": "Enable post processor that wraps Feign Context in its tracing representations.", - "defaultValue": true - }, - { - "name": "spring.sleuth.grpc.enabled", - "type": "java.lang.Boolean", - "description": "Enable span information propagation when using GRPC.", - "defaultValue": true - }, - { - "name": "spring.sleuth.messaging.jms.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing of JMS.", - "defaultValue": true - }, - { - "name": "spring.sleuth.messaging.rabbit.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing of RabbitMQ.", - "defaultValue": true - }, - { - "name": "spring.sleuth.messaging.kafka.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing of Kafka.", - "defaultValue": true - }, - { - "name": "spring.sleuth.messaging.kafka.mapper.enabled", - "type": "java.lang.Boolean", - "description": "Enable DefaultKafkaHeaderMapper tracing for Kafka.", - "defaultValue": true - }, - { - "name": "spring.sleuth.quartz.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing for Quartz.", - "defaultValue": true - }, - { - "name": "spring.sleuth.mongodb.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing for MongoDb.", - "defaultValue": true - }, - { - "name": "spring.sleuth.rpc.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing of RPC.", - "defaultValue": true - }, - { - "name": "spring.sleuth.sampler.refresh.enabled", - "type": "java.lang.Boolean", - "description": "Enable refresh scope for sampler.", - "defaultValue": true - }, - { - "name": "spring.sleuth.web.webclient.enabled", - "type": "java.lang.Boolean", - "description": "Enable tracing instrumentation for WebClient.", - "defaultValue": true - } - ] -} diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java index 25e7ace0b..4a1604726 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java @@ -38,7 +38,6 @@ import org.springframework.cloud.sleuth.Span; import org.springframework.cloud.sleuth.ThreadLocalSpan; import org.springframework.cloud.sleuth.TraceContext; import org.springframework.cloud.sleuth.Tracer; -import org.springframework.cloud.sleuth.WithThreadLocalSpan; import org.springframework.cloud.sleuth.docs.AssertingSpanBuilder; import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; import org.springframework.cloud.sleuth.internal.EncodingUtils; @@ -51,7 +50,7 @@ import org.springframework.cloud.sleuth.propagation.Propagator; * @author Oleh Dokuka * @since 3.1.0 */ -public class TracingResponderRSocketProxy extends RSocketProxy implements WithThreadLocalSpan { +public class TracingResponderRSocketProxy extends RSocketProxy { private static final Log log = LogFactory.getLog(TracingResponderRSocketProxy.class); @@ -165,9 +164,4 @@ public class TracingResponderRSocketProxy extends RSocketProxy implements WithTh return this.propagator.extract(headers, this.getter); } - @Override - public ThreadLocalSpan getThreadLocalSpan() { - return this.threadLocalSpan; - } - } From 4f6eb0452772d467fcfd79e3dbd2ad64807a9fed Mon Sep 17 00:00:00 2001 From: Toshiaki Maki Date: Mon, 28 Jun 2021 17:38:46 +0900 Subject: [PATCH 57/59] Set spanId in TracingResponderRSocketProxy (#1986) also respect traceIdHigh and debug flag in TracingMetadata --- .../rsocket/TracingResponderRSocketProxy.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java index 4a1604726..038429143 100644 --- a/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java @@ -152,8 +152,16 @@ public class TracingResponderRSocketProxy extends RSocketProxy { if (extract != null) { TracingMetadata tracingMetadata = TracingMetadataCodec.decode(extract); Span.Builder builder = this.tracer.spanBuilder(); + String traceId = EncodingUtils.fromLong(tracingMetadata.traceId()); + long traceIdHigh = tracingMetadata.traceIdHigh(); + if (traceIdHigh != 0L) { + // ExtendedTraceId + traceId = EncodingUtils.fromLong(traceIdHigh) + traceId; + } TraceContext.Builder parentBuilder = this.tracer.traceContextBuilder() - .sampled(tracingMetadata.isSampled()).traceId(EncodingUtils.fromLong(tracingMetadata.traceId())) + .sampled(tracingMetadata.isDebug() || tracingMetadata.isSampled()) + .traceId(traceId) + .spanId(EncodingUtils.fromLong(tracingMetadata.spanId())) .parentId(EncodingUtils.fromLong(tracingMetadata.parentId())); return builder.setParent(parentBuilder.build()); } From b18819a414f6c039012d4694b9398bd4aa08924e Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Tue, 6 Jul 2021 15:42:41 -0700 Subject: [PATCH 58/59] Fixing CustomPropagationFactoryTests --- .../brave/baggage/CustomPropagationFactoryTests.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/baggage/CustomPropagationFactoryTests.java b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/baggage/CustomPropagationFactoryTests.java index ad08b8385..a408caf8e 100644 --- a/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/baggage/CustomPropagationFactoryTests.java +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/brave/baggage/CustomPropagationFactoryTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -49,7 +50,9 @@ public class CustomPropagationFactoryTests { @Test void should_start_the_context_when_propagation_type_custom_and_no_custom_propagation_provided() { new ApplicationContextRunner().withUserConfiguration(CustomConfig.class) - .withPropertyValues("spring.sleuth.propagation.type=custom").run(context -> BDDAssertions.then(context) + .withPropertyValues("spring.sleuth.propagation.type=custom") + .withPropertyValues("spring.cloud.gateway.redis.enabled=false") + .withPropertyValues("spring.r2dbc.pool.enabled=true").run(context -> BDDAssertions.then(context) .hasNotFailed().getBean(CustomConfig.CustomPropagation.class)); } @@ -64,7 +67,7 @@ public class CustomPropagationFactoryTests { @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, - MongoAutoConfiguration.class, QuartzAutoConfiguration.class }) + MongoAutoConfiguration.class, QuartzAutoConfiguration.class, CassandraAutoConfiguration.class }) static class CustomConfig { @Bean From 6ef9f4b99e8703efb03798fc9d56def9739e33b8 Mon Sep 17 00:00:00 2001 From: Jonatan Ivanov Date: Tue, 6 Jul 2021 16:26:53 -0700 Subject: [PATCH 59/59] fixing TestTracer --- .../org/springframework/cloud/sleuth/test/TestTracer.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/common/src/main/java/org/springframework/cloud/sleuth/test/TestTracer.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/test/TestTracer.java index d420f750b..f3f1a4b2e 100644 --- a/tests/common/src/main/java/org/springframework/cloud/sleuth/test/TestTracer.java +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/test/TestTracer.java @@ -92,6 +92,11 @@ public class TestTracer implements Tracer, AutoCloseable { return new TestSpanBuilder(delegate.spanBuilder(), this); } + @Override + public TraceContext.Builder traceContextBuilder() { + return delegate.traceContextBuilder(); + } + @Override @Nullable public SpanCustomizer currentSpanCustomizer() {