{"id":15403616,"url":"https://github.com/rogervinas/spring-cloud-sleuth-in-action","last_synced_at":"2025-07-24T07:40:36.625Z","repository":{"id":44958562,"uuid":"330751802","full_name":"rogervinas/spring-cloud-sleuth-in-action","owner":"rogervinas","description":"🍀 Spring Cloud Sleuth in Action","archived":false,"fork":false,"pushed_at":"2025-07-02T08:10:52.000Z","size":303,"stargazers_count":3,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-02T09:26:18.654Z","etag":null,"topics":["kafka","spring-boot-application","spring-cloud-sleuth","zipkin"],"latest_commit_sha":null,"homepage":"https://dev.to/rogervinas/spring-cloud-sleuth-in-action-10k2","language":"Kotlin","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rogervinas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2021-01-18T18:20:52.000Z","updated_at":"2025-07-02T08:10:55.000Z","dependencies_parsed_at":"2023-12-28T11:31:49.431Z","dependency_job_id":"b72849b2-eb0f-4730-bc20-de5894416b28","html_url":"https://github.com/rogervinas/spring-cloud-sleuth-in-action","commit_stats":{"total_commits":63,"total_committers":4,"mean_commits":15.75,"dds":"0.46031746031746035","last_synced_commit":"f15b2d3882bbef48087ad504fd0b3069522919f0"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/rogervinas/spring-cloud-sleuth-in-action","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fspring-cloud-sleuth-in-action","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fspring-cloud-sleuth-in-action/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fspring-cloud-sleuth-in-action/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fspring-cloud-sleuth-in-action/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rogervinas","download_url":"https://codeload.github.com/rogervinas/spring-cloud-sleuth-in-action/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rogervinas%2Fspring-cloud-sleuth-in-action/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266808556,"owners_count":23987450,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-24T02:00:09.469Z","response_time":99,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["kafka","spring-boot-application","spring-cloud-sleuth","zipkin"],"created_at":"2024-10-01T16:09:28.289Z","updated_at":"2025-07-24T07:40:36.616Z","avatar_url":"https://github.com/rogervinas.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"![CI](https://github.com/rogervinas/spring-cloud-sleuth-in-action/actions/workflows/ci.yml/badge.svg)\n![Java](https://img.shields.io/badge/Java-17-blue?labelColor=black)\n![Kotlin](https://img.shields.io/badge/Kotlin-2.x-blue?labelColor=black)\n![SpringBoot](https://img.shields.io/badge/SpringBoot-2.7.18-blue?labelColor=black)\n![SpringCloud](https://img.shields.io/badge/SpringCloud-2021.0.8-blue?labelColor=black)\n\n# Spring Cloud Sleuth in action\n\n[Spring Cloud Sleuth](https://docs.spring.io/spring-cloud-sleuth/docs/current/reference/html/index.html) is the solution for [distributed tracing](https://opentracing.io/docs/overview/what-is-tracing/) provided by **Spring** and comes with a bunch of useful [integrations](https://docs.spring.io/spring-cloud-sleuth/docs/current/reference/html/integrations.html#sleuth-integration) out of the box\n\nI've created this [spring-cloud-sleuth-in-action](https://github.com/rogervinas/spring-cloud-sleuth-in-action) sample to use some of these integrations executing the following flow:\n\n![Demo](doc/demo.png)\n\nTo keep it simple everything will be executed within the same **Spring Boot Application** but at the end it is the same as if it was splitted between different services\n\n## Demo time!\n\nLet's follow these steps to execute the demo:\n\n* Run docker-compose:\n```shell\ndocker-compose up -d\n```\n\n* Start the **Spring Boot Application**:\n```shell\n./gradlew bootRun\n```\n\n* Consume from the **Kafka** topic `my.topic` with [kcat](https://github.com/edenhill/kcat):\n```shell\nkcat -b localhost:9092 -C -t my.topic -f '%h %s\\n'\n```\n\n* Execute a request to the first endpoint with [curl](https://curl.se/) or any other tool you like:\n```shell\ncurl http://localhost:8080/request1?payload=hello \\\n  -H 'X-B3-TraceId: aaaaaa1234567890' \\\n  -H 'X-B3-SpanId: bbbbbb1234567890'\n```\n\nNote: the default format for [context propagation](https://docs.spring.io/spring-cloud-sleuth/docs/current/reference/html/project-features.html#features-context-propagation) is [B3](https://github.com/openzipkin/b3-propagation), so we use headers `X-B3-TraceId` and `X-B3-SpanId`\n\n* Check application output, all lines should share the same `traceId`\n```\nStarted MyApplicationKt in 44.739 seconds (JVM running for 49.324) - traceId ? spanId ? - main\n\u003e\u003e\u003e RestRequest1 hello  - traceId aaaaaa1234567890 spanId cf596e6281432fb9 - http-nio-8080-exec-7\n\u003e\u003e\u003e KafkaProducer hello - traceId aaaaaa1234567890 spanId cf596e6281432fb9 - http-nio-8080-exec-7\n\u003e\u003e\u003e KafkaConsumer hello - traceId aaaaaa1234567890 spanId 91e1b6b37334620c - KafkaConsumerDestination...\n\u003e\u003e\u003e RestRequest2 hello  - traceId aaaaaa1234567890 spanId a1ac0233664f5249 - http-nio-8080-exec-8\n\u003e\u003e\u003e RestRequest3 hello  - traceId aaaaaa1234567890 spanId bf384c3b4d97efe9 - http-nio-8080-exec-9\n\u003e\u003e\u003e RestRequest4 hello  - traceId aaaaaa1234567890 spanId c84470ce03e993f1 - http-nio-8080-exec-1\n\u003e\u003e\u003e AsyncService hello  - traceId aaaaaa1234567890 spanId acccead477b4e1c8 - task-3\n```\n\n* Check [kcat](https://github.com/edenhill/kcat) output:\n```\nb3=aaaaaa1234567890-331986280d41ccdc-1,\nnativeHeaders={\"b3\":[\"aaaaaa1234567890-331986280d41ccdc-1\"]},\ncontentType=application/json,\nspring_json_header_types={\n    \"b3\":\"java.lang.String\",\n    \"nativeHeaders\":\"org.springframework.util.LinkedMultiValueMap\",\n    \"contentType\":\"java.lang.String\"\n}\nhello\n```\n\n* Check [zipkin](https://zipkin.io/) at [http://localhost:9411/zipkin/](http://localhost:9411/zipkin/)\n\n![Zipkin](doc/zipkin.png)\n\n* Stop the **Spring Boot Application** just with CTRL-C\n\n* Stop docker-compose:\n```shell\ndocker-compose down\n```\n\n## Show me the code!\n\nThis demo was created using this [spring initializr configuration](https://start.spring.io/#!type=gradle-project\u0026language=kotlin\u0026platformVersion=2.4.2.RELEASE\u0026packaging=jar\u0026jvmVersion=11\u0026groupId=com.example\u0026artifactId=demo\u0026name=demo\u0026description=Demo%20project%20for%20Spring%20Boot\u0026packageName=com.example.demo\u0026dependencies=web,cloud-starter-sleuth,webflux,cloud-starter-zipkin,cloud-stream)\n\nJust adding the **sleuth** dependency will enable tracing by default in any of the supported [integrations](https://docs.spring.io/spring-cloud-sleuth/docs/current/reference/html/integrations.html#sleuth-integration), so as you will see no extra coding is needed (maybe only a few exceptions) \n\n### Logging\n\nWe need to add **traceId** and **spanId** values to the application log. In production we would use the [logstash-logback-encoder](https://github.com/logstash/logstash-logback-encoder) to generate logs in JSON format and send them to an [ELK](https://www.elastic.co/what-is/elk-stack) but for the demo we use this plain text [logback layout](http://logback.qos.ch/manual/layouts.html):\n\n```xml\n\u003cconfiguration\u003e\n    \u003cappender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\"\u003e\n        \u003cencoder\u003e\n            \u003cpattern\u003e%msg - traceId %X{traceId:-?} spanId %X{spanId:-?} - %thread%n\u003c/pattern\u003e\n        \u003c/encoder\u003e\n    \u003c/appender\u003e\n    \u003croot level=\"ERROR\"\u003e\n        \u003cappender-ref ref=\"STDOUT\" /\u003e\n    \u003c/root\u003e\n\u003c/configuration\u003e\n```\n\n### RestController\n\nCreate your @RestController as usual\n\n```kotlin\n@RestController\nclass MyRestController {\n\n  companion object {\n    private val LOGGER = LoggerFactory.getLogger(MyRestController::class.java)\n  }\n\n  @GetMapping(\"/request1\")\n  fun request1(@RequestParam(\"payload\") payload: String): String {\n    LOGGER.info(\"\u003e\u003e\u003e RestRequest1 $payload\")\n    // do more stuff\n    return \"ok\"\n  }\n}\n```\n\n### Kafka Producer \u0026 Consumer\n\nWe have a few alternatives to propagate tracing information when publishing to **kafka**\n\nFor example, we can use [Spring for Apache Kafka](https://docs.spring.io/spring-kafka/docs/current/reference/html) and create a **KafkaProducer** or **KafkaConsumer** using the autoconfigured **KafkaProducerFactory** or **KafkaConsumerFactory**. We can use the autoconfigured **KafkaTemplate** too\n\nIn this demo we use [Spring Cloud Stream](https://docs.spring.io/spring-cloud-stream/docs/3.1.0/reference/html/) and [Reactive Functions Support](https://docs.spring.io/spring-cloud-stream/docs/3.1.0/reference/html/spring-cloud-stream.html#_reactive_functions_support)\n\n* Configure binding and function definitions:\n\n    ```yaml\n    spring:\n      cloud:\n        stream:\n          kafka:\n            binder:\n              brokers: \"localhost:9092\"\n          bindings:\n            consumer-in-0:\n              group: ${spring.application.name}\n              destination: \"my.topic\"\n            producer-out-0:\n              destination: \"my.topic\"\n        function:\n          definition: consumer;producer\n    ```\n\n* The consumer is just a @Bean implementing a lambda consuming a `\u003cMessage\u003cPAYLOAD\u003e\u003e`:\n\n    ```kotlin\n    @Component(\"consumer\")\n    class MyKafkaConsumer: (Message\u003cString\u003e) -\u003e Unit {\n    \n      companion object {\n        private val LOGGER = LoggerFactory.getLogger(MyKafkaConsumer::class.java)\n      }\n    \n      override fun invoke(message: Message\u003cString\u003e) {\n        LOGGER.info(\"\u003e\u003e\u003e KafkaConsumer ${message.payload}\")\n        // do more stuff\n      }\n    }\n    ```\n\n* The producer is just a @Bean implementing a lambda producing a `Flux\u003cMessage\u003cPAYLOAD\u003e\u003e`:\n\n    In this case we have to use [MessagingSleuthOperators](https://docs.spring.io/spring-cloud-sleuth/docs/current/reference/html/integrations.html#sleuth-messaging-spring-cloud-function-integration) helper methods in order to preserve the tracing context when using reactive stream functions\n\n    ```kotlin\n    @Component(\"producer\")\n    class MyKafkaProducer(private val beanFactory: BeanFactory) : () -\u003e Flux\u003cMessage\u003cString\u003e\u003e {\n    \n      companion object {\n        private val LOGGER = LoggerFactory.getLogger(MyKafkaProducer::class.java)\n      }\n    \n      private val sink = Sinks.many().unicast().onBackpressureBuffer\u003cMessage\u003cString\u003e\u003e()\n    \n      fun produce(payload: String) {\n        LOGGER.info(\"\u003e\u003e\u003e KafkaProducer $payload\")\n        sink.emitNext(createMessageWithTracing(payload), FAIL_FAST)\n      }\n    \n      private fun createMessageWithTracing(payload: String): Message\u003cString\u003e {\n        return MessagingSleuthOperators.handleOutputMessage(\n          beanFactory,\n          MessagingSleuthOperators.forInputMessage(beanFactory, GenericMessage(payload))\n        )\n      }\n    \n      override fun invoke() = sink.asFlux()\n    }\n    ```\n\n### RestTemplate\n\nJust create a RestTemplate @Bean and inject it wherever is needed\n\n```kotlin\n@Configuration\nclass MyConfiguration {\n  @Bean\n  fun restTemplate() = RestTemplate()\n}\n```\n\n### FeignClient\n\nJust declare the @FeignClient as usual\n\n```kotlin\n@SpringBootApplication\n@EnableFeignClients\nclass MyApplication\n\n@FeignClient(name = \"request3\", url = \"http://localhost:\\${server.port}\")\ninterface MyFeignClient {\n  @RequestMapping(method = [RequestMethod.GET], path = [\"/request3\"])\n  fun request3(@RequestParam(\"payload\") payload: String) : String\n}\n```\n\n### WebClient\n\nJust create a WebClient @Bean and inject it wherever is needed\n\n```kotlin\n@Configuration\nclass MyConfiguration {\n  @Bean\n  fun webClient() = WebClient.create()\n}\n```\n\n### Async\n\nJust annotate the method with @Async as usual. Tracing context will be preserved between threads\n\n```kotlin\n@SpringBootApplication\n@EnableAsync\nclass MyApplication\n\n@Service\nclass MyAsyncService {\n\n  companion object {\n    private val LOGGER = LoggerFactory.getLogger(MyAsyncService::class.java)\n  }\n\n  @Async   \n  fun execute(payload: String): CompletableFuture\u003cString\u003e {\n    LOGGER.info(\"\u003e\u003e\u003e AsyncService $payload\")\n    return CompletableFuture.completedFuture(\"ok\")\n  }\n}\n```\n\n## Zipkin\n\nIn production we would send to [zipkin](https://zipkin.io/) a small percentage of all the traces (sampling) but for the demo we will send all of them:\n\n```yaml\nspring:\n  sleuth:\n    sampler:\n      probability: 1.0\n  zipkin:\n    base-url: \"http://localhost:9411\"\n```\n\n### Test\n\nOne easy way to test the demo is running a SpringBootTest with an [OutputCaptureExtension](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/system/OutputCaptureExtension.html) and verify that all logs contain the expected **traceId** and **spanId** values: \n\n```kotlin\n@SpringBootTest(webEnvironment = DEFINED_PORT)\n@Testcontainers\n@ExtendWith(OutputCaptureExtension::class)\nclass MyApplicationIntegrationTest {\n  @Test\n  fun `should propagate tracing`(log: CapturedOutput) {\n    val traceId = \"edb77ece416b3196\"\n    val spanId = \"c58ac2aa66d238b9\"\n\n    val response = request1(traceId, spanId)\n\n    assertThat(response.statusCode).isEqualTo(OK)\n    assertThat(response.body).isEqualTo(\"ok\")\n\n    val logLines = await()\n      .atMost(TEN_SECONDS)\n      .pollDelay(ONE_SECOND)\n      .until({ parseLogLines(log) }, { it.size \u003e= 7 })\n\n    assertThatLogLineContainsMessageAndTraceId(logLines[0], \"RestRequest1 hello\", traceId)\n    assertThatLogLineContainsMessageAndTraceId(logLines[1], \"KafkaProducer hello\", traceId)\n    assertThatLogLineContainsMessageAndTraceId(logLines[2], \"KafkaConsumer hello\", traceId)\n    assertThatLogLineContainsMessageAndTraceId(logLines[3], \"RestRequest2 hello\", traceId)\n    assertThatLogLineContainsMessageAndTraceId(logLines[4], \"RestRequest3 hello\", traceId)\n    assertThatLogLineContainsMessageAndTraceId(logLines[5], \"RestRequest4 hello\", traceId)\n    assertThatLogLineContainsMessageAndTraceId(logLines[6], \"AsyncService hello\", traceId)\n  }\n}\n```\n\nRun test with `./gradlew test`\n\nThat's it! Happy coding! 💙\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frogervinas%2Fspring-cloud-sleuth-in-action","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frogervinas%2Fspring-cloud-sleuth-in-action","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frogervinas%2Fspring-cloud-sleuth-in-action/lists"}