Spring Cloud Sleuth in einer Monolith-Anwendung

1. Übersicht

In diesem Artikel stellen wir Spring Cloud Sleuth vor - ein leistungsstarkes Tool zum Verbessern von Protokollen in jeder Anwendung, insbesondere aber in einem System, das aus mehreren Diensten besteht.

Und für diesen Artikel konzentrieren wir uns auf die Verwendung von Sleuth in einer Monolith-Anwendung, nicht über Microservices hinweg .

Wir haben alle die unglückliche Erfahrung gemacht, ein Problem mit einer geplanten Aufgabe, einem Multithread-Vorgang oder einer komplexen Webanforderung zu diagnostizieren. Selbst bei der Protokollierung ist es oft schwierig zu sagen, welche Aktionen miteinander korreliert werden müssen, um eine einzelne Anforderung zu erstellen.

Dies kann die Diagnose einer komplexen Aktion sehr schwierig oder sogar unmöglich machen. Dies führt häufig zu Lösungen wie der Übergabe einer eindeutigen ID an jede Methode in der Anforderung zur Identifizierung der Protokolle.

Da kommt Sleuth . Diese Bibliothek ermöglicht es, Protokolle zu identifizieren, die sich auf einen bestimmten Job, Thread oder eine bestimmte Anforderung beziehen. Sleuth lässt sich mühelos in Protokollierungsframeworks wie Logback und SLF4J integrieren , um eindeutige Kennungen hinzuzufügen, mit denen Probleme mithilfe von Protokollen verfolgt und diagnostiziert werden können.

Werfen wir einen Blick darauf, wie es funktioniert.

2. Setup

Zunächst erstellen wir ein Spring Boot- Webprojekt in unserer bevorzugten IDE und fügen diese Abhängigkeit unserer Datei pom.xml hinzu :

 org.springframework.cloud spring-cloud-starter-sleuth 

Unsere Anwendung läuft mit Spring Boot und der übergeordnete POM bietet Versionen für jeden Eintrag. Die neueste Version dieser Abhängigkeit finden Sie hier: Spring-Cloud-Starter-Sleuth. Um das gesamte POM zu sehen, schauen Sie sich das Projekt auf Github an.

Fügen Sie außerdem einen Anwendungsnamen hinzu, um Sleuth anzuweisen , die Protokolle dieser Anwendung zu identifizieren.

Fügen Sie in unserer Datei application.properties diese Zeile hinzu:

spring.application.name=Baeldung Sleuth Tutorial

3. Sleuth-Konfigurationen

Sleuth ist in der Lage, Protokolle in vielen Situationen zu verbessern. Ab Version 2.0.0 verwendet Spring Cloud Sleuth Brave als Ablaufverfolgungsbibliothek, die jeder Webanforderung, die in unsere Anwendung eingeht, eindeutige IDs hinzufügt. Darüber hinaus hat das Spring-Team Unterstützung für die gemeinsame Nutzung dieser IDs über Thread-Grenzen hinweg hinzugefügt.

Traces können als einzelne Anforderung oder Job betrachtet werden, die in einer Anwendung ausgelöst werden. Alle verschiedenen Schritte in dieser Anforderung, auch über Anwendungs- und Threadgrenzen hinweg, haben dieselbe traceId.

Spans hingegen können als Abschnitte eines Jobs oder einer Anfrage betrachtet werden. Eine einzelne Ablaufverfolgung kann aus mehreren Bereichen bestehen, die jeweils einem bestimmten Schritt oder Abschnitt der Anforderung entsprechen. Mithilfe von Trace- und Span-IDs können wir genau bestimmen, wann und wo sich unsere Anwendung befindet, während sie eine Anforderung verarbeitet. Erleichtert das Lesen unserer Protokolle erheblich.

In unseren Beispielen werden wir diese Funktionen in einer einzigen Anwendung untersuchen.

3.1. Einfache Webanforderung

Lassen Sie uns zunächst eine Controller-Klasse erstellen, die als Einstiegspunkt für die Arbeit dient:

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

Lassen Sie uns unsere Anwendung ausführen und zu "// localhost: 8080" navigieren. Überprüfen Sie die Protokolle auf Ausgabe, die wie folgt aussieht:

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

Dies sieht aus wie ein normales Protokoll, mit Ausnahme des Teils am Anfang zwischen den Klammern. Dies sind die Kerninformationen, die Spring Sleuth hinzugefügt hat. Diese Daten folgen dem Format von:

[Anwendungsname, traceId, spanId, export]

  • Anwendungsname - Dies ist der Name, den wir in der Eigenschaftendatei festgelegt haben und der zum Aggregieren von Protokollen aus mehreren Instanzen derselben Anwendung verwendet werden kann.
  • TraceId - Dies ist eine ID, die einer einzelnen Anforderung, einem Job oder einer Aktion zugewiesen ist. So etwas wie jede einzelne vom Benutzer initiierte Webanforderung hat eine eigene traceId .
  • SpanId - Verfolgt eine Arbeitseinheit. Stellen Sie sich eine Anfrage vor, die aus mehreren Schritten besteht. Jeder Schritt kann eine eigene spanId haben und einzeln verfolgt werden. Standardmäßig startet jeder Anwendungsfluss mit derselben TraceId und SpanId.
  • Exportieren - Diese Eigenschaft ist ein Boolescher Wert, der angibt, ob dieses Protokoll in einen Aggregator wie Zipkin exportiert wurde . Zipkin geht über den Rahmen dieses Artikels hinaus, spielt jedoch eine wichtige Rolle bei der Analyse von von Sleuth erstellten Protokollen .

