Spring Cloud Sleuth в приложении монолита

Весенняя облачность в монолитном приложении

1. обзор

В этой статье мы представляемSpring Cloud Sleuth - мощный инструмент для улучшения журналов в любом приложении, но особенно в системе, состоящей из нескольких служб.

И в этой статье мы сосредоточимся на использовании Sleuthin a monolith application, not across microservices.

У всех нас был неудачный опыт попыток диагностировать проблему с помощью запланированной задачи, многопоточной операции или сложного веб-запроса. Часто даже при ведении журнала сложно определить, какие действия необходимо соотнести, чтобы создать один запрос.

Это может сделатьdiagnosing a complex action very difficult или даже невозможным. Часто это приводит к таким решениям, как передача уникального идентификатора каждому методу в запросе для идентификации журналов.

ПриходитSleuth. Эта библиотека позволяет идентифицировать журналы, относящиеся к конкретному заданию, потоку или запросу. Sleuth легко интегрируется со структурами ведения журналов, такими какLogback иSLF4J, для добавления уникальных идентификаторов, которые помогают отслеживать и диагностировать проблемы с помощью журналов.

Давайте посмотрим, как это работает.

2. Настроить

Мы начнем с создания веб-проектаSpring Boot в нашей любимой среде IDE и добавления этой зависимости в наш файлpom.xml:


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

Наше приложение работает сSpring Boot, а родительский pom предоставляет версии для каждой записи. Последнюю версию этой зависимости можно найти здесь:spring-cloud-starter-sleuth. Чтобы увидеть весь POM, проверьте проект наGithub.

Кроме того, давайте добавим имя приложения, чтобы указатьSleuth идентифицировать журналы этого приложения.

В нашем файлеapplication.properties добавьте эту строку:

spring.application.name=example Sleuth Tutorial

3. Конфигурации сыщика

Sleuth может улучшать журналы во многих ситуациях. Начиная с версии 2.0.0, Spring Cloud Sleuth используетBrave в качестве библиотеки трассировки, которая добавляет уникальные идентификаторы к каждому веб-запросу, входящему в наше приложение. Кроме того, команда Spring добавила поддержку совместного использования этих идентификаторов через границы потоков.

Следы можно рассматривать как отдельный запрос или задание, которое запускается в приложении. Все различные шаги в этом запросе, даже через границы приложения и потока, будут иметь одинаковый traceId.

С другой стороны, промежутки можно рассматривать как разделы работы или запроса. Одна трасса может состоять из нескольких интервалов, каждый из которых связан с определенным этапом или разделом запроса. Используя идентификаторы трассировки и диапазона, мы можем точно определить, когда и где находится наше приложение, когда оно обрабатывает запрос. Сделать чтение наших журналов намного проще.

В наших примерах мы рассмотрим эти возможности в одном приложении.

3.1. Простой веб-запрос

Во-первых, давайте создадим класс контроллера, который будет точкой входа для работы:

@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
  [example Sleuth Tutorial,4e30f7340b3fb631,4e30f7340b3fb631,false] 12516
  --- [nio-8080-exec-1] c.b.spring.session.SleuthController : Hello Sleuth

Это похоже на нормальный журнал, за исключением части в начале между скобками. Это основная информация, которую добавилSpring Sleuth. Эти данные соответствуют формату:

[название приложения, traceId, spanId, экспорт]

  • Application name - это имя, которое мы установили в файле свойств, и его можно использовать для агрегирования журналов из нескольких экземпляров одного и того же приложения.

  • TraceId - это идентификатор, который назначается отдельному запросу, заданию или действию. Что-то вроде каждого уникального веб-запроса, инициированного пользователем, будет иметь свойtraceId.

  • SpanId - отслеживает единицу работы. Подумайте о запросе, который состоит из нескольких шагов. Каждый шаг может иметь свойspanId и отслеживаться индивидуально. По умолчанию любой поток приложения будет начинаться с одного и того же TraceId и SpanId.

  • Export - это свойство является логическим, которое указывает, был ли этот журнал экспортирован в агрегатор, напримерZipkin. Zipkin выходит за рамки этой статьи, но играет важную роль в анализе журналов, созданныхSleuth.

