Отдел Observability Spring уже довольно долго работает над поддержкой наблюдаемости в Spring-приложениях, и мы рады сообщить вам, что в Spring Framework 6 и Spring Boot 3 вы наконец-то увидите результаты этой работы!

Что такое наблюдаемость/observability? В нашем понимании — это то, «насколько хорошо вы можете понять внутреннее устройство вашей системы на основе ее выходных данных». Мы старались сделать так, чтобы взаимосвязь между метриками, логированием и распределенной трассировкой давала вам возможность формировать максимально четкое понимание состояний вашей системы для отладки исключений и задержек в ваших приложениях. Подробнее о том, что такое наблюдаемость, вы можете узнать в этом выпуске Enlightning с Джонатаном Ивановым (Jonatan Ivanov).

Предстоящий релиз Spring Boot 3.0.0-RC1 будет содержать множество автоконфигураций для улучшения метрик с помощью Micrometer и поддержку новой распределенной трассировки Micrometer Tracing (ранее Spring Cloud Sleuth). Среди наиболее заметных изменений можно также выделить встроенную поддержку корреляции логов, передачу (распространение/пропагацию) контекста W3C в качестве дефолтного типа пропагации и поддержку автоматической передачи метаданных инфраструктуре трассировки (так называемый "remote baggage") с целью упрощения маркировки наблюдений (observations).

В течение этого года мы сильно изменили API Micrometer. Наиболее важным изменением является появление нового Observation API.

Причина его создания заключалась в том, что мы хотели, чтобы пользователи могли один раз инструментировать свой код с помощью одного API и получать от этого множество преимуществ (например, метрики, трассировка, логирование).

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

Как работает Micrometer Observation?

Чтобы начать любое наблюдение (Observation), необходимо зарегистрировать обработчик наблюдения (ObservationHandler) в реестре наблюдений (ObservationRegistry). ObservationHandler реагирует только на поддерживаемые реализации контекста наблюдения (Observation.Context), создавая, например, таймеры, диапазоны и логи. Он реагирует на следующие события жизненного цикла наблюдения:

  • start — Наблюдение запущено. Возникает при вызове метода Observation#start().

  • stop — Наблюдение остановлено. Возникает при вызове метода Observation#stop().

  • error — Во время наблюдения произошла ошибка. Возникает при вызове метода Observation#error(exception).

  • event — Какое-либо событие, произошедшее во время наблюдения. Возникает при вызове метода Observation#event(event).

  • scope started — Открытие области наблюдения (scope). Если область наблюдения больше не используется, она должна быть закрыта. Обработчики могут создавать локальные переменные потока при открытии области наблюдения, которые впоследствии будут удалены при ее закрытии. Возникает при вызове метода Observation#openScope().

  • scope stopped — Закрытие области наблюдения. Возникает при вызове метода Observation.Scope#close().

При вызове любого из этих методов вызывается метод ObservationHandler (например, onStart(T extends Observation.Context ctx), onStop(T extends Observation.Context ctx) и т. д.). Для передачи состояния между методами-обработчиками можно использовать Observation.Context.

Ниже вы можете увидеть, как выглядит диаграмма состояний наблюдения:

    Контекст наблюдения   Контекст наблюдения
        

Создано ----------> Запущено ----------> Остановлено

Диаграмма состояний области наблюдения (observation scope) выглядит следующим образом:

                 Контекст

              
Область открыта ----------> Область закрыта

Для отладки трудно воспроизводимых производственных проблем наблюдению нужны в дополнительные метаданные, представляющие из себя пары ключ-значение, также известные как теги. На основе этих тегов можно запрашивать из метрик или бэкенда распределенной трассировки необходимые вам данные. Теги могут иметь высокую (high), и низкую (low) мощность (cardinality).

Ниже приведен пример использования Observation API библиотеки Micrometer:

