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
⚠️ 注意:不同类的日志共享相同的 traceId
和 spanId
,证明它们属于同一请求
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
✅ 关键点:每个定时任务自动生成独立的 traceId
和 spanId
4. 总结
通过本文我们了解到 Spring Cloud Sleuth 在单体应用中的强大能力:
核心优势
- 自动关联日志:通过
traceId
关联同一请求的所有日志 - 细粒度追踪:使用
spanId
标识请求中的具体步骤 - 多线程支持:自动传递追踪信息到新线程
- 无缝集成:与 Spring 生态(@Async, @Scheduled)完美配合
适用场景
- 复杂 Web 请求追踪
- 多线程操作诊断
- 定时任务监控
- 异步方法调用链分析
即使不使用微服务架构,Sleuth 也是提升日志可读性的利器,集成简单但价值巨大。后续可探索其分布式追踪能力(如 RestTemplate、RabbitMQ、Zuul 等场景)。
完整代码示例请参考 GitHub 仓库