Spring Cloud Sleuth in einer Monolithanwendung

Spring Cloud Sleuth in einer Monolith-Anwendung

1. Überblick

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

Und für diesen Artikel konzentrieren wir uns auf die Verwendung von Sleuthin a monolith application, not across microservices.

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

Dies kanndiagnosing a complex action very difficult 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.

In kommtSleuth. Mit dieser Bibliothek können Protokolle identifiziert werden, die sich auf einen bestimmten Job, Thread oder eine bestimmte Anforderung beziehen. Sleuth lässt sich mühelos in Protokollierungsframeworks wieLogback undSLF4J 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. Konfiguration

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


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

Unsere Anwendung wird mitSpring Boot ausgeführt und der übergeordnete POM bietet Versionen für jeden Eintrag. Die neueste Version dieser Abhängigkeit finden Sie hier:spring-cloud-starter-sleuth. Um den gesamten POM zu sehen, überprüfen Sie das Projekt aufGithub.

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

Fügen Sie in unsererapplication.properties-Datei diese Zeile hinzu:

spring.application.name=example Sleuth Tutorial

3. Sleuth-Konfigurationen

Sleuth kann in vielen Situationen Protokolle verbessern. Ab Version 2.0.0 verwendet Spring Cloud SleuthBrave 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 Threadgrenzen hinweg hinzugefügt.

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

Auf der anderen Seite können Bereiche als Teile eines Auftrags oder einer Anfrage betrachtet werden. Eine einzelne Ablaufverfolgung kann aus mehreren Abschnitten 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.

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

3.1. Einfache Webanforderung

Erstellen wir zunächst eine Controller-Klasse als Einstiegspunkt für die Arbeit mit:

@RestController
public class SleuthController {

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

Lassen Sie uns unsere Anwendung ausführen und zu "http: // localhost: 8080" navigieren. Sehen Sie sich die Protokolle für die Ausgabe an, die wie folgt aussieht:

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

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

[Anwendungsname, traceId, spanId, export]

  • Application name - 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 ihre eigenentraceId.

  • SpanId - Verfolgt eine Arbeitseinheit. Stellen Sie sich eine Anfrage vor, die aus mehreren Schritten besteht. Jeder Schritt könnte seine eigenenspanIdhaben und einzeln verfolgt werden. Standardmäßig startet jeder Anwendungsfluss mit derselben TraceId und SpanId.