// Создание реестра наблюдений (ObservationRegistry)
ObservationRegistry registry = ObservationRegistry.create();
// Регистрация обработчика наблюдения (ObservationHandler)
registry.observationConfig().observationHandler(new MyHandler());

// Создайте наблюдение (Observation) и наблюдайте за своим кодом!
Observation.createNotStarted("user.name", registry)
        .contextualName("getting-user-name")
        .lowCardinalityKeyValue("userType", "userType1") // исходя из предположения, что у вас может быть 3 типа пользователей
        .highCardinalityKeyValue("userId", "1234") // исходя из предположения, что это произвольное число 
        .observe(() -> log.info("Hello")); // это шорткат для запуска наблюдения, открытия области наблюдения, выполнения пользовательского кода, закрытия области видимости и остановки наблюдения

Важно

Высокая мощность (highCardinalityKeyValue) означает, что ключ будет иметь неограниченное число возможных ассоциированных значений. Хорошим примером такой пары может послужить обычный HTTP URL (например, /user/user1234, /user/user2345 и т. д.). Низкая мощность (lowCardinalityKeyValue) означает, что ключ будет иметь ограниченное число возможных ассоциированных значений. Примером такой пары может быть шаблонный HTTP URL (например, /user/{userId}).

Чтобы отделить операции жизненного цикла наблюдения от конфигураций наблюдения (например, имен и тегов с низкой и высокой мощностью), можно использовать объек ObservationConvention, который представляет из себя простой способ переопределения стандартных соглашений об именовании.

Создаем ваше первое наблюдаемое приложение

Самый простой способ включиться в работу — создать новый проект с сайта https://start.spring.io. Обязательно выберите Spring Boot 3.0.0-SNAPSHOT (вы сможете переключиться на RC1, как только он станет доступен). Инструмент сборки можете выбрать любой на ваше усмотрение.

Мы создадим серверное Spring WebMvc приложение и клиент для вызова сервера на основе RestTemplate. Начнем с серверной части.

Предварительная настройка WebMvc сервера 

Поскольку мы хотим запустить HTTP-сервер, нам необходимо выбрать зависимость org.springframework.boot:spring-boot-starter-web.

Для создания наблюдений с помощью аспекта @Observed нужно добавить зависимость org.springframework.boot:spring-boot-starter-aop.

Чтобы добавить в приложение функционал наблюдения, выберите spring-boot-starter-actuator (для добавления Micrometer в classpath).

И теперь мы можем добавлять непосредственно observability-фичи!

  • Метрики

    • Для работы метрик Micrometer с Prometheus необходимо добавить зависимость io.micrometer:micrometer-registry-prometheus.

  • Трассировка

    • Для передачи контекста трассировки с помощью Micrometer Tracing нам необходимо выбрать трассировщик (трассировщик — это библиотека, которая используется для обработки жизненного цикла спанов). Здесь мы выберем Zipkin Brave, добавив io.micrometer:micrometer-tracing-bridge-brave.

      • В клиентском приложении, которое мы также будем создавать в этой статье, будет использоваться другая библиотека-трассировщик. Это позволит нам посмотреть взаимодействие между разными трассировщиками.

    • Чтобы иметь возможность визуализировать задержки нам нужно будет отправлять на сервер завершенные спаны в любом формате по нашему выбору. В данном случае мы будем создавать спаны, совместимые с Zipkin. Для этого необходимо добавить зависимость io.zipkin.reporter2:zipkin-reporter-brave.

  • Логи

    • Поскольку Micrometer Tracing уже находится в classpath, корреляция логов будет происходить автоматически (то есть они будут снабжаться уникальными трассировочными идентификаторами). Нам же нужно позаботиться об отправке этих логов куда-нибудь. В данном примере мы отправим их в Grafana Loki. Для этого нужно добавить зависимость com.github.loki4j:loki-logback-appender (последнюю версию вы найдете, перейдя по этой ссылке)

Важно

