知識庫 / Spring / Spring Cloud RSS 訂閱

Spring Cloud Sleuth 在單體應用中的應用

Logging,Spring Cloud
HongKong
20
02:41 PM · Dec 06 ,2025

1. 概述

在本教程中,我們將介紹 Spring Cloud Sleuth ,這是一種強大的工具,可用於增強任何應用程序的日誌記錄,尤其是在由多個服務組成的系統中。 僅供本教程使用,我們將重點介紹在單體應用程序中使用 Sleuth,而不是在微服務之間使用。

我們都曾有過嘗試診斷計劃任務、多線程操作或複雜 Web 請求問題的不幸經歷。 即使存在日誌記錄,也很難確定需要關聯哪些操作以創建單個請求。

這使得 診斷複雜的動作非常困難,甚至不可能,通常會導致解決方案是向每個請求的方法傳遞唯一的 ID 以識別日誌。

這時 Sleuth 登場了。 該庫使您可以識別與特定工作、線程或請求相關聯的日誌記錄。 Sleuth 與日誌記錄框架(如 LogbackSLF4J)無縫集成,以添加唯一標識符,從而幫助您使用日誌記錄跟蹤和診斷問題。

讓我們看看它如何工作。

2. 設置

我們將首先在您最喜歡的 IDE 中創建一個 Spring Boot Web 項目,並將以下依賴項添加到我們的 pom.xml 文件中:

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

我們的應用程序使用 Spring Boot, 並且父 pom 文件提供了每個條目的版本。該依賴項的最新版本可以在這裏找到:spring-cloud-starter-sleuth.

此外,我們將添加應用程序名稱,以指示 Sleuth識別此應用程序的日誌。

在我們的 application.properties文件中,我們將添加以下行:

spring.application.name=Baeldung Sleuth Tutorial

3. Sleuth 配置

Sleuth 能夠在許多情況下增強日誌。從 2.0.0 版本開始,Spring Cloud Sleuth 使用 Brave 作為 tracing 庫,為進入我們應用程序的每個 Web 請求添加唯一的 ID。 此外,Spring 團隊還添加了跨線程邊界共享這些 ID 的支持。

Trace 可以被認為是單個請求或作業,它在應用程序中觸發。 該請求中的所有各個步驟,即使跨應用程序和線程邊界,也具有相同的 traceId。

另一方面,Span 可以被認為是作業或請求中的一個部分。 單個 Trace 可以由多個 Span 組成,每個 Span 對應於請求中的一個特定步驟或部分。 使用 traceId 和 spanId,我們可以準確地確定應用程序在處理請求時何時以及在何處,從而使我們更容易閲讀日誌。

在我們的示例中,我們將探索這些功能,在一個應用程序中。

3.1. 簡單 Web 請求

首先,我們將創建一個控制器類作為與請求交互的入口點:

@RestController
public class SleuthController {

    @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

這看起來像一個正常的日誌,除了開頭的括號之間的部分。這是 Spring Sleuth 添加的核心信息。這些數據遵循以下格式:

[應用程序名稱,traceId,spanId,導出]