Inzwischen sollten Sie eine Vorstellung von der Leistungsfähigkeit dieser Bibliothek haben. Schauen wir uns ein anderes Beispiel an, um weiter zu demonstrieren, wie wichtig diese Bibliothek für die Protokollierung ist.

3.2. Einfache Webanforderung mit Servicezugriff

Beginnen wir mit der Erstellung eines Dienstes mit einer einzigen Methode:

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

Lassen Sie uns nun unseren Service in unseren Controller einfügen und eine Anforderungszuordnungsmethode hinzufügen, die darauf zugreift:

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

Starten Sie abschließend die Anwendung neu und navigieren Sie zu "// localhost: 8080 / same-span". Achten Sie auf die Protokollausgabe, die wie folgt aussieht:

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

Beachten Sie, dass die Trace- und Span-IDs zwischen den beiden Protokollen identisch sind, obwohl die Nachrichten aus zwei verschiedenen Klassen stammen. Dies macht es trivial, jedes Protokoll während einer Anforderung zu identifizieren, indem nach der traceId dieser Anforderung gesucht wird .

Dies ist das Standardverhalten. Eine Anforderung erhält eine einzelne traceId und spanId . Wir können jedoch nach Belieben manuell Bereiche hinzufügen. Schauen wir uns ein Beispiel an, das diese Funktion verwendet.

3.3. Manuelles Hinzufügen einer Spanne

Fügen wir zunächst einen neuen Controller hinzu:

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

Und jetzt fügen wir die neue Methode in unseren Service ein:

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

Note that we also added a new object, Tracer. The tracer instance is created by Spring Sleuth during startup and is made available to our class through dependency injection.

Traces must be manually started and stopped. To accomplish this, code that runs in a manually created span is placed inside a try-finally block to ensure the span is closed regardless of the operation's success. Also, notice that new span has to be placed in scope.

Restart the application and navigate to “//localhost:8080/new-span”. Watch for the log output that looks like:

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

We can see that the third log shares the traceId with the others, but it has a unique spanId. This can be used to locate different sections in a single request for more fine-grained tracing.

Now let's take a look at Sleuth's support for threads.

3.4. Spanning Runnables

To demonstrate the threading capabilities of Sleuth let's first add a configuration class to set up a thread pool:

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

It is important to note here the use of LazyTraceExecutor. This class comes from the Sleuth library and is a special kind of executor that will propagate our traceIds to new threads and create new spanIds in the process.

Now let's wire this executor into our controller and use it in a new request mapping method:

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

With our runnable in place, let's restart our application and navigate to “//localhost:8080/new-thread”. Watch for log output that looks like:

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

Much like the previous example we can see that all the logs share the same traceId. But the log coming from the runnable has a unique span that will track the work done in that thread. Remember that this happens because of the LazyTraceExecutor, if we were to use a normal executor we would continue to see the same spanId used in the new thread.

Now let's look into Sleuth's support for @Async methods.

3.5. @Async Support

To add async support let's first modify our ThreadConfig class to enable this feature:

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

Note that we extend AsyncConfigurerSupport to specify our async executor and use LazyTraceExecutor to ensure traceIds and spanIds are propagated correctly. We have also added @EnableAsync to the top of our class.

Let's now add an async method to our service:

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

Now let's call into this method from our controller:

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

Finally, let's restart our service and navigate to “//localhost:8080/async”. Watch for the log output that looks like:

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

We can see here that much like our runnable example, Sleuth propagates the traceId into the async method and adds a unique spanId.

Let's now work through an example using spring support for scheduled tasks.

3.6. @Scheduled Support

Finally, let's look at how Sleuth works with @Scheduled methods. To do this let's update our ThreadConfig class to enable scheduling:

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

Note that we have implemented the SchedulingConfigurer interface and overridden its configureTasks method. We have also added @EnableScheduling to the top of our class.

Next, let's add a service for our scheduled tasks:

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

In this class, we have created a single scheduled task with a fixed delay of 30 seconds.

Let's now restart our application and wait for our task to be executed. Watch the console for output like this:

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

We can see here that Sleuth has created new trace and span ids for our task. Each instance of a task will get it's own trace and span by default.

4. Conclusion

In conclusion, we have seen how Spring Sleuth can be used in a variety of situations inside a single web application. We can use this technology to easily correlate logs from a single request, even when that request spans multiple threads.

By now we can see how Spring Cloud Sleuth can help us keep our sanity when debugging a multi-threaded environment. By identifying each operation in a traceId and each step in a spanId we can really begin to break down our analysis of complex jobs in our logs.

Selbst wenn wir nicht in die Cloud gehen, ist Spring Sleuth wahrscheinlich eine kritische Abhängigkeit in fast jedem Projekt. Es lässt sich nahtlos integrieren und ist ein massiver Mehrwert .

Von hier aus möchten Sie möglicherweise andere Funktionen von Sleuth untersuchen . Es kann die Ablaufverfolgung in verteilten Systemen mithilfe von RestTemplate , über von RabbitMQ und Redis verwendete Messaging-Protokolle und über ein Gateway wie Zuul unterstützen.

Wie immer finden Sie den Quellcode auf Github.