Если вы никогда раньше не занимались трассировкой, то нам сперва нужно быстро пройтись по нескольким основным терминам. Любую операцию можно обернуть в спан (span). Он имеет уникальный span id и содержит информацию о времени выполнения и некоторые дополнительные метаданные (пары ключ-значение). В спанах могут создаваться дочерние спаны, а все дерево связанных спанов в совокупности образует трассировку (trace), имеющую свой уникальный trace id (т.е. корреляционный идентификатор).

Теперь нам необходимо подкорректировать пару настроек. Мы настраиваем актуатор (actuator) и метрики (metrics) на публикацию гистограмм с процентилями и переопределяем шаблон логирования, чтобы он включал идентификаторы трассировок и спанов. Мы установили вероятность выборки равной 1.0, чтобы в инструмент анализа латентности отправлялись все трассировки.

/src/main/resources/application.properties

server.port=7654
spring.application.name=server

# В инструмент анализа задержек должны быть отправлены все трассировки 
management.tracing.sampling.probability=1.0
management.endpoints.web.exposure.include=prometheus

# Чтобы экземпляры (Exemplars) работали корректно нам нужны группы (buckets) гистограмм
management.metrics.distribution.percentiles-histogram.http.server.requests=true

# traceID и spanId являются предопределенными ключами MDC — мы хотим, чтобы в логи попадали именно они
logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]

Поскольку мы разворачиваем стек Grafana с Loki и Tempo локально, мы настраиваем loki-logback-appender на отправку логов в локальный инстанс Loki.

/src/main/resources/logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml" />
    <springProperty scope="context" name="appName" source="spring.application.name"/>

    <appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">
        <http>
            <url>http://localhost:3100/loki/api/v1/push</url>
        </http>
        <format>
            <label>
                <pattern>app=${appName},host=${HOSTNAME},traceID=%X{traceId:-NONE},level=%level</pattern>
            </label>
            <message>
                <pattern>${FILE_LOG_PATTERN}</pattern>
            </message>
            <sortByTime>true</sortByTime>
        </format>
    </appender>

    <root level="INFO">
        <appender-ref ref="LOKI"/>
    </root>
</configuration>

Код WebMvc сервера

Наконец пора приступать к написанию код нашего сервера! Мы хотим добиться полной наблюдаемости в нашем приложении, поэтому нам нужны метрики, трассировка и подробное логирование.

Для начала мы напишем контроллер, который регистрирует сообщение в консоли и делегирует работу сервису.

MyController.java

@RestController
class MyController {

    private static final Logger log = LoggerFactory.getLogger(MyController.class);
    private final MyUserService myUserService;

    MyController(MyUserService myUserService) {
        this.myUserService = myUserService;
    }

    @GetMapping("/user/{userId}")
    String userName(@PathVariable("userId") String userId) {
        log.info("Got a request");
        return myUserService.userName(userId);
    }
}

Допустим нам нужно реализовать наблюдение за методом MyUserService#userName. Благодаря добавлению поддержки AOP мы можем использовать аннотацию @Observed. Для этого мы можем зарегистрировать бин ObservedAspect.

MyConfiguration.java

@Configuration(proxyBeanMethods = false)
class MyConfiguration {
    // Чтобы  поддерживался, нам необходимо зарегистрировать этот аспект
    @Bean
    ObservedAspect observedAspect(ObservationRegistry observationRegistry) {
        return new ObservedAspect(observationRegistry);
    }
}

MyUserService.java

@Service
class MyUserService {

    private static final Logger log = LoggerFactory.getLogger(MyUserService.class);

    private final Random random = new Random();

    // Пример использования аннотации для наблюдения за методами
    // <user.name> будет использоваться в качестве имени метрики
    // <getting-user-name> будет использоваться в качестве имени спана
    // <userType=userType2> будет установлен в качестве тега для метрики и спанов
    @Observed(name = "user.name",
            contextualName = "getting-user-name",
            lowCardinalityKeyValues = {"userType", "userType2"})
    String userName(String userId) {
        log.info("Getting user name for user with id <{}>", userId);
        try {
            Thread.sleep(random.nextLong(200L)); // simulates latency
        }
        catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return "foo";
    }
}

