Observability-as-Code - это подход, при котором базовые элементы наблюдаемости (метрики, логи, трассировки) описываются и проверяются так же строго, как и код самого приложения. Эта информация хранится в VCS и к ней выдвигаются такие же требования, как “остальному коду”. Инженеры явно описывают "контракт" наблюдаемости (какие метрики и логи должна выдавать система, с какими свойствами), снабжают этот контракт автоматическими тестами и включают проверки в процесс CI/CD.

В данной публикации мы подробно разберем, почему такой подход эффективен, и как его реализовать в Spring Boot с использованием Micrometer, OpenTelemetry и инструментов вроде ArchUnit.

Чтобы "контракт наблюдаемости" не превращался в бесконечный список метрик "я так вижу", удобно опираться на общепринятые модели:

  • Golden Signals (latency, traffic, errors, saturation) - минимальный набор сигналов здоровья user-facing системы;

  • RED (rate, errors, duration) - практичный срез Golden Signals для каждого входа в сервис (HTTP, gRPC, consumer);

  • USE (utilization, saturation, errors) - базовая модель для каждого ограничивающего ресурса (CPU/GC, пул потоков, пул соединений, очереди, lag).

Observability-as-Code встречается редко

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

  • Отсутствие явного контракта. Обычно, метрики и логи добавляются по мере необходимости, без общего "списка обязательных показателей". Как следствие, со временем покрытие наблюдаемости становится неравномерным, а новые разработчики могут не знать, что каждый новый компонент должен добавлять определенные метрики или логи.

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

  • Инструменты в основном нацелены на продакшн. Исторически фокус observability-инструментов - сбор и отображение данных в работающей системе (Prometheus, Grafana и т.д.), а не на этапе разработки. "Наблюдаемость как код" подразумевает, что конфигурации дашбордов, алертов и пр. хранятся в репозитории и проходят ревью как код. На данный момент это не самая распространенная практика, и процессы CI/CD многих компаний к этому не готовы.

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

Модель метрик и "контракт" наблюдаемости

Чтобы внедрить Observability-as-Code, сначала определим, что входит в контракт наблюдаемости Spring-сервиса. 

В первую очередь, рекомендуется опираться на каркас контракта RED + USE

  • RED - обязателен для каждого "входа" (entry point):

    • HTTP endpoint / gRPC method / Kafka listener / job.

    • Мы должны уметь ответить на 3 вопроса: сколько (Rate), насколько часто ломается (Errors), насколько долго (Duration).

  • USE - обязателен для каждого ресурса, который может ограничить пропускную способность:

    • thread pool (Tomcat/Netty event loop/пулы), DB connection pool (Hikari), очереди/lag (Kafka), память/GC, диск/сеть.

    • Мы должны уметь ответить: насколько занят (Utilization), насколько "уперся" и копится очередь (Saturation), какие ошибки дает (Errors).

Правило контракта: RED описывает "сервис как API", USE - "сервис как систему", а вместе они покрывают золотые сигналы (latency/traffic/errors через RED + saturation через USE).