  • 應用程序名稱 – 這是我們在屬性文件中設置的名稱,可用於從同一應用程序的多個實例中聚合日誌。
  • TraceId – 這是一個為單個請求、作業或操作分配的ID。例如,每個獨特的用户啓動的 Web 請求都將擁有自己的 traceId
  • SpanId – 跟蹤一個工作單元。可以將其視為包含多個步驟的請求。每個步驟都可以有自己的 spanId 並單獨跟蹤。默認情況下,任何應用程序流程都將使用相同的 TraceId 和 SpanId。
  • 導出 – 此屬性是一個布爾值,指示此日誌是否已導出到聚合器(如 Zipkin)。Zipkin 超出了本文檔的範圍,但它在分析由 Sleuth 創建的日誌方面起着重要作用。

現在,我們應該對該庫的強大功能有所瞭解。讓我們來看另一個例子,以進一步證明該庫在日誌記錄中的重要性。

3.2. 使用服務訪問的簡單 Web 請求

我們將首先創建一個具有單個方法的服務:

@Service
public class SleuthService {

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

現在讓我們將服務注入到我們的控制器中,並添加一個訪問該服務的請求映射方法:

@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

請注意,這兩個日誌的 trace 和 span ID 相同,儘管它們來自不同的類。這使得通過搜索請求的 traceId 來輕鬆識別每個日誌成為可能。

這是默認行為:一個請求會獲得一個唯一的 traceIdspanId,但我們可以根據需要手動添加 span。下面我們來看一個使用該功能的示例。

3.3. 手動添加 Span

為了開始,我們將添加一個新的控制器:

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

現在我們將添加新的方法到我們的服務中:

@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");
}

請注意,我們還添加了一個新的對象,Tracertracer 實例由 Spring Sleuth 在啓動時創建,並通過依賴注入提供給我們的類。

追蹤必須手動啓動和停止。 要實現這一點,在手動創建的 span 內運行的代碼將被放置在 try-finally 塊中,以確保無論操作是否成功,span 都能正確關閉。 此外,新的 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。 這可以用於定位單個請求中的不同部分,從而實現更精細的追蹤。

現在,讓我們來查看 Sleuth 對線程的支持。

3.4. 跨度運行對象 (Runnables)

為了演示 Sleuth 的線程能力,我們首先添加一個配置類來設置線程池:

@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 的使用。該類來自 Sleuth 庫,是一種特殊的執行器,它會將我們的 traceId 傳播到新的線程中,並在過程中創建新的 spanId

現在我們將此執行器連接到我們的控制器,並在一個新的請求映射方法中使用它:

@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,但來自可運行程序的日誌具有唯一的 span,該 span 會跟蹤該線程中執行的工作。這要歸功於 LazyTraceExecutor。如果使用正常的 executor,我們仍然會看到新線程中相同的 spanId

現在讓我們來研究 Sleuth@Async 方法的支持。

3.5. @Async 支持

為了添加異步支持,我們首先將修改我們的 ThreadConfig 類以啓用此功能:

@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);
    }
}

請注意,我們擴展了 AsyncConfigurerSupport 以指定我們的異步執行器,並使用 LazyTraceExecutor 以確保 traceIds 和 spanIds 正確傳播。我們還添加了 @EnableAsync 到我們的類頂部。

現在我們將為我們的服務添加一個異步方法:

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

我們將在控制器中調用這個方法:

@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

我們這裏可以觀察到,與可執行示例類似,Sleuth會將 traceId 傳播到異步方法中,並添加一個唯一的 spanId。

現在我們將通過一個使用 Spring 支持的定時任務示例進行説明。

3.6. @Scheduled 支持

最後,我們將探討 Sleuth 如何與 @Scheduled 方法一起工作。為此,我們將更新 ThreadConfig 類以啓用調度:

@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);
    }
}

請注意,我們實現了 SchedulingConfigurer 接口,並覆蓋了其 configureTasks 方法。我們還添加了 @EnableScheduling 到我們的類頂部。

接下來,我們將為我們的定時任務添加一個服務:

@Service
public class SchedulingService {

    private Logger logger = LoggerFactory.getLogger(this.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");
    }
}

在本課程中,我們創建了一個具有30秒固定延遲的單個計劃任務。

現在讓我們重啓我們的應用程序,等待任務執行。我們將監視控制枱輸出,例如如下所示:

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

我們這裏可以觀察到 Sleuth 為我們的任務創建了新的 trace 和 span ID。 默認情況下,每個任務實例都會獲得自己的 trace 和 span。

4. 結論

在本文中,我們學習瞭如何使用 Spring Sleuth

在本文中,我們學習瞭如何使用 Spring Sleuth

通過本文,我們可以理解 Spring Cloud Sleuth 在單線程環境中保持我們理智的關鍵作用。通過識別每個操作的 traceId 和每個步驟的 spanId

即使我們沒有上雲,Spring Sleuth 仍然幾乎在任何項目中都是一個關鍵依賴項,它易於集成,並且 為項目帶來了巨大的價值

從這裏,我們可以進一步研究 Sleuth 的其他功能。它支持分佈式系統中的跟蹤,使用 RestTemplate,通過 RabbitMQ 和 Redis 等消息協議,以及 Zuul 這樣的網關。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.