С метриками и трассировкой в classpath наличие этой аннотации приводит к созданию таймера (timer), таймера для продолжительной задачи (long task timer) и спана (span). Таймер будет иметь имя user.name, таймер для продолжительной задачи — user.name.active, а спан — get-user-name.

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

MyHandler.java

// Пример подключения пользовательского обработчика, который в данном случае будет выводить сообщение до и после проведения всех наблюдений
@Component
class MyHandler implements ObservationHandler<Observation.Context> {

    private static final Logger log = LoggerFactory.getLogger(MyHandler.class);

    @Override
    public void onStart(Observation.Context context) {
        log.info("Before running the observation for context [{}], userType [{}]", context.getName(), getUserTypeFromContext(context));
    }

    @Override
    public void onStop(Observation.Context context) {
        log.info("After running the observation for context [{}], userType [{}]", context.getName(), getUserTypeFromContext(context));
    }

    @Override
    public boolean supportsContext(Observation.Context context) {
        return true;
    }

    private String getUserTypeFromContext(Observation.Context context) {
        return StreamSupport.stream(context.getLowCardinalityKeyValues().spliterator(), false)
                .filter(keyValue -> "userType".equals(keyValue.getKey()))
                .map(KeyValue::getValue)
                .findFirst()
                .orElse("UNKNOWN");
    }
}

Вот и все! Теперь можем заняться клиентской частью.

Предварительная настройка RestTemplate клиента 

Как и раньше, мы добавляем зависимости spring-boot-starter-web и spring-boot-starter-actuator, чтобы запустить веб-сервер и добавить поддержку Micrometer.

Пора добавлять observability-фичи!

  • Метрики

    • Для работы метрик Micrometer с Prometheus необходимо добавить зависимость io.micrometer:micrometer-registry-prometheus.

  • Трассировка

    • Для передачи контекста трассировки с помощью Micrometer Tracing нам необходимо выбрать трассировщик. Теперь мы выберем OpenTelemetry, добавив io.micrometer:micrometer-tracing-bridge-otel.

    • Чтобы иметь возможность визуализировать задержки нам нужно будет отправлять на сервер завершенные спаны в любом формате по нашему выбору. Теперь мы будем создавать спаны, совместимые с OpenZipkin. Для этого необходимо добавить зависимость io.opentelemetry:opentelemetry-exporter-zipkin.

  • Логи

    • Как и ранее, для отправки логов в Loki мы добавляем зависимость com.github.loki4j:loki-logback-appender (последнюю версию можно найти, перейдя по этой ссылке).

Теперь нам необходимо внести небольшие правки в настройки. Мы добавляем практически ту же конфигурацию, что и для сервера.

/src/main/resources/application.properties

server.port=6543
spring.application.name=client

# В инструмент анализа задержек должны быть отправлены все трассировки
management.tracing.sampling.probability=1.0
management.endpoints.web.exposure.include=prometheus

# traceID и spanId являются предопределенными ключами MDC - мы хотим, чтобы в логи попадали именно они
logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]

Конфигурация Loki Appender выглядит точно так же.

/src/main/resources/logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml" />
    <springProperty scope="context" name="appName" source="spring.application.name"/>

    <appender name="LOKI" class="com.github.loki4j.logback.Loki4jAppender">
        <http>
            <url>http://localhost:3100/loki/api/v1/push</url>
        </http>
        <format>
            <label>
                <pattern>app=${appName},host=${HOSTNAME},traceID=%X{traceId:-NONE},level=%level</pattern>
            </label>
            <message>
                <pattern>${FILE_LOG_PATTERN}</pattern>
            </message>
            <sortByTime>true</sortByTime>
        </format>
    </appender>

    <root level="INFO">
        <appender-ref ref="LOKI"/>
    </root>
