1. 概述

本文将介绍 Spring Cloud Sleuth,这是一个强大的日志增强工具,特别适用于由多个服务构建的系统。不过本教程将聚焦于 在单体应用中使用 Sleuth,而非跨微服务场景。

我们都曾有过这样的痛苦经历:尝试诊断定时任务、多线程操作或复杂 Web 请求的问题。即使有日志,也很难确定哪些操作属于同一个请求,导致:

  • 难以诊断复杂操作,甚至无法追踪
  • 常见的解决方案是手动传递唯一 ID 来关联日志

这时 Sleuth 就派上用场了。它能自动为特定任务、线程或请求的日志添加唯一标识符,无缝集成 Logback 和 SLF4J 等日志框架,让问题追踪变得轻而易举。

2. 环境搭建

2.1 添加依赖

在 Spring Boot 项目的 pom.xml 中添加 Sleuth 依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

最新版本可在 Maven 仓库 查询。

2.2 配置应用名称

application.properties 中设置应用名,方便 Sleuth 识别日志来源:

spring.application-name=Baeldung Sleuth Tutorial

3. Sleuth 核心配置

Sleuth 能在多种场景下增强日志。从 2.0.0 版本开始,它使用 Brave 作为追踪库,为每个 Web 请求添加唯一 ID,并支持跨线程边界传递这些 ID。

3.1 关键概念

  • Trace(追踪):代表一个完整的请求或任务(如用户的一次 Web 请求)
  • Span(跨度):代表请求中的某个步骤(如数据库查询、远程调用)
  • ID 作用
    • traceId:关联整个请求的所有日志
    • spanId:标识请求中的具体步骤
    • export:布尔值,表示是否导出到 Zipkin 等聚合工具

3.2 基础 Web 请求示例

创建一个简单的 Controller:

@RestController
public class SleuthController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @GetMapping("/")
    public String helloSleuth() {
        logger.info("Hello Sleuth");
        return "success";
    }
}

启动应用后访问 http://localhost:8080,日志会显示类似内容:

2017-01-10 22:36:38.254  INFO 
  [Baeldung Sleuth Tutorial,4e30f7340b3fb631,4e30f7340b3fb631,false] 12516 
  --- [nio-8080-exec-1] c.b.spring.session.SleuthController : Hello Sleuth

日志格式解析: [应用名,traceId,spanId,export]

关键点:所有日志自动携带追踪信息,无需手动编码

3.3 服务调用场景

创建一个 Service 类:

@Service
public class SleuthService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    public void doSomeWorkSameSpan() {
        Thread.sleep(1000L);
        logger.info("Doing some work");
    }
}

在 Controller 中调用该服务:

@Autowired
private SleuthService sleuthService;

@GetMapping("/same-span")
public String helloSleuthSameSpan() throws InterruptedException {
    logger.info("Same Span");
    sleuthService.doSomeWorkSameSpan();
    return "success";
}

访问 http://localhost:8080/same-span 后日志:

2017-01-10 22:51:47.664  INFO 
  [Baeldung Sleuth Tutorial,b77a5ea79036d5b9,b77a5ea79036d5b9,false] 12516 
  --- [nio-8080-exec-3] c.b.spring.session.SleuthController      : Same Span
2017-01-10 22:51:48.664  INFO 
  [Baeldung Sleuth Tutorial,b77a5ea79036d5b9,b77a5ea79036d5b9,false] 12516 
  --- [nio-8080-exec-3] c.baeldung.spring.session.SleuthService  : Doing some work

⚠️ 注意:不同类的日志共享相同的 traceIdspanId,证明它们属于同一请求

3.4 手动创建 Span

添加新的 Controller 方法:

@GetMapping("/new-span")
public String helloSleuthNewSpan() {
    logger.info("New Span");
    sleuthService.doSomeWorkNewSpan();
    return "success";
}

在 Service 中手动创建 Span:

@Autowired
private Tracer tracer;