Под моделью метрик мы понимаем набор метрик, трассировок и лог-сообщений, которые сервис обязан генерировать, и требования к ним:

  • Обязательные метрики - список метрик, которые должны присутствовать во всех сервисах или компонентах данного типа. Один из вариантов, для каждого REST-эндпоинта может требоваться метрика счетчика запросов и таймер времени ответа. Spring Boot Actuator уже дает общую метрику HTTP-запросов (http.server.requests), но в контракт можно включить и кастомные метрики - счетчики доменных операций в (orders.created, orders.failed и т.д.). 

  • Обязательные теги (лейблы) - для каждой метрики контракт может предписывать определенные теги. К примеру, все метрики должны иметь тег service=<name> или каждый счетчик ошибок - тег причины ошибки. Популярная ошибка: делать тэг "path" и класть в него "/products/12345" вместо того, чтобы делать плейсхолдер и класть "/products/{productId}". Эти ошибки “раздувают” БД Prometheus/VictoriaMetrics. Также, если метрика связана с методом или продуктовым сценарием, может быть требование тегировать по имени метода или функциональности.

  • Инварианты метрик - логические соотношения, которые метрики должны соблюдать. Например: метрика success_count + error_count = total_count, либо error_count никогда не превышает total_count. Еще пример - гистограмма времени ответа не должна иметь значений > N для локальных вызовов и т.п. Эти инварианты можно проверять тестами на уровне метрик.

  • Обязательные точки логирования - контракт может требовать, чтобы при определенных событиях всегда писался лог. Например: все логи должны быть коррелируемыми (traceId/spanId) и структурированными. Обязательно логируем ошибки и аномалии (ERROR/WARN) с доменным контекстом; "успехи" - только для ключевых переходов состояния и/или с sampling. Логирование "каждого запроса на INFO" лучше вынести в access-логи на периметре (gateway/ingress) или включать выборочно, иначе при высокой нагрузке это превращается в шум и существенную стоимость. Также сюда относятся требования к структуре логов (использование JSON-формата или включение корреляционного ID).

  • Трассировки - определяем, какие операции должны сопровождаться distributed trace. Для веб-сервиса каждый входящий запрос логически рассматривается как потенциальный "корень" трассировки.  Фактически же trace создается только для сэмплированных запросов - стратегия и процент сэмплирования  настраиваются (head/tail sampling) и являются частью observability-конфигурации, а не бизнес-контракта. Если трассировка активна, корневой span автоматически оборачивает HTTP-запрос, а downstream-спаны (вызовы БД, внешних API, кэша и т.д.) создаются автоматически либо явно - через Observation API для ключевых доменных этапов. Контракт может требовать наличие кастомных спанов для бизнес-критичных операций, но не гарантирует,  что они будут созданы для каждого запроса - только для тех, которые попали в трассировку. Атрибуты спанов доб��вляются с учетом PII и кардинальности: userId/orderId - только при разрешении политиками безопасности и только как high-cardinality атрибуты (не попадающие в метрики). Для метрик используются безопасные low-cardinality признаки: тип операции, результат, класс ошибки, сценарий.

Таким образом, контракт наблюдаемости похож на спецификацию API, только для телеметрии. Он может быть оформлен в виде документа (Markdown/YAML) в репозитории или кода (класс с константами метрик и методами проверки). Главное - этот контракт версионирован и развивается вместе с кодом. Новая фича - значит, вероятно, нужно обновить и описание метрик/логов.

Пример контракта: шаблон метрик для REST-сервисов

Представим упрощенный контракт для REST-сервиса:

  • HTTP (baseline): используем встроенный Timer http.server.requests с тегами method/status/uri (uri - route template, не raw path). Он дает count/sum/max (и histogram при включении).

  • Domain (custom): бизнес-метрики называем в dot-нотации (orders.created, orders.failed, order.processing) и даем только low-cardinality теги (operation/result/error_class), без userId/orderId.

  • Логирование: обязательно - структурированные ERROR/WARN (и ключевые бизнес-события) с traceId/spanId; "Starting/Completed на запрос" - либо access-логи на периметре, либо выборочно (sampling/allowlist).

  • Трейсы: root span на входящий HTTP-запрос создается автоматически, имя/route должны быть шаблонными (route template).

Многое из этого Spring Boot дает прямо "из коробки" (Actuator metrics, интеграция с OpenTelemetry). При этом Observability-as-Code подразумевает, что мы не полагаемся целиком на автоматику - мы явно утверждаем, какие сигналы должны быть, и проверяем их.

Реализация контракта в Spring Boot

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

  • Micrometer Metrics: Spring Boot Actuator + Micrometer предоставляют нам API для метрик. Мы можем объявлять нужные метрики через MeterRegistry (или ObservationRegistry, см. ниже). Например, для каждого бизнес-метода инкрементировать счетчик или записывать таймер. Хорошей практикой будет собрать все названия метрик и их теги в одном месте - enum или класс MetricsContract с константами:

public final class MetricsContract {
   public static final String METRIC_REQUEST_COUNT = "api.requests";
    public static final String TAG_ENDPOINT = "endpoint";
    // ... etc.
}
  • Таким образом, разработчики используют только константы при создании метрик, избегая опечаток. Кроме того, можно предусмотреть утилиты, которые автоматически при создании метрики добавляют стандартные теги (берут имя сервиса из настроек).

  • OpenTelemetry Tracing: стандартный путь - Micrometer Tracing (бриджи Brave или OpenTelemetry). Стартер spring-boot-starter-opentelemetry, упрощает включение OTLP-экспорта для трейсов/метрик/логов через management. свойства. Он интегрируется с Micrometer Tracing и умеет отправлять трейсы по протоколу OTLP. Это значит, что каждое наблюдение в приложении может автоматически становиться как метрикой, так и спаном трассировки. Spring Boot уже создает наблюдение (observation) для каждого HTTP-запроса, поэтому мы получим базовые трейсы без кода. Но для контрактных внутренних спанов можно вручную использовать API Observation или аннотации.

  • Аннотации наблюдаемости: Micrometer предоставляет аннотации, например @Timed, @Counted, @Observed, @NewSpan. Мы можем пометить каждый метод контроллера аннотацией @Timed(value="requests.duration", extraTags={"endpoint", "<name>"}) - тогда Micrometer сам будет собирать таймеры и счетчики вызовов метода. Но осторожно: некоторые контроллеры уже автоматом обернуты Observations, и аннотации могут задублировать метрики.

  • Observation API: Альтернативно, в коде можно явно создавать observation. Как один из примеров:

observationRegistry.observationConfig().observationConvention(new MyObservationConvention());
Observation.createNotStarted("processOrder", observationRegistry)
          .lowCardinalityKeyValue("orderType","digital")
          .highCardinalityKeyValue("userId", userId)
          .observe(() -> service.processOrder());
  • Это запишет метрику и создаст span с именем processOrder, имеющий указанные атрибуты (lowCardinality пойдут и в метрики, high только в трассу). Этот подход удобен для того, чтобы вокруг критических блоков кода самостоятельно ставить "замеры", задавая имена по контракту.

  • Логирование: Чтобы соблюдать контракт логирования, стоит использовать структурированные логи (эту тему мы также рассматривали в нашей статье Структурное логирование в Spring Boot 3.4). Логгер через Logback + JSON encoder, чтобы каждое событие было JSON с полями (timestamp, level, message, traceId, ...). Тогда можно требовать, чтобы определенные поля всегда присутствовали. Spring Observability позволяет связывать логи и трассировки - с помощью MDC вставлять текущий trace-id в каждый лог. Можно в конфигурации Logback добавить %X{traceId} в шаблон лога, и тогда каждый лог-сообщение автоматически содержит идентификатор трассировки.

Для ключевых мест, скорее всего, придется явно вызывать логгер. Написать аспект или фильтр, который на входе запроса пишет "Incoming request [endpoint=..., traceId=...] и на выходе - "Completed request ... in ...ms". В простейшем случае можно делать это в каждом контроллере вручную (на уровне template метода/аннотации). Главное - не пропустить новый endpoint. Мы позже увидим, как это контролировать инструментально.

Интеграция с observability-стеком: Наша цель - сгенерированные метрики/логи/трейсы должны попадать в стандартные инструменты: Prometheus/Grafana для метрик, Grafana Loki для логов, Grafana Tempo для трейсов (или Jaeger/Zipkin аналоги). К счастью, выбранные технологии легко интегрируются:

  • Prometheus: /actuator/prometheus появляется, когда на classpath есть spring-boot-starter-actuator и io.micrometer:micrometer-registry-prometheus; сам эндпоинт нужно явно экспонировать (например, management.endpoints.web.exposure.include=prometheus). Альтернатива - использовать OtlpMeterRegistry, отправляя метрики прямо в OpenTelemetry Collector (Grafana Alloy). Alloy - дистрибутив OpenTelemetry Collector от Grafana: он принимает OTLP, обрабатывает сигналы и экспортирует их дальше. Для метрик корректнее говорить так: Alloy может либо отдать metrics endpoint для Prometheus scrape (pull-модель), либо отправлять метрики в remote storage (Mimir) по push-механизмам. Логи обычно уходят в Loki, трейсы - в Tempo.

  • Loki: Логи можно сразу писать в Loki через docker driver (если наше приложение в контейнере) или через OTel Collector (Alloy) настроить прием логов. Spring Boot 4 дает конфигурационные точки для OTLP-логирования, но "из коробки" логи по OTLP не поедут - нужен appender/handler для вашей logging-системы (Logback/Log4j2). Если не хотите тащить экспериментальные/сыроватые OTLP-appenders, самый безопасный путь для продакшена: JSON-логи в stdout + агент/драйвер (promtail / fluent-bit / docker logging driver) до Loki. OTLP-логи включайте осознанно, как часть контракта и инфраструктуры.

  • Tempo: Для трассировок используем OTLP экспорт из Spring Boot. Boot + Micrometer Tracing с OTel-бриджем экспортирует трейсы по OTLP: по умолчанию через OTLP/HTTP (HTTP exporter), а OTLP/gRPC можно выбрать при необходимости. Дальше - напрямую в Tempo или через Alloy/OTel Collector. Если вы используете Spring Boot 4 + spring-boot-starter-opentelemetry, настраивайте OTLP через application.yaml/properties:

management.opentelemetry.tracing.export.otlp.endpoint=...
management.otlp.metrics.export.url=...
management.opentelemetry.logging.export.otlp.endpoint=...
  • а имя сервиса задавайте через spring.application.name.

    Переменные OTEL_ используйте в сценарии javaagent/чистого OpenTelemetry SDK - не смешивайте оба подхода в одном объяснении без оговорки.

  • Grafana: Grafana служит единым окном, где мы настроим дашборды для метрик и визуализацию трассировок. Grafana может связывать метрики/трейсы/логи: провал из метрики в трейс возможен при включенных exemplars/корреляции, а связь трейс→логи обычно делается по traceId. Это нужно один раз настроить в стеке (Tempo/Loki/Prometheus/Mimir) и зафиксировать как часть Observability-контракта окружения.

Архитектура наблюдаемости Spring Boot сервиса: приложение публикует метрики через Actuator/Micrometer (собираются Prometheus или OTel-коллектором), логи отправляются в Loki (через драйвер или коллектор), трассы по OTLP в Tempo. Grafana отображает метрики и позволяет переходить к связанным трейсам и логам.

Таким образом, инструментарий есть - основная работа в рамках Observability-as-Code заключается не столько в настройке экспорта, сколько в гарантии, что разработчики не забывают покрывать код метриками и логами согласно контракту. Этого можно достичь с помощью тестов и статических анализаторов.

Тестирование метрик и логов

Наличие хорошо определенного контракта позволяет писать авто-тесты, которые проверяют соблюдение этого контракта. Такие тесты делятся на юнит-тесты, проверяющие отдельные метрики/логи в конкретных классах, и интеграционные, проверяющие работу системы в целом (через Actuator). Рассмотрим техники тестирования:

Unit-тесты для метрик

Для unit-тестирования метрик удобно воспользоваться in-memory реализацией MeterRegistry, которую предоставляет Micrometer - класс SimpleMeterRegistry. В тесте мы можем передать этот registry нашему сервису, вызвать методы, а затем проверить, что в registry появились нужные метрики с правильными значениями. Если мы протестируем метод FooService.foo(), который должен увеличивать счетчик foo.count:

MeterRegistry meterRegistry = new SimpleMeterRegistry();
FooService fooService = new FooService(meterRegistry);

// Act: вызываем метод несколько раз
fooService.foo();
fooService.foo();
fooService.foo();

// Assert: проверяем, что счетчик увеличился на 3
double invocations = meterRegistry.get("foo.count").counter().count();
assertThat(invocations).isEqualTo(3);

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

Micrometer также предлагает специальный модуль micrometer-test с удобными ассертами. Подключив зависимость тестового scope, можно писать проверки вроде MeterRegistryAssert.assertThat(meterRegistry).hasTimerWithName("foo.time") - то есть убедиться, что таймер с таким именем зарегистрирован после выполнения операции.

Важно: если вы пишете @SpringBootTest, где MeterRegistry будет общим для всех тестов, то после каждого теста его надо чистить (meterRegistry.clear()), иначе метрики будут накапливать значение между тестами.

Таким образом, юнит-тесты метрик проверяют локально, что при вызове функций счетчики и другие метрики меняются строго по контракту. Мы можем написать тест на инвариант: вызвать success и error и проверить, что total_count = success+error.

Интеграционные тесты для метрик