</configuration>

Код RestTemplate клиента 

Теперь пришло время писать код клиента! Мы отправляем запросы на сервер с помощью RestTemplate. Нашей конечной целью является полная наблюдаемость нашего приложения на основе метрик и трассировки.

Для начала нам понадобится бин RestTemplate, который автоматически инструментируется Spring Boot. Не забудьте внедрить RestTemplateBuilder и сконструировать в нем инстанс RestTemplate.

MyConfiguration.java

@Configuration(proxyBeanMethods = false)
class MyConfiguration {
    // ВАЖНО! Для использования RestTemplate необходимо внедрить RestTemplateBuilder
    @Bean
    RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }
}

Теперь мы напишем бин CommandLineRunner, который будет буквально пронизан Observation API. Его задача заключается в отправке запросов на сервер. Во фрагменте кода, который приведен ниже, мы подробно разберем все используемые элементы API:

MyConfiguration.java

@Configuration(proxyBeanMethods = false)
class MyConfiguration {
    @Bean
    CommandLineRunner myCommandLineRunner(ObservationRegistry registry, RestTemplate restTemplate) {
        Random highCardinalityValues = new Random(); // Simulates potentially large number of values
        List<String> lowCardinalityValues = Arrays.asList("userType1", "userType2", "userType3"); // Simulates low number of values
        return args -> {
            String highCardinalityUserId = String.valueOf(highCardinalityValues.nextLong(100_000));
            // Пример использования Observability API вручную
            // <my.observation> - это "техническое" имя, которое не зависит от контекста. Оно будет использоваться для именования метрик name e.g. Metrics
             Observation.createNotStarted("my.observation", registry)
                     // Низкая мощность означает, что количество потенциальных значений не будет большим. Записи с низкой мощность попадают в метрики
                    .lowCardinalityKeyValue("userType", randomUserTypePicker(lowCardinalityValues))
                     // Высокая мощность означает, что количество потенциальных значений может быть большим. Записи с высокой мощностью будут попадать в спаны
                    .highCardinalityKeyValue("userId", highCardinalityUserId)
                     // <command-line-runner> - это "контекстное" имя, дающее более подробную информацию в рамках заданного контекста. Оно будет использоваться для именования спанов
                    .contextualName("command-line-runner")
                     // Следующая лямбда будет выполнена в открытой области наблюдения (например, все записи MDC будут дополнены трассировочной информацией). Это означает, что наблюдение будет запущено, остановлено и, если за это время произойдет какая-нибудь ошибка, то она будет отражена в наблюдении
                    .observe(() -> {
                        log.info("Will send a request to the server"); // Поскольку мы находимся в области наблюдения — в этой строке лога будут содержаться трассировочные записи MDC ...
                        String response = restTemplate.getForObject("http://localhost:7654/user/{userId}", String.class, highCardinalityUserId); // Инструментация Boot'а RestTemplate создает здесь дочерний спан
                        log.info("Got response [{}]", response); // ... так же будет и с этой строкой
                    });

        };
    }
}

Запуск нашего сетапа

Docker-сетап всей инфраструктуры наблюдаемости, подготовленный нами, можно найти по этой ссылке. Для запуска инфраструктуры и обоих приложений вам нужно будет выполнить следующие шаги:

Запуск примеров

Для запуска примеров:

  1. Запустите весь стек наблюдаемости (для этого демо можно использовать предоставленный выше стек Grafana, Tempo и Loki) и дождитесь пока все загрузится.

$ docker compose up
  • Чтобы получить доступ к Prometheus, перейдите по адресу http://localhost:9090/.

  • Чтобы получить доступ к Grafana, перейдите по адресу http://localhost:3000/.

  1. Запустите серверное приложение (это заблокирует текущее окно терминала).

$ ./mvnw spring-boot:run -pl :server
  1. Запустите приложение-клиент (это также заблокирует текущее окно терминала)