public void doSomeWorkNewSpan() throws InterruptedException {
    logger.info("I'm in the original span");

    Span newSpan = tracer.nextSpan().name("newSpan").start();
    try (SpanInScope ws = tracer.withSpanInScope(newSpan.start())) {
        Thread.sleep(1000L);
        logger.info("I'm in the new span doing some cool work that needs its own span");
    } finally {
        newSpan.finish();
    }

    logger.info("I'm in the original span");
}

访问 http://localhost:8080/new-span 后日志:

2017-01-11 21:07:54.924  
  INFO [Baeldung Sleuth Tutorial,9cdebbffe8bbbade,9cdebbffe8bbbade,false] 12516 
  --- [nio-8080-exec-6] c.b.spring.session.SleuthController      : New Span
2017-01-11 21:07:54.924  
  INFO [Baeldung Sleuth Tutorial,9cdebbffe8bbbade,9cdebbffe8bbbade,false] 12516 
  --- [nio-8080-exec-6] c.baeldung.spring.session.SleuthService  : 
  I'm in the original span
2017-01-11 21:07:55.924  
  INFO [Baeldung Sleuth Tutorial,9cdebbffe8bbbade,1e706f252a0ee9c2,false] 12516 
  --- [nio-8080-exec-6] c.baeldung.spring.session.SleuthService  : 
  I'm in the new span doing some cool work that needs its own span
2017-01-11 21:07:55.924  
  INFO [Baeldung Sleuth Tutorial,9cdebbffe8bbbade,9cdebbffe8bbbade,false] 12516 
  --- [nio-8080-exec-6] c.baeldung.spring.session.SleuthService  : 
  I'm in the original span

关键点:第三条日志拥有相同的 traceId 但不同的 spanId,实现细粒度追踪

3.5 多线程追踪

3.5.1 配置线程池

@Configuration
public class ThreadConfig {

    @Autowired
    private BeanFactory beanFactory;

    @Bean
    public Executor executor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(1);
        threadPoolTaskExecutor.setMaxPoolSize(1);
        threadPoolTaskExecutor.initialize();

        return new LazyTraceExecutor(beanFactory, threadPoolTaskExecutor);
    }
}

⚠️ 注意:必须使用 LazyTraceExecutor 才能自动传递追踪信息

3.5.2 线程任务示例

在 Controller 中添加:

@Autowired
private Executor executor;

@GetMapping("/new-thread")
public String helloSleuthNewThread() {
    logger.info("New Thread");
    Runnable runnable = () -> {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.info("I'm inside the new thread - with a new span");
    };
    executor.execute(runnable);

    logger.info("I'm done - with the original span");
    return "success";
}

访问 http://localhost:8080/new-thread 后日志:

2017-01-11 21:18:15.949  
  INFO [Baeldung Sleuth Tutorial,96076a78343c364d,96076a78343c364d,false] 12516 
  --- [nio-8080-exec-9] c.b.spring.session.SleuthController      : New Thread
2017-01-11 21:18:15.950  
  INFO [Baeldung Sleuth Tutorial,96076a78343c364d,96076a78343c364d,false] 12516 
  --- [nio-8080-exec-9] c.b.spring.session.SleuthController      : 
  I'm done - with the original span
2017-01-11 21:18:16.953  
  INFO [Baeldung Sleuth Tutorial,96076a78343c364d,e3b6a68013ddfeea,false] 12516 
  --- [lTaskExecutor-1] c.b.spring.session.SleuthController      : 
  I'm inside the new thread - with a new span

关键点:新线程自动继承 traceId 并生成新的 spanId

3.6 @Async 支持

3.6.1 配置异步执行器

@Configuration
@EnableAsync
public class ThreadConfig extends AsyncConfigurerSupport {
    
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(1);
        threadPoolTaskExecutor.setMaxPoolSize(1);
        threadPoolTaskExecutor.initialize();

        return new LazyTraceExecutor(beanFactory, threadPoolTaskExecutor);
    }
}

3.6.2 异步方法示例

在 Service 中添加:

@Async
public void asyncMethod() {
    logger.info("Start Async Method");
    Thread.sleep(1000L);
    logger.info("End Async Method");
}

在 Controller 中调用:

@GetMapping("/async")
public String helloSleuthAsync() {
    logger.info("Before Async Method Call");
    sleuthService.asyncMethod();
    logger.info("After Async Method Call");
    
    return "success";
}

访问 http://localhost:8080/async 后日志:

2017-01-11 21:30:40.621  
  INFO [Baeldung Sleuth Tutorial,c187f81915377fff,c187f81915377fff,false] 10072 
  --- [nio-8080-exec-2] c.b.spring.session.SleuthController      : 
  Before Async Method Call
2017-01-11 21:30:40.622  
  INFO [Baeldung Sleuth Tutorial,c187f81915377fff,c187f81915377fff,false] 10072 
  --- [nio-8080-exec-2] c.b.spring.session.SleuthController      : 
  After Async Method Call
2017-01-11 21:30:40.622  
  INFO [Baeldung Sleuth Tutorial,c187f81915377fff,8a9f3f097dca6a9e,false] 10072 
  --- [lTaskExecutor-1] c.baeldung.spring.session.SleuthService  : 
  Start Async Method
2017-01-11 21:30:41.622  
  INFO [Baeldung Sleuth Tutorial,c187f81915377fff,8a9f3f097dca6a9e,false] 10072 
  --- [lTaskExecutor-1] c.baeldung.spring.session.SleuthService  : 
  End Async Method

关键点:异步方法自动继承 traceId 并生成新 spanId

3.7 @Scheduled 支持

3.7.1 配置定时任务

@Configuration
@EnableAsync
@EnableScheduling
public class ThreadConfig extends AsyncConfigurerSupport
  implements SchedulingConfigurer {
 
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        scheduledTaskRegistrar.setScheduler(schedulingExecutor());
    }

    @Bean(destroyMethod = "shutdown")
    public Executor schedulingExecutor() {
        return Executors.newScheduledThreadPool(1);
    }
}

3.7.2 定时任务示例

创建定时任务服务:

@Service
public class SchedulingService {

    private Logger logger = LoggerFactory.getLogger(getClass());
 
    @Autowired
    private SleuthService sleuthService;

    @Scheduled(fixedDelay = 30000)
    public void scheduledWork() throws InterruptedException {
        logger.info("Start some work from the scheduled task");
        sleuthService.asyncMethod();
        logger.info("End work from scheduled task");
    }
}

任务执行日志:

2017-01-11 21:30:58.866  
  INFO [Baeldung Sleuth Tutorial,3605f5deaea28df2,3605f5deaea28df2,false] 10072 
  --- [pool-1-thread-1] c.b.spring.session.SchedulingService     : 
  Start some work from the scheduled task
2017-01-11 21:30:58.866  
  INFO [Baeldung Sleuth Tutorial,3605f5deaea28df2,3605f5deaea28df2,false] 10072 
  --- [pool-1-thread-1] c.b.spring.session.SchedulingService     : 
  End work from scheduled task

关键点:每个定时任务自动生成独立的 traceIdspanId

4. 总结

通过本文我们了解到 Spring Cloud Sleuth 在单体应用中的强大能力:

核心优势

  • 自动关联日志:通过 traceId 关联同一请求的所有日志
  • 细粒度追踪:使用 spanId 标识请求中的具体步骤
  • 多线程支持:自动传递追踪信息到新线程
  • 无缝集成:与 Spring 生态(@Async, @Scheduled)完美配合

适用场景

  • 复杂 Web 请求追踪
  • 多线程操作诊断
  • 定时任务监控
  • 异步方法调用链分析

即使不使用微服务架构,Sleuth 也是提升日志可读性的利器,集成简单但价值巨大。后续可探索其分布式追踪能力(如 RestTemplate、RabbitMQ、Zuul 等场景)。

完整代码示例请参考 GitHub 仓库


原始标题:Spring Cloud Sleuth - Single Application | Baeldung