На интеграционном уровне (поднимая контекст Spring) можно проверить, что вся необходимая телеметрия доступна "снаружи" так, как ожидается мониторингом. Например:

  • Проверять наличие метрик через /actuator/metrics/<name> корректно только после smoke-сценария, который гарантированно регистрирует эти метрики (например, 1-2 HTTP вызова + 1 доменная операция). Иначе тест может быть флейки из-за ленивой регистрации meters.

  • Либо воспользоваться meterRegistry.getMeters() внутри теста и сравнить набор имен с ожидаемым списком из контракта.

  • Проверить, что значения метрик меняются при настоящих HTTP-запросах. Через TestRestTemplate выполнить вызов контроллера, а затем проверить, что у http.server.requests вырос count (Timer) для нужных тегов method/status/uri (uri - route template).

  • Если используются распределенные трейсы, в тесте можно проверить наличие заголовков трассировки. Не проверяйте трассировку по traceparent в HTTP-ответе: сервер не обязан возвращать этот заголовок. Для e2e-проверки поднимайте in-memory exporter (или тестовый OTLP-collector) и проверяйте список сформированных spans. Заголовок traceparent в ответ добавляйте только если это явное требование вашего контракта (через фильтр/интерсептор). Можно также подключить in-memory экспортер OpenTelemetry (OTel SDK с InMemorySpanExporter) и проверить, что после тестового сценария экспортер содержит ожидаемые spans. Этот путь сложнее, но реализуемый.

Для логов интеграционный тест может использовать библиотеку захвата логов. Один из подходов - подключить в тесте кастомный Appender к Logback, который сохраняет события в список. Logback Classic позволяет программно добавить ListAppender<ILoggingEvent> на нужный логгер. В тесте мы можем выполнить действие и затем проверить список appender.list на наличие сообщений с нужным текстом или уровнем.