$ ./mvnw spring-boot:run -pl :client

В логах должны появиться сообщения, аналогичные приведенным ниже:

2022-10-04T15:04:55.345+02:00  INFO [client,bbe3aea006077640b66d40f3e62f04b9,93b7a150b7e293ef] 92556 --- [           main] com.example.client.ClientApplication     : Will send a request to the server
2022-10-04T15:04:55.385+02:00  INFO [client,bbe3aea006077640b66d40f3e62f04b9,93b7a150b7e293ef] 92556 --- [           main] com.example.client.ClientApplication     : Got response [foo]
  1. Зайдите в Grafana, перейдите в дашборды и кликните на панели Logs, Traces, Metrics. Там можно выбрать значение трассировочного идентификатора (например, bbe3aea006077640b66d40f3e62f04b9), чтобы найти все логи и трассировки из обоих приложений, соответствующие этому идентификатору. Вы должны увидеть следующее представление с логами и трассировками, связанными с этим трассировочным идентификатором, а также метрики, происходящие в том же временном диапазоне. Эти метрики демонстрируют задержки обработки HTTP-запросов. Они получены с помощью автоматизированной инструментации Spring Boot WebMvc, использующей API Micrometer.

    logs metrics traces
    logs metrics traces

    Обратите внимание на небольшие ромбики в этих метриках. Это экземпляры (Exemplars) — "измерения, связанные с конкретной трассировкой, выполненные в заданном временном интервале". Кликните по ромбу, чтобы перейти к представлению трассировочного идентификатора и увидеть соответствующую трассировку.

    exemplar
    exemplar
  2. Либо кликните по трассировочному идентификатору, чтобы запросить его в Tempo, либо зайдите в Tempo и выберите трассировочный идентификатор самостоятельно. На экране появится следующее окно.

trace view
trace view

Каждый столбик представляет собой спан. Вы можете увидеть, сколько времени потребовалось для выполнения каждой операции. Если кликнуть по определенному спану, то можно увидеть теги (метаданные ключ-значение) и информацию о времени, относящуюся к данной операции.

span tags
span tags

Вот как будут выглядеть связанные логи в Loki.

correlated logs
correlated logs

Если вы хотите увидеть метрики метода аннотированного @Observed, вы можете перейти в представление Prometheus и найти таймер user_name.

annotation metric
annotation metric

Если вы хотите увидеть метрики метода аннотированного @Observed, вы можете перейти в представление Prometheus и найти таймер my_observation.

my observation
my observation

Запуск с поддержкой AOT

Если хотите разобраться, как Spring Boot поддерживает Native, прочитайте эту замечательную статью. Мы используем эти знания для запуска ранее созданных приложений с помощью Spring Native.

Сборка

Для сборки приложений необходимо наличие GraalVM в вашем path. Если вы используете SDKMan, выполните следующую команду:

sdk install java 22.3.r17.ea-nik

Также рекомендую ознакомиться с руководством по быстрому запуску GraalVM.

Для сборки приложения с помощью Maven необходимо включить профиль native:

$ ./mvnw native:compile -Pnative

Запуск

Сначала запустите серверное приложение:

$ ./server/target/server

Затем запустите приложение-клиент:

$ ./client/target/client

Вы должны получить результат, аналогичный этому:

Логи из клиента