  • Export - Diese Eigenschaft ist ein Boolescher Wert, der angibt, ob dieses Protokoll in einen Aggregator wieZipkin exportiert wurde. Zipkin geht über den Rahmen dieses Artikels hinaus, spielt jedoch eine wichtige Rolle bei der Analyse von Protokollen, die vonSleuth erstellt wurden.

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 "http: // localhost: 8080 / same-span". Achten Sie auf die Protokollausgabe, die wie folgt aussieht:

2017-01-10 22:51:47.664  INFO
  [example 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
  [example Sleuth Tutorial,b77a5ea79036d5b9,b77a5ea79036d5b9,false] 12516
  --- [nio-8080-exec-3] c.example.spring.session.SleuthService  : Doing some work

Beachten Sie, dass die Trace- und Span-IDs in 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 nachtraceId dieser Anforderung gesucht wird.

Dies ist das Standardverhalten. Eine Anforderung erhält ein einzelnestraceId undspanId. Aber wir können Spans manuell hinzufügen, wie wir es für richtig halten. Schauen wir uns ein Beispiel an, das diese Funktion verwendet.

3.3. Manuelles Hinzufügen einer Spanne

Fügen Sie 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");
}

Beachten Sie, dass wir auch ein neues Objekt hinzugefügt haben,Tracer. Die Instanztracer wird beim Start vonSpring Sleuth erstellt und unserer Klasse durch Abhängigkeitsinjektion zur Verfügung gestellt.

Traces müssen manuell gestartet und gestoppt werden. Zu diesem Zweck wird Code, der in einem manuell erstelltenspan ausgeführt wird, in einentry-finally-Block eingefügt, um sicherzustellen, dassspan unabhängig vom Erfolg der Operation geschlossen wird. Beachten Sie auch, dass eine neue Spanne in den Geltungsbereich eingefügt werden muss.

Starten Sie die Anwendung neu und navigieren Sie zu "http: // localhost: 8080 / new-span". Achten Sie auf die Protokollausgabe, die wie folgt aussieht:

2017-01-11 21:07:54.924
  INFO [example 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 [example Sleuth Tutorial,9cdebbffe8bbbade,9cdebbffe8bbbade,false] 12516
  --- [nio-8080-exec-6] c.example.spring.session.SleuthService  :
  I'm in the original span
2017-01-11 21:07:55.924
  INFO [example Sleuth Tutorial,9cdebbffe8bbbade,1e706f252a0ee9c2,false] 12516
  --- [nio-8080-exec-6] c.example.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 [example Sleuth Tutorial,9cdebbffe8bbbade,9cdebbffe8bbbade,false] 12516
  --- [nio-8080-exec-6] c.example.spring.session.SleuthService  :
  I'm in the original span

Wir können sehen, dass das dritte Protokoll dietraceId mit den anderen teilt, aber es hat ein eindeutigesspanId. Dies kann verwendet werden, um verschiedene Abschnitte in einer einzelnen Anforderung zu lokalisieren, um eine feinere Ablaufverfolgung zu erhalten.

Schauen wir uns nun die Unterstützung vonSleuth’sfür Threads an.

3.4. Spanning Runnables

Um die Threading-Funktionen vonSleuthzu demonstrieren, fügen wir zunächst eine Konfigurationsklasse hinzu, um einen Thread-Pool einzurichten:

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

Es ist wichtig, hier die Verwendung vonLazyTraceExecutor zu beachten. Diese Klasse stammt aus derSleuth-Bibliothek und ist eine spezielle Art von Executor, der unseretraceIds an neue Threads weitergibt und dabei neuespanIds erstellt.

Lassen Sie uns nun diesen Executor mit unserem Controller verbinden und ihn in einer neuen Anforderungszuordnungsmethode verwenden:

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

Starten Sie unsere Anwendung neu und navigieren Sie zu "http: // localhost: 8080 / new-thread". Achten Sie auf die Protokollausgabe, die wie folgt aussieht:

2017-01-11 21:18:15.949
  INFO [example 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 [example 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 [example Sleuth Tutorial,96076a78343c364d,e3b6a68013ddfeea,false] 12516
  --- [lTaskExecutor-1] c.b.spring.session.SleuthController      :
  I'm inside the new thread - with a new span

Ähnlich wie im vorherigen Beispiel können wir sehen, dass alle Protokolle die gleichentraceId haben. Das Protokoll, das von der ausführbaren Datei stammt, hat jedoch eine eindeutige Zeitspanne, die die in diesem Thread geleistete Arbeit protokolliert. Denken Sie daran, dass dies aufgrund derLazyTraceExecutor geschieht. Wenn wir einen normalen Executor verwenden würden, würden wir weiterhin dieselbenspanId sehen, die im neuen Thread verwendet werden.

Schauen wir uns nun die Unterstützung vonSleuth’sfür die Methoden von@Asyncan.

3.5. @Async Unterstützung

Um die asynchrone Unterstützung hinzuzufügen, ändern wir zunächst dieThreadConfig-Klasse, um diese Funktion zu aktivieren:

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

Beachten Sie, dass wirAsyncConfigurerSupport erweitern, um unseren asynchronen Executor anzugeben, undLazyTraceExecutor verwenden, um sicherzustellen, dass traceIds und spanIds korrekt weitergegeben werden. Wir haben auch@EnableAsync zur Spitze unserer Klasse hinzugefügt.

Fügen wir unserem Service jetzt eine asynchrone Methode hinzu:

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

Rufen wir nun diese Methode von unserem Controller aus auf:

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

    return "success";
}

Lassen Sie uns abschließend unseren Dienst neu starten und zu "http: // localhost: 8080 / async" navigieren. Achten Sie auf die Protokollausgabe, die wie folgt aussieht:

2017-01-11 21:30:40.621
  INFO [example 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 [example 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 [example Sleuth Tutorial,c187f81915377fff,8a9f3f097dca6a9e,false] 10072
  --- [lTaskExecutor-1] c.example.spring.session.SleuthService  :
  Start Async Method
2017-01-11 21:30:41.622
  INFO [example Sleuth Tutorial,c187f81915377fff,8a9f3f097dca6a9e,false] 10072
  --- [lTaskExecutor-1] c.example.spring.session.SleuthService  :
  End Async Method

Wir können hier sehen, dassSleuth ähnlich wie in unserem ausführbaren BeispieltraceId in die asynchrone Methode weitergibt und eine eindeutige spanId hinzufügt.

Lassen Sie uns nun ein Beispiel mit Federunterstützung für geplante Aufgaben durcharbeiten.

3.6. @Scheduled Unterstützung

Schauen wir uns abschließend an, wieSleuth mit@Scheduled Methoden funktioniert. Dazu aktualisieren wir dieThreadConfig-Klasse, um die Planung zu ermöglichen:

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

Beachten Sie, dass wir dieSchedulingConfigurer-Schnittstelle implementiert und ihre configureTasks-Methode überschrieben haben. Wir haben auch@EnableScheduling zur Spitze unserer Klasse hinzugefügt.

Als Nächstes fügen wir einen Service für unsere geplanten Aufgaben hinzu:

@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 dieser Klasse haben wir eine einzelne geplante Aufgabe mit einer festen Verzögerung von 30 Sekunden erstellt.

Lassen Sie uns nun unsere Anwendung neu starten und warten, bis unsere Aufgabe ausgeführt wurde. Beobachten Sie die Konsole für die Ausgabe wie folgt:

2017-01-11 21:30:58.866
  INFO [example 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 [example Sleuth Tutorial,3605f5deaea28df2,3605f5deaea28df2,false] 10072
  --- [pool-1-thread-1] c.b.spring.session.SchedulingService     :
  End work from scheduled task

Wir können hier sehen, dassSleuth neue Trace- und Span-IDs für unsere Aufgabe erstellt hat. Jede Instanz einer Aufgabe erhält standardmäßig eine eigene Ablaufverfolgung.

4. Fazit

Zusammenfassend haben wir gesehen, wieSpring Sleuth in einer Vielzahl von Situationen innerhalb einer einzelnen Webanwendung verwendet werden können. Mit dieser Technologie können Sie problemlos Protokolle einer einzelnen Anforderung korrelieren, auch wenn diese Anforderung mehrere Threads umfasst.

Inzwischen können wir sehen, wieSpring Cloud Sleuth uns helfen können, beim Debuggen einer Multithread-Umgebung unsere Vernunft zu bewahren. Indem wir jede Operation intraceId und jeden Schritt inspanId identifizieren, können wir wirklich beginnen, unsere Analyse komplexer Jobs in unseren Protokollen aufzuschlüsseln.

Selbst wenn wir nicht in die Cloud gehen, istSpring Sleuth wahrscheinlich eine kritische Abhängigkeit in fast jedem Projekt. Es ist nahtlos zu integrieren undis a massive addition of value.

Von hier aus möchten Sie möglicherweise andere Funktionen vonSleuth untersuchen. Es kann die Ablaufverfolgung in verteilten Systemen mithilfe vonRestTemplate, über Messaging-Protokolle, die vonRabbitMQ undRedis verwendet werden, und über ein Gateway wie Zuul unterstützen.

Wie immer finden Sie den Quellcodeover on Github.