Пример: проверим, что при обработке заказа логируется ошибка, если произошел эксепшн:

ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.start();
((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(OrderService.class))
    .addAppender(appender);

// вызов, провоцирующий ошибку
try { orderService.process(null); } catch(Exception e) { /* ignore */ }

// теперь проверить, что в appender поймалось как минимум одно ERROR-сообщение с словом "Exception"
boolean errorLogged = appender.list.stream().anyMatch(event ->
    event.getLevel() == Level.ERROR && event.getFormattedMessage().contains("Exception while processing order"));
assertTrue(errorLogged);

Такой тест гарантирует, что в коде не забыли залогировать исключение. Аналогично можно проверять наличие обязательных полей: traceId присутствует в MDC и попал в сообще��ие (это сложнее без полного end-to-end, но можно эмулировать MDC).

Совет: Для удобства можно написать util-классы для тестов, которые инкапсулируют проверку контракта. Метод ObservabilityTestHelper.assertMetricsPresent(MeterRegistry, Set<String> requiredMetrics) или LogTestHelper.expectLog(Level.INFO, "Completed.*"). Тогда при добавлении новой обязательной метрики разработчик поправит один список в helper’e и тесты сразу начнут требовать ее везде.

Архитектурные проверки: ArchUnit

Кроме runtime-тестов, мощным подспорьем могут стать статические проверки на уровне кода. Библиотека ArchUnit позволяет анализировать байткод Java и проверять различные правила архитектуры с помощью unit-тестов. С их помощью мы можем автоматически ловить случаи, когда разработчик добавил новый код, но забыл соблюсти контракт наблюдаемости.

Отслеживание новых эндпоинтов без метрик/логов

Главный риск - появление нового REST-эндпоинта или бизнес-метода без должного инструментирования. Не формулируйте контракт как "в каждом контроллере обязательны INFO-логи и кастомные метрики": HTTP-метрики/трейсы уже покрываются авто-инструментацией. Контракт лучше закреплять на доменных use-case/service методах: там обязательны бизнес-метрики/спаны и структурированные ERROR/WARN, а web-слой оставляем на стандартных http.server.requests и server spans.

  • ArchUnit для логов: можно написать ArchUnit-правило, которое ищет вызовы логгера без контекста. Пример из практики - проверка, что все logger.info вызываются с параметрами (контекстом). Ниже адаптированный пример: запретить вызовы Logger.info(String) без параметров:

import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import org.slf4j.Logger;


import static com.tngtech.archunit.base.DescribedPredicate.describe;
import static com.tngtech.archunit.core.domain.JavaCall.Predicates.target;
import static com.tngtech.archunit.lang.conditions.ArchConditions.callMethodWhere;
import static com.tngtech.archunit.lang.conditions.ArchPredicates.is;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;


public class LoggingRules {

    @ArchTest
    static final ArchRule noInfoLoggingWithoutContext =
            noClasses().should(callMethodWhere(target(is(describe("logger.info without context",
                    t -> t.getOwner().isAssignableTo(Logger.class)
                            && t.getName().equals("info")
                            && t.getRawParameterTypes().size() < 2
            )))));
}
  • Это правило пройдет, только если все вызовы info имеют хотя бы два параметра (сообщение и один аргумент). В нашем случае мы могли бы конкретизировать: требовать, чтобы в сообщении был placeholder и хотя бы один аргумент (что означает наличие контекста). Аналогично можно запретить logger.error(String msg) без исключения. Такие правила гарантируют, что разработчики пишут информативные логи с контекстом (ID сущностей, и т.д.).

  • ArchUnit для метрик: Отслеживать использование метрик сложнее, но можно придумать правила. Правило: класс контроллера (аннотирован @RestController) должен либо вызывать MeterRegistry, либо быть аннотирован @Timed/@Observed. ArchUnit умеет проверять наличие аннотации у классов и методов. Если у нас контракт, что все эндпоинты должны иметь аннотацию @Observed, то простейшее правило:

classes().that().areAnnotatedWith(RestController.class)
         .should().beAnnotatedWith(Observed.class);
  • Хотя на практике это слишком строго и не гибко (некоторые могут предпочесть метрики вызывать в сервисном слое).

В итоге ArchUnit - более прямой способ закодировать проверки. Они выполняются как обычные unit-тесты и падают, если нарушено правило. Рекомендуется сопровождать такие тесты пояснением (ссылкой на ADR или раздел README), почему правило важно. Тогда разработчику будет понятно, что тест упал, потому что "Новый endpoint X не логгирует событие начала/конца - добавьте лог во исполнение контракта наблюдаемости".

Примеры проверок контракта

Что можно автоматизировать:

  • Полнота метрик: Тест, проверяющий, что список метрик в meterRegistry содержит все имена из списка MetricsContract.REQUIRED_METRICS. Если чего-то нет - либо метрику забыли зарегистрировать, либо ошиблись в имени. Этот тест должен выполняться после короткого smoke-сценария (минимальный набор вызовов/операций), потому что часть метрик регистрируется лениво - только при первом реальном использовании.

  • Запрет изменений метрик без явного обновления контракта: Кстати, вышеупомянутый тест также ловит ситуацию, когда разработчик переименовал метрику или удалил - тогда тест упадет. Чтобы тест не падал, нужно обновить список REQUIRED_METRICS - это и есть "обновление контракта". Таким образом, любой MR с изменением метрик заставит изменить и контракт, иначе pipeline красный.

  • Отслеживание новых endpoint’ов: Можно автоматически получать список URL, обслуживаемых приложением (через WebApplicationContext.getBean(RequestMappingHandlerMapping) - он выдаст все mappings). Для каждого URL проверить, что либо его контроллер/метод помечен @Observed/@Timed, либо он вызывает metric/log. Это сложнее, но даже простая эвристика - хотя бы наличие @Observed - уже дисциплинирует.

  • Архитектурные инварианты: Проверки ArchUnit, как в примерах, можно включить: финальность логгеров, наличие контекста в логах, отсутствие вызовов System.out вместо логгера, и т.д. Эти вещи опциональны, но улучшают качество наблюдаемости.

Подобные проверки следует запускать в CI. Обычно это просто часть mvn test. Если какая-то из них падает, сборка провалится, и merge request не пройдет. Таким образом достигается требуемое: "MR без обновления observability-контракта - FAIL". Команда начнет воспринимать изменения в наблюдаемости всерьез и сопровождать их нужными правками в коде и документации.

Встраивание в CI/CD и процесс разработки

Когда инфраструктура Observability-as-Code настроена, важно интегрировать ее в ежедневный процесс:

  • CI/CD pipeline: В конвейер сборки (GitLab CI) включаются шаги запуска тестов. Здесь нет ничего особенного, просто все вышеописанные тесты (юнит, интеграционные, ArchUnit) должны выполняться. При желании можно сделать отдельную джобу, например observability-check, которая будет явно запускать только эти проверки, и публиковать отчет (но достаточно и общей сборки).

  • Code Review: Полезно описать в CONTRIBUTING, что любой значимый код, влияющий на метрики или логи, должен сопровождаться соответствующими изменениями в контракте и тестах. Ревьюеры кода могут использовать чек-лист: "Добавил новый публичный метод -> а где метрика? Добавил новый ключевой WARN-лог -> а тест на него есть?". Со временем разработчики привыкают добавлять наблюдаемость сразу.

  • Версионирование контракта: Контракт (документ Markdown с перечнем всех метрик и логов) может поддерживаться вручную и обновляться в каждом MR. Однако это легко забыть. Лучше, когда сам код (списки констант, тесты) выступают живой документацией. Можно сгенерировать документацию автоматически: у нас есть enum всех метрик - написать тест, который выкачивает /actuator/metrics и сверяет, что для каждой метрики из enum есть описание (Spring Actuator позволяет задавать описание метрик через MeterRegistry.config().meterFilter(...)). Если описание отсутствует - тест напомнит дописать. Эту документацию можно публиковать артефактом.

  • Мониторинг самого контракта: Интересная идея - метрики на сами метрики! Мы можем завести счетчик "количество метрик в системе" и отслеживать его по времени. Резкое снижение будет сигналом, что кто-то отключил значимую метрику. Или метрика "покрытие контрактом" - процент endpoint’ов, имеющих логирование (эти вещи можно вычислять и отправлять как спецметрику при старте приложения).

В итоге CI/CD, тесты и ревью образуют Quality Gate для наблюдаемости. Этот подход схож с тем, как мы защищаем другие аспекты качества (без новых юнит-тестов код не пройдет, без обновления OpenAPI спеки MR не мерджится и т.д.). Практика крупной компании показывает: "когда observability-требования автоматизированы, они перестают игнорироваться" - разработчики начинают проектировать фичи с учетом мониторинга изначально, то есть по сути Shift-Left Observability.

Лучшие практики и выводы

Observability-as-Code - это следующий шаг в эволюции DevOps-культуры, сближающий разработку и эксплуатацию. Подведем ключевые рекомендации для внедрения такого подхода в Spring-проектах:

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

  • Используйте возможности Spring Boot по максимуму. Включите Spring Actuator, Micrometer, Observability аннотации. Больше половины работы (HTTP метрики, базовые трейсы) сделается автоматически. Остальное достраивайте поверх (к примеру, бизнес-метрики через MeterRegistry.counter(...).increment() в нужных местах).

  • Пишите тесты на метрики и логи. Это необычно, но как мы показали - реализуемо. Unit-тесты с SimpleMeterRegistry быстры и ловят регрессии метрик. Логирование можно тестировать через добавление кастомного Appender. Эти тесты не только ловят забытые метрики, но и документируют, какие значения должны расти при тех или иных действиях.

  • Внедрите архитектурные проверки. ArchUnit - простой в освоении инструмент, чтобы запретить анти-паттерны ("логгеры должны быть private final" или "не log.error без stacktrace"). Также можно отслеживать слои приложения: требовать, чтобы класс в слое контроллеров не вызывал репозиторий напрямую (это уже про архитектуру, но косвенно влияет и на наблюдаемость, соблюдение структурных constraints).

  • Интегрируйтесь с мониторингом как с кодом. Не забудьте про обратную сторону: не только само приложение, но и окружение мониторинга должно быть управляемо кодом. Дашборды Grafana, оповещения Prometheus - все это можно хранить в Git (JSON для Grafana, правила алертов в коде). Grafana предоставляет возможности Observability as Code для своих конфигураций. Это поможет синхронизировать изменения: новая метрика - сразу PR с обновлением дашборда, и они выкатываются одновременно.

  • Обучайте команду и отслеживайте метрики процесса. Вводя новые проверки, убедитесь, что команда понимает их смысл. Хорошо провести мастер-класс по Micrometer, объяснить, как влияют теги на кардинальность метрик, как правильно логировать без излишней болтовни. Также следите за "метриками наблюдаемости" - процент покрытых логированием потоков, среднее число метрик на сервис. Это мета-метрики, но они покажут прогресс в культуре.

В итоге, реализуя Observability-as-Code, вы достигаете того, что наблюдаемость перестает быть чем-то из разряда “неплохо было бы”. Она становится встроенной в разработку: при проектировании API мы сразу думаем, какие метрики нужны, при написании функции сразу вставляем счетчики/логи, а CI не даст забыть о важном. Это особенно важно для senior/lead разработчиков и архитекторов, которые отвечают за надежность системы, так как контракт наблюдаемости становится для них инструментом управления качеством.

И хотя такой подход пока еще редок, со временем он может стать таким же обыденным, как сейчас Continuous Integration. Ведь система, которую легко наблюдать и отслеживать, заведомо более надежна и предсказуема в эксплуатации. Мы не можем контролировать то, что не измеряем - а Observability-as-Code гарантирует, что вы измеряете все, что нужно, и делаете это системно и автоматизированно.