2022-10-10T12:57:17.712+02:00  INFO \[client,,\] 82009 --- \[           main\] com.example.client.ClientApplication     : Starting ClientApplication using Java 17.0.4 on marcin-precision5560 with PID 82009 (/home/marcin/repo/observability/blogs/bootRc1/client/target/client started by marcin in /home/marcin/repo/observability/blogs/bootRc1)
2022-10-10T12:57:17.712+02:00  INFO \[client,,\] 82009 --- \[           main\] com.example.client.ClientApplication     : No active profile set, falling back to 1 default profile: "default"
2022-10-10T12:57:17.723+02:00  INFO \[client,,\] 82009 --- \[           main\] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 6543 (http)
2022-10-10T12:57:17.723+02:00  INFO \[client,,\] 82009 --- \[           main\] o.apache.catalina.core.StandardService   : Starting service \[Tomcat\]
2022-10-10T12:57:17.723+02:00  INFO \[client,,\] 82009 --- \[           main\] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: \[Apache Tomcat/10.0.23\]
2022-10-10T12:57:17.727+02:00  INFO \[client,,\] 82009 --- \[           main\] o.a.c.c.C.\[Tomcat\].\[localhost\].\[/\]       : Initializing Spring embedded WebApplicationContext
2022-10-10T12:57:17.727+02:00  INFO \[client,,\] 82009 --- \[           main\] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 15 ms
2022-10-10T12:57:17.731+02:00  WARN \[client,,\] 82009 --- \[           main\] i.m.c.i.binder.jvm.JvmGcMetrics          : GC notifications will not be available because MemoryPoolMXBeans are not provided by the JVM
2022-10-10T12:57:17.781+02:00  INFO \[client,,\] 82009 --- \[           main\] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 15 endpoint(s) beneath base path '/actuator'
2022-10-10T12:57:17.783+02:00  INFO \[client,,\] 82009 --- \[           main\] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 6543 (http) with context path ''
2022-10-10T12:57:17.783+02:00  INFO \[client,,\] 82009 --- \[           main\] com.example.client.ClientApplication     : Started ClientApplication in 0.077 seconds (process running for 0.079)
2022-10-10T12:57:17.784+02:00  INFO \[client,27c1113e4276c4173daec3675f536bf4,e0f2db8b983607d8\] 82009 --- \[           main\] com.example.client.ClientApplication     : Will send a request to the server
2022-10-10T12:57:17.820+02:00  INFO \[client,27c1113e4276c4173daec3675f536bf4,e0f2db8b983607d8\] 82009 --- \[           main\] com.example.client.ClientApplication     : Got response \[foo\]
2022-10-10T12:57:18.966+02:00  INFO \[client,,\] 82009 --- \[nio-6543-exec-1\] o.a.c.c.C.\[Tomcat\].\[localhost\].\[/\]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-10-10T12:57:18.966+02:00  INFO \[client,,\] 82009 --- \[nio-6543-exec-1\] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2022-10-10T12:57:18.966+02:00  INFO \[client,,\] 82009 --- \[nio-6543-exec-1\] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms

Логи из сервера

