diff --git a/.editorconfig b/.editorconfig index ddda9782f..3240ec9da 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,24 +1,33 @@ -root = 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/.github/workflows/maven.yml b/.github/workflows/maven.yml index f8ee2091f..2e7786e55 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -1,32 +1,32 @@ -name: Build - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - - runs-on: ubuntu-latest - strategy: - matrix: - java: ["8", "11", "16"] - - steps: - - uses: actions/checkout@v2 - - name: Setup java - uses: actions/setup-java@v2 - with: - distribution: 'zulu' - java-version: ${{ matrix.java }} - - name: Cache local Maven repository - uses: actions/cache@v2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - name: Build with Maven - run: ./mvnw clean install -B -U +name: Build + +on: + push: + branches: [ 3.1.x ] + pull_request: + branches: [ 3.1.x ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + java: ["8", "11", "16"] + + steps: + - uses: actions/checkout@v2 + - name: Setup java + uses: actions/setup-java@v2 + with: + distribution: 'zulu' + java-version: ${{ matrix.java }} + - name: Cache local Maven repository + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Build with Maven + run: ./mvnw clean install -B -U -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml index 0972ea89a..a32935783 100644 --- a/benchmarks/pom.xml +++ b/benchmarks/pom.xml @@ -1,598 +1,598 @@ - - - - 4.0.0 - - Benchmarks - Benchmarks (JMH) - org.springframework.cloud - 3.0.4-SNAPSHOT - benchmarks - - - org.springframework.boot - spring-boot-starter-parent - 2.4.7 - - - - - ${project.basedir}/.. - 3.2.1 - true - 1.8 - 1.8 - 4.9.0 - 0.2.0.RELEASE - 1.26 - - - - - - org.springframework.cloud - spring-cloud-sleuth-dependencies - ${project.version} - pom - import - - - org.springframework.cloud - spring-cloud-sleuth - ${project.version} - pom - import - - - - - - - org.springframework.cloud - spring-cloud-starter-sleuth - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.boot - spring-boot-configuration-processor - - - - org.springframework - spring-test - compile - - - org.aspectj - aspectjrt - - - org.aspectj - aspectjweaver - - - org.assertj - assertj-core - compile - - - org.springframework.cloud - spring-cloud-starter-stream-kafka - - - org.springframework.cloud - spring-cloud-stream - test-jar - compile - test-binder - - - org.springframework.boot - spring-boot-starter-test - compile - - - com.github.mp911de.microbenchmark-runner - microbenchmark-runner-junit5 - ${microbenchmark-runner.version} - test - - - com.github.mp911de.microbenchmark-runner - microbenchmark-runner-extras - ${microbenchmark-runner.version} - test - - - org.openjdk.jmh - jmh-generator-annprocess - ${jmh.version} - provided - - - com.squareup.okhttp3 - okhttp - ${okhttp.version} - provided - - - io.zipkin.brave - brave-instrumentation-httpclient - - - org.apache.httpcomponents - httpclient - - - org.awaitility - awaitility - compile - - - - - - - maven-compiler-plugin - 3.8.1 - - ${maven.compiler.source} - ${maven.compiler.target} - - - - - - - - - jitpack.io - https://jitpack.io - - - 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 - - - - - - - jmeter - - false - - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring-boot.version} - - - - repackage - - - - start-non-sleuth-app - pre-integration-test - - start - - - "-Dserver.port=9875" - "-Dspring.sleuth.enabled=false" - - 8875 - true - - - - stop-non-sleuth-app - post-integration-test - - stop - - - 8875 - true - - - - start-sleuth-app - pre-integration-test - - start - - - "-Dserver.port=9876" - 8876 - true - - - - stop-sleuth-app - post-integration-test - - stop - - - 8876 - true - - - - - - maven-failsafe-plugin - 2.22.2 - - - - integration-test - verify - - - - - - com.lazerycode.jmeter - jmeter-maven-plugin - 3.1.1 - - false - false - true - - true - - true - - - - kg.apc - jmeter-plugins - - - - - - execute-jmeter-tests - - jmeter - - integration-test - - - - - kg.apc - jmeter-plugins - 1.0.0 - - - - kg.apc - perfmon - - - org.apache.hadoop - hadoop-core - - - org.apache.hbase - hbase - - - - - org.apache.jmeter - jorphan - - - org.apache.bsf - bsf-api - - - org.bouncycastle - bcmail-jdk15 - - - org.bouncycastle - bcprov-jdk15 - - - javax.activation - activation - - - commons-logging - commons-logging - - - - - - - com.lazerycode.jmeter - jmeter-analysis-maven-plugin - 1.0.6 - - - ${project.build.directory}/jmeter/results/analysis/ - - - - - create-html-report-for-asynchttp - verify - - analyze - - - - ${project.build.directory}/jmeter/results/AsyncHttpBenchmarks.jtl - - - - - create-html-report-for-async-method - verify - - analyze - - - - ${project.build.directory}/jmeter/results/AsyncMethodBenchmarks.jtl - - - - - create-html-report-for-sync-http - verify - - analyze - - - - ${project.build.directory}/jmeter/results/SyncHttpBenchmarks.jtl - - - - - create-html-report-for-nice-index-html - verify - - analyze - - - - ${project.build.directory}/jmeter/results/*.jtl - - - - - - - de.codecentric - jmeter-graph-maven-plugin - 0.1.0 - - - create-graph-threads-for-asynchttp - - create-graph - - verify - - - ${project.build.directory}/jmeter/results/AsyncHttpBenchmarks.jtl - - - - ResponseTimesOverTime - 800 - 600 - - ${project.build.directory}/jmeter/results/AsyncHttpBenchmarks-ResponseTimesOverTime.png - - - - ResponseTimesPercentiles - - 800 - 600 - - ${project.build.directory}/jmeter/results/AsyncHttpBenchmarks-ResponseTimesPercentiles.png - - - - LatenciesOverTime - 800 - 600 - - ${project.build.directory}/jmeter/results/AsyncHttpBenchmarks-LatenciesOverTime.png - - - - - - - create-graph-threads-for-async-method - - create-graph - - verify - - - ${project.build.directory}/jmeter/results/AsyncMethodBenchmarks.jtl - - - - ResponseTimesOverTime - 800 - 600 - - ${project.build.directory}/jmeter/results/AsyncMethodBenchmarks-ResponseTimesOverTime.png - - - - ResponseTimesPercentiles - - 800 - 600 - - ${project.build.directory}/jmeter/results/AsyncMethodBenchmarks-ResponseTimesPercentiles.png - - - - LatenciesOverTime - 800 - 600 - - ${project.build.directory}/jmeter/results/AsyncMethodBenchmarks-LatenciesOverTime.png - - - - - - - create-graph-threads-for-sync-http - - create-graph - - verify - - - ${project.build.directory}/jmeter/results/SyncHttpBenchmarks.jtl - - - - ResponseTimesOverTime - 800 - 600 - - ${project.build.directory}/jmeter/results/SyncHttpBenchmarks-ResponseTimesOverTime.png - - - - ResponseTimesPercentiles - - 800 - 600 - - ${project.build.directory}/jmeter/results/SyncHttpBenchmarks-ResponseTimesPercentiles.png - - - - LatenciesOverTime - 800 - 600 - - ${project.build.directory}/jmeter/results/SyncHttpBenchmarks-LatenciesOverTime.png - - - - - - - - - - - - + + + + 4.0.0 + + Benchmarks + Benchmarks (JMH) + org.springframework.cloud + 3.1.0-SNAPSHOT + benchmarks + + + org.springframework.boot + spring-boot-starter-parent + 2.4.7 + + + + + ${project.basedir}/.. + 3.2.1 + true + 1.8 + 1.8 + 4.9.0 + 0.2.0.RELEASE + 1.26 + + + + + + org.springframework.cloud + spring-cloud-sleuth-dependencies + ${project.version} + pom + import + + + org.springframework.cloud + spring-cloud-sleuth + ${project.version} + pom + import + + + + + + + org.springframework.cloud + spring-cloud-starter-sleuth + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-configuration-processor + + + + org.springframework + spring-test + compile + + + org.aspectj + aspectjrt + + + org.aspectj + aspectjweaver + + + org.assertj + assertj-core + compile + + + org.springframework.cloud + spring-cloud-starter-stream-kafka + + + org.springframework.cloud + spring-cloud-stream + test-jar + compile + test-binder + + + org.springframework.boot + spring-boot-starter-test + compile + + + com.github.mp911de.microbenchmark-runner + microbenchmark-runner-junit5 + ${microbenchmark-runner.version} + test + + + com.github.mp911de.microbenchmark-runner + microbenchmark-runner-extras + ${microbenchmark-runner.version} + test + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + provided + + + com.squareup.okhttp3 + okhttp + ${okhttp.version} + provided + + + io.zipkin.brave + brave-instrumentation-httpclient + + + org.apache.httpcomponents + httpclient + + + org.awaitility + awaitility + compile + + + + + + + maven-compiler-plugin + 3.8.1 + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + + + + jitpack.io + https://jitpack.io + + + 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 + + + + + + + jmeter + + false + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + + start-non-sleuth-app + pre-integration-test + + start + + + "-Dserver.port=9875" + "-Dspring.sleuth.enabled=false" + + 8875 + true + + + + stop-non-sleuth-app + post-integration-test + + stop + + + 8875 + true + + + + start-sleuth-app + pre-integration-test + + start + + + "-Dserver.port=9876" + 8876 + true + + + + stop-sleuth-app + post-integration-test + + stop + + + 8876 + true + + + + + + maven-failsafe-plugin + 2.22.2 + + + + integration-test + verify + + + + + + com.lazerycode.jmeter + jmeter-maven-plugin + 3.1.1 + + false + false + true + + true + + true + + + + kg.apc + jmeter-plugins + + + + + + execute-jmeter-tests + + jmeter + + integration-test + + + + + kg.apc + jmeter-plugins + 1.0.0 + + + + kg.apc + perfmon + + + org.apache.hadoop + hadoop-core + + + org.apache.hbase + hbase + + + + + org.apache.jmeter + jorphan + + + org.apache.bsf + bsf-api + + + org.bouncycastle + bcmail-jdk15 + + + org.bouncycastle + bcprov-jdk15 + + + javax.activation + activation + + + commons-logging + commons-logging + + + + + + + com.lazerycode.jmeter + jmeter-analysis-maven-plugin + 1.0.6 + + + ${project.build.directory}/jmeter/results/analysis/ + + + + + create-html-report-for-asynchttp + verify + + analyze + + + + ${project.build.directory}/jmeter/results/AsyncHttpBenchmarks.jtl + + + + + create-html-report-for-async-method + verify + + analyze + + + + ${project.build.directory}/jmeter/results/AsyncMethodBenchmarks.jtl + + + + + create-html-report-for-sync-http + verify + + analyze + + + + ${project.build.directory}/jmeter/results/SyncHttpBenchmarks.jtl + + + + + create-html-report-for-nice-index-html + verify + + analyze + + + + ${project.build.directory}/jmeter/results/*.jtl + + + + + + + de.codecentric + jmeter-graph-maven-plugin + 0.1.0 + + + create-graph-threads-for-asynchttp + + create-graph + + verify + + + ${project.build.directory}/jmeter/results/AsyncHttpBenchmarks.jtl + + + + ResponseTimesOverTime + 800 + 600 + + ${project.build.directory}/jmeter/results/AsyncHttpBenchmarks-ResponseTimesOverTime.png + + + + ResponseTimesPercentiles + + 800 + 600 + + ${project.build.directory}/jmeter/results/AsyncHttpBenchmarks-ResponseTimesPercentiles.png + + + + LatenciesOverTime + 800 + 600 + + ${project.build.directory}/jmeter/results/AsyncHttpBenchmarks-LatenciesOverTime.png + + + + + + + create-graph-threads-for-async-method + + create-graph + + verify + + + ${project.build.directory}/jmeter/results/AsyncMethodBenchmarks.jtl + + + + ResponseTimesOverTime + 800 + 600 + + ${project.build.directory}/jmeter/results/AsyncMethodBenchmarks-ResponseTimesOverTime.png + + + + ResponseTimesPercentiles + + 800 + 600 + + ${project.build.directory}/jmeter/results/AsyncMethodBenchmarks-ResponseTimesPercentiles.png + + + + LatenciesOverTime + 800 + 600 + + ${project.build.directory}/jmeter/results/AsyncMethodBenchmarks-LatenciesOverTime.png + + + + + + + create-graph-threads-for-sync-http + + create-graph + + verify + + + ${project.build.directory}/jmeter/results/SyncHttpBenchmarks.jtl + + + + ResponseTimesOverTime + 800 + 600 + + ${project.build.directory}/jmeter/results/SyncHttpBenchmarks-ResponseTimesOverTime.png + + + + ResponseTimesPercentiles + + 800 + 600 + + ${project.build.directory}/jmeter/results/SyncHttpBenchmarks-ResponseTimesPercentiles.png + + + + LatenciesOverTime + 800 + 600 + + ${project.build.directory}/jmeter/results/SyncHttpBenchmarks-LatenciesOverTime.png + + + + + + + + + + + + diff --git a/docs/pom.xml b/docs/pom.xml index da7f3999b..fd336d69f 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -1,82 +1,139 @@ - - - - 4.0.0 - - org.springframework.cloud - spring-cloud-sleuth - 3.0.4-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 + + + 2.22.3.Final + .*/spring-cloud-sleuth-instrumentation/.*|.*/spring-cloud-sleuth-brave/.* + + + + ${project.groupId} + spring-cloud-starter-sleuth + + + ${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 + + + + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + generate-sources + + add-source + + + + src/main/asciidoc + + + + + + + + + + docs + + + + pl.project13.maven + git-commit-id-plugin + + + maven-dependency-plugin + + + maven-resources-plugin + + + 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 + asciidoctor-maven-plugin + + + maven-antrun-plugin + + + maven-deploy-plugin + + + + + + diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 48122995d..6df436b44 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -5,20 +5,50 @@ |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.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. +|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 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.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 | `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. +|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 | `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. +|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. @@ -32,12 +62,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). @@ -45,12 +77,16 @@ |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.session.enabled | `true` | Enable Spring Session instrumentation. |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. @@ -59,6 +95,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/_spans.adoc b/docs/src/main/asciidoc/_spans.adoc new file mode 100644 index 000000000..bca12b758 --- /dev/null +++ b/docs/src/main/asciidoc/_spans.adoc @@ -0,0 +1,627 @@ +=== 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. +|=== + +=== 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. + +**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. +|=== + +=== 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. + +**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. +|=== + +=== 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. + +**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. +|=== + +=== 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. + +**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` + +=== 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 + +**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..4798f62e5 100644 --- a/docs/src/main/asciidoc/appendix.adoc +++ b/docs/src/main/asciidoc/appendix.adoc @@ -1,15 +1,22 @@ -: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[] \ 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/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/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..dd1a0a718 --- /dev/null +++ b/docs/src/main/asciidoc/index.adoc @@ -0,0 +1 @@ +include::spring-cloud-sleuth.adoc[] 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.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/integrations.adoc b/docs/src/main/asciidoc/integrations.adoc index 79c796170..21588a64c 100644 --- a/docs/src/main/asciidoc/integrations.adoc +++ b/docs/src/main/asciidoc/integrations.adoc @@ -3,7 +3,26 @@ 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 + +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 @@ -579,5 +598,144 @@ 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. -In order to disable this instrumentation set `spring.sleuth.circuitbreaker.enabled` to `false`. \ No newline at end of file +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`. + +[[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 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`. + +[[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`. + +[[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`. + +[[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`. + +[[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 + +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.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]. + +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]] +== 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`. + +[[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/docs/src/main/asciidoc/project-features.adoc b/docs/src/main/asciidoc/project-features.adoc index fb84d25c7..f871283cd 100644 --- a/docs/src/main/asciidoc/project-features.adoc +++ b/docs/src/main/asciidoc/project-features.adoc @@ -1,494 +1,506 @@ -[[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-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-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 + +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/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/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..b11eb31eb --- /dev/null +++ b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanEntry.java @@ -0,0 +1,110 @@ +/* + * 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 String prefix; + + final Collection tagKeys; + + final Collection events; + + 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"); + this.name = name; + this.enclosingClass = enclosingClass; + this.enumName = enumName; + this.description = description; + this.prefix = prefix; + 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 (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|==="); + } + 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..66f17122a --- /dev/null +++ b/docs/src/main/java/org/springframework/cloud/sleuth/documentation/SpanSearchingFileVisitor.java @@ -0,0 +1,234 @@ +/* + * 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(); + String prefix = ""; + 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 if ("prefix".equals(methodName)) { + prefix = readStringReturnValue(methodDeclaration); + } + } + return new SpanEntry(name, myEnum.getCanonicalName(), enumConstant.getName(), description, prefix, 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 5393834aa..14846526b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,428 +1,494 @@ - - - - - 4.0.0 - - spring-cloud-sleuth - 3.0.4-SNAPSHOT - pom - Spring Cloud Sleuth - Spring Cloud Sleuth - - - org.springframework.cloud - spring-cloud-build - 3.0.4-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.4-SNAPSHOT - 3.0.4-SNAPSHOT - 3.0.4-SNAPSHOT - 2.0.3-SNAPSHOT - 3.1.4-SNAPSHOT - 3.1.4-SNAPSHOT - 3.0.4-SNAPSHOT - 3.0.4-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 - - - - - - - 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.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} - - - - - - - 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.1.0-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.1.0-SNAPSHOT + 3.0.4-SNAPSHOT + 3.0.4-SNAPSHOT + 3.0.4-SNAPSHOT + 2.0.3-SNAPSHOT + 3.1.4-SNAPSHOT + 3.1.4-SNAPSHOT + 3.0.4-SNAPSHOT + 3.0.4-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 + 3.9.1 + 1.7 + 10.0.6 + 2.8.0 + 1.5.10 + + + true + false + 3.8.1 + 2.2 + 4.0.3 + 0.21.3 + 0.14.1 + 1.15.3 + 3.2.0 + + + + + + + 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 + + + maven-surefire-plugin + + + true + + + + + + + + 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} + + + 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 + ${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-api/pom.xml b/spring-cloud-sleuth-api/pom.xml index 90c393d69..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.4-SNAPSHOT + 3.1.0-SNAPSHOT .. 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..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. @@ -202,6 +213,16 @@ public interface Span extends SpanCustomizer { */ Builder remoteServiceName(String remoteServiceName); + /** + * Sets the remote URL for the span. + * @param ip remote service ip + * @param port remote service port + * @return this + */ + default Builder remoteIpAndPort(String ip, int port) { + return this; + } + /** * Builds and starts the span. * @return started span 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..d981cba7f --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/SpanAndScope.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; + +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}. + * + * @author Marcin Grzejszczak + * @since 3.1.0 + */ +public class SpanAndScope implements Closeable { + + private static final Log log = LogFactory.getLog(SpanAndScope.class); + + 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; + } + + @Override + public String toString() { + return "SpanAndScope{" + "span=" + this.span + '}'; + } + + @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/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/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-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/docs/AssertingSpan.java b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java new file mode 100644 index 000000000..0f9f47f23 --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpan.java @@ -0,0 +1,172 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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; + } + + @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 + * @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..de951bdc4 --- /dev/null +++ b/spring-cloud-sleuth-api/src/main/java/org/springframework/cloud/sleuth/docs/AssertingSpanBuilder.java @@ -0,0 +1,162 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 Span.Builder remoteIpAndPort(String ip, int port) { + getDelegate().remoteIpAndPort(ip, port); + 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; + } + + @Override + public String toString() { + return getDelegate().toString(); + } + }; + } + + /** + * @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/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-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-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-autoconfigure/pom.xml b/spring-cloud-sleuth-autoconfigure/pom.xml index 497c9aa02..aedbd9bf8 100644 --- a/spring-cloud-sleuth-autoconfigure/pom.xml +++ b/spring-cloud-sleuth-autoconfigure/pom.xml @@ -1,435 +1,523 @@ - - - - - 4.0.0 - - spring-cloud-sleuth-autoconfigure - jar - Spring Cloud Sleuth AutoConfigure - Spring Cloud Sleuth AutoConfigure - - - org.springframework.cloud - spring-cloud-sleuth - 3.0.4-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-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.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.boot + spring-boot-starter-batch + 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.boot + spring-boot-starter-data-cassandra + true + + + org.springframework.boot + spring-boot-starter-data-cassandra-reactive + true + + + org.springframework.cloud + spring-cloud-deployer-spi + true + + + io.reactivex + rxjava + true + + + io.r2dbc + r2dbc-proxy + 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.vault + 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.session + spring-session-data-redis + 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.projectreactor.kafka + reactor-kafka + 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.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 + + + + + + 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 + + + com.h2database + h2 + 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/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/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..c174467cc --- /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..03add3b2d --- /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/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..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 @@ -113,7 +113,9 @@ public class TraceAsyncDefaultAutoConfiguration { return this.beanFactory.getBean(TaskExecutor.class); } catch (NoUniqueBeanDefinitionException ex) { - log.debug("Could not find unique TaskExecutor bean", 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); 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..6261cc15c --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/batch/TraceBatchAutoConfiguration.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.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/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/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/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-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/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceAppDeployerBeanPostProcessor.java b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceAppDeployerBeanPostProcessor.java new file mode 100644 index 000000000..3229300ec --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceAppDeployerBeanPostProcessor.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.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; + +/** + * {@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-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..8c4905724 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/deployer/TraceDeployerAutoConfiguration.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.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.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/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..1ef0051e8 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/DataSourceProxyConfiguration.java @@ -0,0 +1,114 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 + * @author Chintan Radia + */ +@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())); + BeanUtils.copyProperties(originalProxy.getQuery(), props.getQuery()); + BeanUtils.copyProperties(originalProxy.getSlowQuery(), props.getSlowQuery()); + 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 new file mode 100644 index 000000000..68f05063e --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfiguration.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.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 new file mode 100644 index 000000000..1b4ce7be3 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyPropertiesSetter.java @@ -0,0 +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, + false); + 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 new file mode 100644 index 000000000..1788fddeb --- /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 - 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 new file mode 100644 index 000000000..cb982be83 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcAutoConfiguration.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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; +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, BraveAutoConfiguration.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 new file mode 100644 index 000000000..a813b8e1a --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/TraceJdbcProperties.java @@ -0,0 +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 = false; + + /** + * 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 = false; + + /** + * 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; + + 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; + } + + 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 = false; + + /** + * 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/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/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..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 @@ -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,18 +33,25 @@ 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 - @ConditionalOnMissingBean + @ConditionalOnProperty(value = "spring.sleuth.messaging.aspect.enabled", matchIfMissing = true) + TraceMessagingAspect traceMessagingAspect(Tracer tracer, SpanNamer spanNamer) { + return new TraceMessagingAspect(tracer, spanNamer); + } + + @Bean + @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/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/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..b92d88cc4 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/rsocket/TraceRSocketAutoConfiguration.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.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/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/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..52bf73685 --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/main/java/org/springframework/cloud/sleuth/autoconfig/instrument/task/TraceTaskAutoConfiguration.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.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; + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} that registers instrumentation for Spring Cloud Task. + * + * @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/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/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/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/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/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..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 @@ -1,135 +1,207 @@ -{ - "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 sleuth 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.kafka.enabled", + "type": "java.lang.Boolean", + "description": "Enable instrumenting of Apache Kafka clients.", + "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.web.tomcat.enabled", + "type": "java.lang.Boolean", + "description": "Enable tracing instrumentation for Tomcat.", + "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": 500 + }, + { + "name": "spring.sleuth.config.server.enabled", + "type": "java.lang.Boolean", + "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", + "description": "Enable Spring Batch instrumentation.", + "defaultValue": true + }, + { + "name": "spring.sleuth.r2dbc.enabled", + "type": "java.lang.Boolean", + "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.cassandra.enabled", + "type": "java.lang.Boolean", + "description": "Enable Cassandra 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/main/resources/META-INF/spring.factories b/spring-cloud-sleuth-autoconfigure/src/main/resources/META-INF/spring.factories index 1c5ba3eba..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,32 +1,46 @@ -# 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.circuitbreaker.TraceCircuitBreakerAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.rxjava.TraceRxJavaAutoConfiguration,\ -org.springframework.cloud.sleuth.autoconfig.instrument.quartz.TraceQuartzAutoConfiguration,\ -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.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,\ +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,\ +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,\ +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.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,\ +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,\ +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-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-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..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 @@ -16,6 +16,12 @@ 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; @@ -42,6 +48,7 @@ import org.springframework.context.annotation.Import; * @since 2.0.0 */ @Configuration(proxyBeanMethods = false) +@ConditionalOnSleuth @ConditionalOnProperty("spring.sleuth.noop.enabled") @AutoConfigureBefore(BraveAutoConfiguration.class) @Import(TraceConfiguration.class) @@ -86,3 +93,11 @@ public class TraceNoOpAutoConfiguration { } } + +@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/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..2f675933c --- /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-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 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..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,8 +29,12 @@ 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; 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 +63,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, 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 d874d7544..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,12 @@ 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; +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; @@ -145,8 +151,9 @@ public class WebClientTests { } @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration( - exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class }) + @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, + R2dbcAutoConfiguration.class, R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.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 18bf5a1dd..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,8 +25,12 @@ 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; 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 +54,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, 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/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-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..f9d79f63c --- /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.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 new file mode 100644 index 000000000..21f95af8c --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/P6SpyConfigurationTests.java @@ -0,0 +1,279 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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")); + + @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 new file mode 100644 index 000000000..cb7fdad1e --- /dev/null +++ b/spring-cloud-sleuth-autoconfigure/src/test/java/org/springframework/cloud/sleuth/autoconfig/instrument/jdbc/ProxyDataSourceConfigurationTests.java @@ -0,0 +1,286 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.SLF4JLogLevel; +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; +import static org.assertj.core.api.Assertions.filter; + +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 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) + .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", + "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) + .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", + "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) + .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", + "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) + .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", + "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) + .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); + }); + } + + @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 { + + @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..af03d9335 --- /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.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 new file mode 100644 index 000000000..1bc7cd052 --- /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.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/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/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..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,14 +26,16 @@ 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.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; 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; 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; @@ -45,8 +47,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() { @@ -92,9 +94,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 -> { @@ -114,9 +117,12 @@ public class TraceQuartzAutoConfigurationTest { } @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, - GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, - MongoAutoConfiguration.class, QuartzAutoConfiguration.class }) + @EnableAutoConfiguration( + exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, + GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, + MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.class, + R2dbcDataAutoConfiguration.class, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }, + excludeName = "org.springframework.cloud.gateway.config.GatewayRedisAutoConfiguration") public static class EnableAutoConfig { } @@ -135,16 +141,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-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/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/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-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-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..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,8 +29,12 @@ 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; 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 +124,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, 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 9e0bf7b62..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,8 +23,12 @@ 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; 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 +58,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, 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/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)); + } + +} 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..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,8 +20,12 @@ 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; 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 +47,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, RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) public static class Config { } 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-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..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,8 +22,12 @@ 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; 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; @@ -51,9 +55,12 @@ public class ZipkinSamplerTests { } @Configuration(proxyBeanMethods = false) - @EnableAutoConfiguration(exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, - GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, - MongoAutoConfiguration.class, QuartzAutoConfiguration.class }) + @EnableAutoConfiguration( + exclude = { GatewayClassPathWarningAutoConfiguration.class, GatewayAutoConfiguration.class, + GatewayMetricsAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class, + MongoAutoConfiguration.class, QuartzAutoConfiguration.class, R2dbcAutoConfiguration.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 7712f8c1c..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 +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, org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration diff --git a/spring-cloud-sleuth-brave/pom.xml b/spring-cloud-sleuth-brave/pom.xml index 0d709cbbf..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.0.4-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/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..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 @@ -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 @@ -92,13 +97,19 @@ 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"; } public static brave.Span toBrave(Span span) { - return ((BraveSpan) span).delegate; + return ((BraveSpan) AssertingSpan.unwrap(span)).delegate; } public static Span fromBrave(brave.Span span) { @@ -110,10 +121,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); } 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..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 @@ -107,6 +107,12 @@ class BraveSpanBuilder implements Span.Builder { return this; } + @Override + public Span.Builder remoteIpAndPort(String ip, int port) { + span().remoteIpAndPort(ip, port); + return this; + } + @Override public Span start() { if (this.startTimestamp > 0) { 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/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..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 @@ -90,6 +92,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/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-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-dependencies/pom.xml b/spring-cloud-sleuth-dependencies/pom.xml index f71f2d5f3..b8899a8d0 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.4-SNAPSHOT - - - spring-cloud-sleuth-dependencies - 3.0.4-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.1.0-SNAPSHOT + + + 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/spring-cloud-sleuth-instrumentation/pom.xml b/spring-cloud-sleuth-instrumentation/pom.xml index f7e966f9f..471bae1db 100644 --- a/spring-cloud-sleuth-instrumentation/pom.xml +++ b/spring-cloud-sleuth-instrumentation/pom.xml @@ -1,199 +1,362 @@ - - - - - 4.0.0 - - spring-cloud-sleuth-instrumentation - jar - Spring Cloud Sleuth Instrumentation - Spring Cloud Sleuth Instrumentation - - - org.springframework.cloud - spring-cloud-sleuth - 3.0.4-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 - - - 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-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.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 + + + org.springframework.boot + spring-boot-starter-rsocket + true + + + io.micrometer + micrometer-core + true + + + io.projectreactor + reactor-core + true + + + io.rsocket + rsocket-core + true + + + io.r2dbc + r2dbc-proxy + 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.boot + spring-boot-starter-batch + 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.boot + spring-boot-starter-data-cassandra + true + + + org.springframework.boot + spring-boot-starter-data-cassandra-reactive + 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.session + 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 + p6spy + true + + + net.ttddyy + datasource-proxy + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.awaitility + awaitility + test + + + com.tngtech.archunit + archunit-junit5 + test + + + io.projectreactor + reactor-test + test + + + + + + + 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 + + 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/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 0d11b88f9..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,14 +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())) { - 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/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..b461403cb --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceJobExecutionListener.java @@ -0,0 +1,73 @@ +/* + * 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; +import org.springframework.cloud.sleuth.docs.AssertingSpan; + +class TraceJobExecutionListener implements JobExecutionListener { + + private final Tracer tracer; + + private static final Map SPANS = new ConcurrentHashMap<>(); + + TraceJobExecutionListener(Tracer tracer) { + this.tracer = tracer; + } + + @Override + public void beforeJob(JobExecution jobExecution) { + 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)); + } + + @Override + public void afterJob(JobExecution jobExecution) { + SpanAndScope spanAndScope = SPANS.remove(jobExecution); + List throwables = jobExecution.getFailureExceptions(); + // @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/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..9bbdce678 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/batch/TraceStepExecutionListener.java @@ -0,0 +1,75 @@ +/* + * 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; +import org.springframework.cloud.sleuth.docs.AssertingSpan; + +class TraceStepExecutionListener implements StepExecutionListener { + + private final Tracer tracer; + + private static final Map SPANS = new ConcurrentHashMap<>(); + + TraceStepExecutionListener(Tracer tracer) { + this.tracer = tracer; + } + + @Override + public void beforeStep(StepExecution stepExecution) { + 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)); + } + + @Override + public ExitStatus afterStep(StepExecution stepExecution) { + SpanAndScope spanAndScope = SPANS.remove(stepExecution); + List throwables = stepExecution.getFailureExceptions(); + // @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)); + } + 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/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/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/TraceCircuitBreakerFactoryAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceCircuitBreakerFactoryAspect.java index d5f3fb3a5..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 @@ -19,13 +19,12 @@ 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; /** - * Aspec around {@link CircuitBreaker} creation. + * Aspect around {@link CircuitBreaker} creation. * * @author Marcin Grzejszczak * @since 3.0.0 @@ -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..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,18 +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/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..ecf8cce3c --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/circuitbreaker/TraceReactiveCircuitBreaker.java @@ -0,0 +1,75 @@ +/* + * 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 reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.client.circuitbreaker.ReactiveCircuitBreaker; +import org.springframework.cloud.sleuth.CurrentTraceContext; +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.instrument.reactor.ReactorSleuth; + +class TraceReactiveCircuitBreaker implements ReactiveCircuitBreaker { + + 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 ReactorSleuth.tracedMono(this.tracer, this.currentTraceContext, "function", mono); + } + + private Flux runAndTraceFlux(Supplier> flux) { + return ReactorSleuth.tracedFlux(this.tracer, this.currentTraceContext, "function", flux); + } + +} 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/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 new file mode 100644 index 000000000..f18d88f00 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/config/TraceEnvironmentRepositoryAspect.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.config; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import org.springframework.cloud.sleuth.Tracer; +import org.springframework.cloud.sleuth.docs.AssertingSpan; + +/** + * 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 { + // @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(); + } + finally { + findOneSpan.end(); + } + } + +} 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 new file mode 100644 index 000000000..f0c60f0d1 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployer.java @@ -0,0 +1,338 @@ +/* + * 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) + .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) { + 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/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..befaa4f62 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyBuilderCustomizer.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.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(); + } + 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..1a3954f3b --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceProxyProperties.java @@ -0,0 +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.instrument.jdbc; + +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +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; + + 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. + * + * @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 = false; + + /** + * 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 = false; + + /** + * 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..3a3a8511e --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/DataSourceWrapper.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.DataSource; + +import org.springframework.jdbc.datasource.DelegatingDataSource; + +/** + * Returns link of both real {@link DataSource}, decorated {@link DataSource}. + * + * @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..c89cbb2d2 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategy.java @@ -0,0 +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); + } + + } + +} 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..4e592d673 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceListenerStrategySpanCustomizer.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.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 dataSource data source for which we're building the 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..2206a287e --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TraceQueryExecutionListener.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.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; + if (source == null) { + return null; + } + 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..cef75651a --- /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 { + + /** + * Related to JDBC connections. + */ + CONNECTION, + + /** + * Related to query executions. + */ + QUERY, + + /** + * Related to ResultSets. + */ + FETCH + +} 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..77da0902f --- /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.1.0 + */ +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..55e06ae75 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaTracingUtils.java @@ -0,0 +1,50 @@ +/* + * 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.docs.AssertingSpanBuilder; +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) { + // @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); + } + span.end(); + } + +} 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/TracingKafkaConsumer.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumer.java new file mode 100644 index 000000000..4e655899e --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumer.java @@ -0,0 +1,335 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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 + * {@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.1.0 + */ +public class TracingKafkaConsumer implements Consumer { + + private final BeanFactory beanFactory; + + private final Consumer delegate; + + private Propagator propagator; + + private Propagator.Getter> extractor; + + public TracingKafkaConsumer(Consumer consumer, BeanFactory beanFactory) { + this.delegate = consumer; + 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 + 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, propagator(), extractor()); + } + return consumerRecords; + } + + @Override + public ConsumerRecords poll(Duration duration) { + ConsumerRecords consumerRecords = this.delegate.poll(duration); + for (ConsumerRecord consumerRecord : consumerRecords) { + KafkaTracingUtils.buildAndFinishSpan(consumerRecord, propagator(), 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/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 new file mode 100644 index 000000000..b10804a54 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducer.java @@ -0,0 +1,177 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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; + +/** + * 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.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 Tracer tracer; + + private Propagator propagator; + + private Propagator.Setter> injector; + + public TracingKafkaProducer(Producer producer, BeanFactory beanFactory) { + this.delegate = producer; + 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 + 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 = 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)) { + 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..0f2200483 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerFactory.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.producer.Producer; +import reactor.kafka.sender.KafkaSender; +import reactor.kafka.sender.SenderOptions; +import reactor.kafka.sender.internals.ProducerFactory; + +import org.springframework.beans.factory.BeanFactory; + +/** + * 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.1.0 + */ +public class TracingKafkaProducerFactory extends ProducerFactory { + + private final BeanFactory beanFactory; + + public TracingKafkaProducerFactory(BeanFactory beanFactory) { + super(); + this.beanFactory = beanFactory; + } + + @Override + public Producer createProducer(SenderOptions senderOptions) { + 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 new file mode 100644 index 000000000..b5a96cebf --- /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.1.0 + */ +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..d327235ca --- /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.1.0 + */ +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/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 6c9d84e84..19d163308 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 @@ -101,7 +101,8 @@ class TraceMessageHandler { static TraceMessageHandler forNonSpringIntegration(Tracer tracer, Propagator propagator, Propagator.Setter injector, Propagator.Getter extractor, List customizers) { - Function preSendFunction = span -> tracer.nextSpan(span).name("function").start(); + Function preSendFunction = span -> SleuthMessagingSpan.MESSAGING_SPAN.wrap(tracer.nextSpan(span)) + .name("function").start(); TriConsumer preSendMessageManipulator = (headers, parentSpan, childSpan) -> { headers.setHeader("traceHandlerParentSpan", parentSpan); headers.setHeader(Span.class.getName(), childSpan); @@ -148,8 +149,10 @@ class TraceMessageHandler { */ MessageAndSpans wrapInputMessage(Message message, String destinationName) { MessageHeaderAccessor headers = mutableHeaderAccessor(message); - Span.Builder consumerSpanBuilder = this.propagator.extract(headers, this.extractor); + Span.Builder consumerSpanBuilder = SleuthMessagingSpan.MESSAGING_SPAN + .wrap(this.propagator.extract(headers, this.extractor)); Span consumerSpan = consumerSpan(destinationName, consumerSpanBuilder, message); + if (log.isDebugEnabled()) { log.debug("Built a consumer span " + consumerSpan); } @@ -193,7 +196,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)); } } @@ -326,6 +330,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/TraceMessagingAspect.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/messaging/TraceMessagingAspect.java new file mode 100644 index 000000000..a2b262bc0 --- /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("@annotation(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/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 new file mode 100644 index 000000000..8de1d8ba2 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListener.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.r2dbc; + +import java.net.URI; + +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)) { + 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(); + } + + 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/reactor/ReactorHooksHelper.java b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/reactor/ReactorHooksHelper.java index 65821a78b..bee9fac26 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 @@ -47,13 +47,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. @@ -71,7 +71,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(); *} * @@ -84,6 +84,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 db6a784a0..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 @@ -16,9 +16,11 @@ package org.springframework.cloud.sleuth.instrument.reactor; +import java.util.function.BiConsumer; import java.util.function.BiFunction; 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,21 @@ 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 reactor.util.context.ContextView; 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; import static org.springframework.cloud.sleuth.instrument.reactor.ReactorHooksHelper.named; @@ -317,6 +324,299 @@ 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 BiConsumer spanCustomizer) { + return runMonoSupplierInScope(supplier, spanCustomizer).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 + * @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); + 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) + .doFinally(signalType -> { + span.end(); + scope.close(); + }); + // @formatter:on + }); + } + + /** + * 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, (o, span) -> { + }); + } + + /** + * 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, (o, 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. + * @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 BiConsumer 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 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. + * @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, (o, span1) -> { + }).contextWrite(context -> ReactorSleuth.putSpanInScope(tracer, context, span)); + } + + private static Flux runFluxSupplierInScope(Supplier> supplier, BiConsumer spanCustomizer) { + return Flux.deferContextual(contextView -> { + Span span = contextView.get(Span.class); + 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) + .doFinally(signalType -> { + span.end(); + scope.close(); + }); + // @formatter:on + }); + } + + /** + * 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, (o, span) -> { + }); + } + + 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 = spanSupplier.apply(null); + if (log.isDebugEnabled()) { + log.debug("There was no previous span in reactor context, created a new one [" + span + "]"); + } + } + 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()) { + log.debug("Found a trace context in reactor context [" + traceContext + "]"); + } + span = spanSupplier.apply(null); + if (log.isDebugEnabled()) { + log.debug("Created a child span [" + span + "]"); + } + } + } + else { + if (log.isDebugEnabled()) { + log.debug("Found a span in reactor context [" + span + "]"); + } + span = spanSupplier.apply(span); + if (log.isDebugEnabled()) { + log.debug("Created a child span [" + span + "]"); + } + } + 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 tracer tracer + * @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 + * 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 = childSpanFromContext(tracer, currentTraceContext, context, childSpanName); + return putSpanInScope(tracer, context, 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/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/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/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..90e3d6ec1 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingRequesterRSocketProxy.java @@ -0,0 +1,172 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.docs.AssertingSpanBuilder; +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 = 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); + } + 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 = 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); + } + 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..7cbc8e7df --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TracingResponderRSocketProxy.java @@ -0,0 +1,174 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.docs.AssertingSpanBuilder; +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 { + + 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 AssertingSpanBuilder + .of(SleuthRSocketSpan.RSOCKET_RESPONDER_SPAN, 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(); + 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.isDebug() || tracingMetadata.isSampled()).traceId(traceId) + .spanId(EncodingUtils.fromLong(tracingMetadata.spanId())) + .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); + } + +} 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/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/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 new file mode 100644 index 000000000..3b7b173c2 --- /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 = SleuthTaskSpan.TASK_RUNNER_SPAN.wrap(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..741d25ab8 --- /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 = SleuthTaskSpan.TASK_RUNNER_SPAN.wrap(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..c6b82724c --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/task/TraceTaskExecutionListener.java @@ -0,0 +1,90 @@ +/* + * 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 = 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"); + } + } + + @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/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 new file mode 100644 index 000000000..c12f605c8 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManager.java @@ -0,0 +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 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 new file mode 100644 index 000000000..2629e1660 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTags.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.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; + +final class TracePlatformTransactionManagerTags { + + private TracePlatformTransactionManagerTags() { + throw new IllegalStateException("Can't instantiate a utility class"); + } + + static void tag(Span span, TransactionDefinition def, Class transactionManagerClass) { + // @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) { + 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..b90e24dee --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/main/java/org/springframework/cloud/sleuth/instrument/tx/TraceReactiveTransactionManager.java @@ -0,0 +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 = 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/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/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/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/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..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 @@ -31,10 +31,13 @@ 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 */ + /** + * 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 5971fc1ef..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 @@ -31,14 +31,14 @@ 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 /** + * 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/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/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/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/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(); 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/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/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/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..45ee9aaf6 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/deployer/TraceAppDeployerTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.BeforeEach; +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.cloud.deployer.spi.core.RuntimeEnvironmentInfo; +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; +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()); + + @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())) + .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 SimpleCurrentTraceContext()); + return beanFactory; + } + + private Environment environment() { + return new MockEnvironment(); + } + +} 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/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..dcac1f099 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaConsumerTest.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 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.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +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; + + @Mock + Propagator.Getter> extractor; + + @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, + 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 new file mode 100644 index 000000000..568968727 --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/kafka/TracingKafkaProducerTest.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.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.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; + +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, + beanFactory()); + + 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, + beanFactory()); + + 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); + } + + 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/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..80100381d --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/r2dbc/TraceProxyExecutionListenerTests.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.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/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/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..c7b47457b --- /dev/null +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/instrument/tx/TracePlatformTransactionManagerTests.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.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.docs.AssertingSpan; +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 = AssertingSpan.unwrap(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/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/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/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/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpSpanInScope.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpSpanInScope.java index d629edc94..ac13149a4 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpSpanInScope.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpSpanInScope.java @@ -22,7 +22,7 @@ import org.springframework.cloud.sleuth.Tracer; * A noop implementation. Does nothing. * * @author Marcin Grzejszczak - * @since 3.0.4 + * @since 3.0.0 */ public class NoOpSpanInScope implements Tracer.SpanInScope { diff --git a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpTraceContext.java b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpTraceContext.java index c4b992773..c8fa01ac6 100644 --- a/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpTraceContext.java +++ b/spring-cloud-sleuth-instrumentation/src/test/java/org/springframework/cloud/sleuth/tracer/NoOpTraceContext.java @@ -22,7 +22,7 @@ import org.springframework.cloud.sleuth.TraceContext; * A noop implementation. Does nothing. * * @author Marcin Grzejszczak - * @since 3.0.4 + * @since 3.0.0 */ public class NoOpTraceContext implements TraceContext { 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 452979618..5fa21c0d3 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 @@ -42,7 +42,7 @@ public class SimpleSpan implements Span { public String remoteServiceName; - public Kind spanKind; + public Span.Kind spanKind; public List events = new ArrayList<>(); @@ -94,6 +94,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/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 ae160b945..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 @@ -28,7 +28,7 @@ import org.springframework.cloud.sleuth.TraceContext; * A noop implementation. Does nothing. * * @author Marcin Grzejszczak - * @since 3.0.4 + * @since 3.0.0 */ public class SimpleSpanBuilder implements Span.Builder { 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 0999ee328..666fd8795 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 @@ -95,6 +95,11 @@ public class SimpleTracer implements Tracer { return new SimpleSpanBuilder(this); } + @Override + public TraceContext.Builder traceContextBuilder() { + return null; + } + @Override public Map getAllBaggage() { return new HashMap<>(); 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/spring-cloud-sleuth-samples/pom.xml b/spring-cloud-sleuth-samples/pom.xml index e5a8bfa00..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.4-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 eb0bf8f5b..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.4-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 03b355e96..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.4-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 a90a64a0e..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.4-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 9e33b60f2..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.4-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 a4fd89c19..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.4-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 eb889cd40..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.4-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/spring-cloud-sleuth-zipkin/pom.xml b/spring-cloud-sleuth-zipkin/pom.xml index 0b3414aa2..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.4-SNAPSHOT + 3.1.0-SNAPSHOT .. 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(); + +} diff --git a/spring-cloud-starter-sleuth/pom.xml b/spring-cloud-starter-sleuth/pom.xml index ab1055058..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.4-SNAPSHOT + 3.1.0-SNAPSHOT .. spring-cloud-starter-sleuth diff --git a/tests/brave/pom.xml b/tests/brave/pom.xml index b2357cee3..50083a144 100644 --- a/tests/brave/pom.xml +++ b/tests/brave/pom.xml @@ -1,70 +1,80 @@ - - - - - 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.0.4-SNAPSHOT - .. - - - - spring-cloud-sleuth-instrumentation-annotation-tests - spring-cloud-sleuth-instrumentation-async-tests - spring-cloud-sleuth-instrumentation-baggage-tests - spring-cloud-sleuth-instrumentation-circuitbreaker-tests - spring-cloud-sleuth-instrumentation-feign-tests - spring-cloud-sleuth-instrumentation-gateway-tests - spring-cloud-sleuth-instrumentation-grpc-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-webflux-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-actuator-tests + 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-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 + 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 + spring-cloud-sleuth-instrumentation-rsocket-tests + spring-cloud-sleuth-zipkin-tests + + + + + + + + maven-deploy-plugin + + true + + + + + + + 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/brave/spring-cloud-sleuth-instrumentation-annotation-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-annotation-tests/pom.xml index c86c39229..2c12a1fcb 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.4-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 62991ccdd..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.4-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 dba3e37e5..e2a5b6625 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.4-SNAPSHOT + 3.1.0-SNAPSHOT .. 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..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,9 +21,13 @@ 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; 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 +76,9 @@ 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, + RedisAutoConfiguration.class, CassandraAutoConfiguration.class }) static class Config { @Bean 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/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/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-circuitbreaker-tests/pom.xml index 99bdd5b2a..0e29dd8db 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.4-SNAPSHOT + 3.1.0-SNAPSHOT .. 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/brave/spring-cloud-sleuth-instrumentation-feign-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-feign-tests/pom.xml index d3e933e7c..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.4-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 2562050e8..b648e3bc8 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.4-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 256434202..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.4-SNAPSHOT + 3.1.0-SNAPSHOT .. 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..0abe1b11e --- /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..e9bacd2b8 --- /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-kafka-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/pom.xml new file mode 100644 index 000000000..5692dd97b --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-kafka-tests/pom.xml @@ -0,0 +1,100 @@ + + + + + 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 + + + io.projectreactor + reactor-test + + + 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/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..46a7aaff3 --- /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(15)).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 new file mode 100644 index 000000000..a7e76064f --- /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,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); + } + +} 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..6b26069bf --- /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(15)).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..52b763679 --- /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(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-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/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/brave/spring-cloud-sleuth-instrumentation-lettuce-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-lettuce-tests/pom.xml index e05955b17..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.4-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 f7583b221..fe4a8b232 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.4-SNAPSHOT + 3.1.0-SNAPSHOT .. 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..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,10 +35,14 @@ 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; 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 +170,9 @@ 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, + 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 66a907885..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,15 +22,21 @@ 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; 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, RedisAutoConfiguration.class, + CassandraAutoConfiguration.class }) @ImportResource("classpath:beans/applicationContext.xml") @EnableIntegration @EnableAsync 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 52f058620..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.4-SNAPSHOT + 3.1.0-SNAPSHOT .. 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..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 @@ -16,8 +16,10 @@ 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; @@ -119,19 +121,25 @@ public class TraceAsyncIntegrationTests { Awaitility.await().atMost(5, SECONDS).untilAsserted(() -> { then(TraceAsyncIntegrationTests.this.classPerformingAsyncLogic.getSpan().context().traceId()) .isEqualTo(span.context().traceId()); - then(this.spans).hasSize(2); + List webSpans = this.spans.spans().stream() + .filter(mutableSpan -> mutableSpan.traceId().equalsIgnoreCase(span.context().traceIdString())) + .collect(Collectors.toList()); + then(webSpans).hasSize(2); // HTTP - then(this.spans.get(0).name()).isEqualTo("http:existing"); + then(webSpans.get(0).name()).isEqualTo("http:existing"); // ASYNC - then(this.spans.get(1).tags()).containsEntry("class", "ClassPerformingAsyncLogic").containsEntry("method", + 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); + 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"); @@ -140,6 +148,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); 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(); } 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..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 @@ -17,6 +17,7 @@ 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; @@ -49,7 +50,7 @@ public class Issue469Tests { then(e).hasMessageContaining("404"); } - then(this.spans).isNotEmpty(); + Awaitility.await().untilAsserted(() -> then(this.spans).isNotEmpty()); } private int port() { 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 bad8e7c2e..9e48b22f7 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.4-SNAPSHOT + 3.1.0-SNAPSHOT .. 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..687a3ab51 --- /dev/null +++ b/tests/brave/spring-cloud-sleuth-instrumentation-r2dbc-tests/src/test/resources/application.yml @@ -0,0 +1 @@ +logging.level.org.springframework.cloud: TRACE 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 b4efca33b..c1e357579 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.4-SNAPSHOT + 3.1.0-SNAPSHOT .. 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/brave/spring-cloud-sleuth-instrumentation-rxjava-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-rxjava-tests/pom.xml index 0ddbf23ee..0bca13699 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.4-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 681c81935..45d3de776 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.4-SNAPSHOT + 3.1.0-SNAPSHOT .. @@ -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/brave/spring-cloud-sleuth-instrumentation-webflux-tests/pom.xml b/tests/brave/spring-cloud-sleuth-instrumentation-webflux-tests/pom.xml index d8d4c02ea..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.4-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 4024f0f78..e55f844fd 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.4-SNAPSHOT + 3.1.0-SNAPSHOT .. diff --git a/tests/common/pom.xml b/tests/common/pom.xml index edd2d0aaf..cd6b92e72 100644 --- a/tests/common/pom.xml +++ b/tests/common/pom.xml @@ -1,201 +1,351 @@ - - - - - 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.0.4-SNAPSHOT - .. - - - - - org.springframework.cloud - spring-cloud-sleuth-instrumentation - - - org.springframework.cloud - spring-cloud-function-context - - true - - - 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-circuitbreaker-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.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 - - - 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.cloud + spring-cloud-function-context + true + + + org.springframework.integration + spring-integration-core + true + + + org.springframework.boot + spring-boot-starter-websocket + true + + + org.springframework.boot + spring-boot-starter-batch + 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 + reactor-test + true + + + io.projectreactor.kafka + reactor-kafka + true + + + org.springframework.boot + spring-boot-starter-data-r2dbc + true + + + io.r2dbc + r2dbc-proxy + true + + + org.testcontainers + testcontainers + true + + + org.testcontainers + junit-jupiter + true + + + org.testcontainers + kafka + true + + + org.springframework.boot + spring-boot-starter-rsocket + true + + + p6spy + p6spy + true + + + net.ttddyy + datasource-proxy + 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 + + + + + + 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/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/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/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"); + } + +} 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..8a954219e --- /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 + "/foo/default/main", + String.class); + log.info("Got [\n" + result + "\n]"); + } + + } + +} 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 new file mode 100644 index 000000000..2cdc6f977 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingJdbcEventListenerTests.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.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +import javax.sql.DataSource; + +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 org.springframework.cloud.sleuth.exporter.FinishedSpan; +import org.springframework.cloud.sleuth.test.TestSpanHandler; + +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.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.getTags()).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 new file mode 100644 index 000000000..a598e9504 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingListenerStrategyTests.java @@ -0,0 +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; + } + + } + +} 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 new file mode 100644 index 000000000..6897f4293 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/jdbc/TracingQueryExecutionListenerTests.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.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")); + } + +} 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..9dc76c744 --- /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(15)).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 new file mode 100644 index 000000000..8efc776be --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaProducerTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2013-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.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(); + + protected TestSpanHandler spans = tracerTest().handler(); + + 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 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); + kafkaProducer = new TracingKafkaProducer<>(new KafkaProducer<>(producerProperties), beanFactory); + consumerRun.set(true); + consumerRecords.clear(); + } + + @AfterEach + void destroy() { + 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<>(testTopic, "test", "test"); + + this.kafkaProducer.send(producerRecord, callback); + await().atMost(Duration.ofSeconds(15)).until(acknowledged::get); + + 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(testTopic)); + while (consumerRun.get()) { + ConsumerRecords records = kafkaConsumer.poll(Duration.ofSeconds(1)); + for (ConsumerRecord record : records) { + 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/KafkaReceiverTest.java b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/kafka/KafkaReceiverTest.java new file mode 100644 index 000000000..24a700620 --- /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(15)).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..896132dbe --- /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(15, 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); + } + +} 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 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..7ad6008b8 --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/r2dbc/R2dbcIntegrationTests.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.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/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/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(); 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..691591fca --- /dev/null +++ b/tests/common/src/main/java/org/springframework/cloud/sleuth/instrument/rsocket/TraceRSocketTests.java @@ -0,0 +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; + }); + } + + } + +} 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..cd55d5174 --- /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", "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 89fad7d8f..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 @@ -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,29 @@ 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(templatedName()); + } + + protected String templatedName() { + return "GET /prefix/{variable}/suffix"; + } + /** * Cancel before {@link Subscription#request(long)} means a network request was never * sent @@ -549,6 +573,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) 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() { 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() + } +} 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)); diff --git a/tests/pom.xml b/tests/pom.xml index 77baa0dce..04b8b898f 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -30,7 +30,7 @@ org.springframework.cloud spring-cloud-sleuth - 3.0.4-SNAPSHOT + 3.1.0-SNAPSHOT ..