К настоящему времени вы должны иметь представление о силе этой библиотеки. Давайте рассмотрим еще один пример, чтобы еще раз продемонстрировать, насколько важна эта библиотека для ведения журнала.

3.2. Простой веб-запрос с доступом к сервису

Начнем с создания сервиса с помощью одного метода:

@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
  [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

Обратите внимание, что идентификаторы трассировки и span совпадают между двумя журналами, даже если сообщения происходят из двух разных классов. Это упрощает идентификацию каждого журнала во время запроса путем поискаtraceId этого запроса.

Это поведение по умолчанию, один запрос получает отдельныеtraceId иspanId. Но мы можем вручную добавить пролеты по своему усмотрению. Давайте посмотрим на пример, в котором используется эта функция.

3.3. Добавление диапазона вручную

Для начала добавим новый контроллер:

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

Обратите внимание, что мы также добавили новый объектTracer. Экземплярtracer создаетсяSpring Sleuth во время запуска и становится доступным для нашего класса посредством внедрения зависимостей.

Трассировки должны быть запущены и остановлены вручную. Для этого код, который выполняется в вручную созданномspan, помещается в блокtry-finally, чтобы гарантировать закрытиеspan независимо от успеха операции. Также обратите внимание, что новый диапазон должен быть помещен в область видимости.

Перезапустите приложение и перейдите к «http: // localhost: 8080 / new-span». Следите за выходом журнала, который выглядит следующим образом:

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

Мы видим, что третий журнал разделяетtraceId с другими, но имеет уникальныйspanId. Это можно использовать для поиска разных разделов в одном запросе для более детальной трассировки.

Теперь давайте посмотрим на поддержку потоковSleuth’s.

3.4. Spanning 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 и представляет собой особый вид исполнителя, который будет распространять нашиtraceIds на новые потоки и создавать новыеspanIds в процессе.

Теперь давайте подключим этого исполнителя к нашему контроллеру и используем его в новом методе сопоставления запросов:

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

Когда наш runnable на месте, давайте перезапустим наше приложение и перейдем к http: // localhost: 8080 / new-thread. Следите за выходом журнала, который выглядит следующим образом:

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

Как и в предыдущем примере, мы видим, что все журналы имеют один и тот жеtraceId. Но журнал, поступающий из runnable, имеет уникальный диапазон, который будет отслеживать работу, проделанную в этом потоке. Помните, что это происходит из-заLazyTraceExecutor, если бы мы использовали обычный исполнитель, мы бы продолжали видеть тот же самыйspanId, используемый в новом потоке.

Теперь давайте рассмотрим поддержкуSleuth’s методов@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 [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

Здесь мы видим, что так же, как и в нашем примере выполнения,Sleuth распространяетtraceId в метод async и добавляет уникальный 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 [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

Здесь мы видим, чтоSleuth создал новые идентификаторы трассировки и диапазона для нашей задачи. По умолчанию каждому экземпляру задачи будет присвоена собственная трассировка и диапазон.

4. Заключение

В заключение мы увидели, какSpring Sleuth можно использовать в различных ситуациях внутри одного веб-приложения. Мы можем использовать эту технологию для простой корреляции журналов из одного запроса, даже если этот запрос охватывает несколько потоков.

Теперь мы видим, какSpring Cloud Sleuth может помочь нам сохранить рассудок при отладке многопоточной среды. Идентифицируя каждую операцию вtraceId и каждый шаг вspanId, мы действительно можем начать анализировать сложные задания в наших журналах.

Даже если мы не перейдем в облако,Spring Sleuth, вероятно, является критически важной зависимостью практически в любом проекте; легко интегрировать иis a massive addition of value.

Отсюда вы можете захотеть изучить другие особенностиSleuth. Он может поддерживать трассировку в распределенных системах с использованиемRestTemplate, по протоколам обмена сообщениями, используемымRabbitMQ иRedis, а также через шлюз, такой как Zuul.

Как всегда, вы можете найти исходный кодover on Github.