2022-10-10T12:57:07.200+02:00  INFO \[server,,\] 81760 --- \[           main\] com.example.server.ServerApplication     : Starting ServerApplication using Java 17.0.4 on marcin-precision5560 with PID 81760 (/home/marcin/repo/observability/blogs/bootRc1/server/target/server started by marcin in /home/marcin/repo/observability/blogs/bootRc1)
2022-10-10T12:57:07.201+02:00  INFO \[server,,\] 81760 --- \[           main\] com.example.server.ServerApplication     : No active profile set, falling back to 1 default profile: "default"
2022-10-10T12:57:07.213+02:00  INFO \[server,,\] 81760 --- \[           main\] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 7654 (http)
2022-10-10T12:57:07.213+02:00  INFO \[server,,\] 81760 --- \[           main\] o.apache.catalina.core.StandardService   : Starting service \[Tomcat\]
2022-10-10T12:57:07.213+02:00  INFO \[server,,\] 81760 --- \[           main\] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: \[Apache Tomcat/10.0.23\]
2022-10-10T12:57:07.217+02:00  INFO \[server,,\] 81760 --- \[           main\] o.a.c.c.C.\[Tomcat\].\[localhost\].\[/\]       : Initializing Spring embedded WebApplicationContext
2022-10-10T12:57:07.217+02:00  INFO \[server,,\] 81760 --- \[           main\] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 16 ms
2022-10-10T12:57:07.222+02:00  WARN \[server,,\] 81760 --- \[           main\] i.m.c.i.binder.jvm.JvmGcMetrics          : GC notifications will not be available because MemoryPoolMXBeans are not provided by the JVM
2022-10-10T12:57:07.278+02:00  INFO \[server,,\] 81760 --- \[           main\] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 15 endpoint(s) beneath base path '/actuator'
2022-10-10T12:57:07.280+02:00  INFO \[server,,\] 81760 --- \[           main\] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 7654 (http) with context path ''
2022-10-10T12:57:07.281+02:00  INFO \[server,,\] 81760 --- \[           main\] com.example.server.ServerApplication     : Started ServerApplication in 0.086 seconds (process running for 0.088)
2022-10-10T12:57:07.639+02:00  INFO \[server,,\] 81760 --- \[nio-7654-exec-1\] o.a.c.c.C.\[Tomcat\].\[localhost\].\[/\]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-10-10T12:57:07.639+02:00  INFO \[server,,\] 81760 --- \[nio-7654-exec-1\] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2022-10-10T12:57:07.640+02:00  INFO \[server,,\] 81760 --- \[nio-7654-exec-1\] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2022-10-10T12:57:17.785+02:00  INFO \[server,,\] 81760 --- \[nio-7654-exec-8\] com.example.server.MyHandler             : Before running the observation for context \[http.server.requests\]
2022-10-10T12:57:17.785+02:00  INFO \[server,27c1113e4276c4173daec3675f536bf4,9affba5698490e2d\] 81760 --- \[nio-7654-exec-8\] com.example.server.MyController          : Got a request
2022-10-10T12:57:17.820+02:00  INFO \[server,,\] 81760 --- \[nio-7654-exec-8\] com.example.server.MyHandler             : After running the observation for context \[http.server.requests\]

Вы можете посмотреть метрики, трассировки и логи в Grafana!

Ограничения в поддержке Native

На стороне клиента нам необходимо предоставить конфигурацию reflect-config.js вручную. Больше информации вы можете найти здесь.

Заключение

В этой статье мы с вами познакомились с главными понятиями, лежащими в основе Micrometer Observability API. Мы также узнали, как можно создавать наблюдения с помощью Observation API и аннотаций. Кроме того, вы можете визуализировать задержки, просматривать связанные логи и проверять метрики, получаемые из ваших Spring Boot приложений.

Вы также можете наблюдать за своими приложениями, используя нативные образы Spring Native.

Слова благодарности

Работа над Micrometer Observability была бы невозможна без широкой поддержки всей команды Spring, Тадая Цуюкубо (Tadaya Tsuyukubo), Джонни Лима (Johnny Lim) и всех других авторов и рецензентов.

Планы на будущее

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

Для нас это очень волнительный период. Мы хотели бы еще раз поблагодарить всех, кто уже внес свой вклад и оставил отзывы, и с нетерпением ждем дальнейших откликов! Ознакомьтесь с последними снапшотами Spring Boot! Также вы можете почитать документацию по нашим проектам: Micrometer Context Propagation, Micrometer, Micrometer Observation, Micrometer Tracing и Micrometer Docs Generator! Перейдите сюда, чтобы посмотреть код, использованный в этой статье.


Подробнее работу со Spring Boot разберем на онлайн-курсе «Разработчик на Spring Framework».

Комментарии (2)


  1. Kazzman
    16.09.2023 17:00

    Перевод задержался на год.

    Отправка логов в loki описана, а как трейсы добираются до tempo? И как tempo связан с loki? Про эту настройку ни в оригинале, ни у вас нет ничего.


  1. olku
    16.09.2023 17:00

    Ручное инструментирование подходит когда знаешь заранее узкие места или не можешь развернуть полноценный OpenTelemery сетап. Автоинструментирование Спринга включается в три строчки в Docker файле. Все. Логи, спаны и трейсы летят в коллектор без необходимости менять код.