← назад к разделу

«Observability» в Spring-сервисе строится из трёх пилларов: metrics (что и сколько), traces (как один запрос проходит через сервисы), logs (что произошло детально). Spring Boot объединяет их в стек: Actuator + Micrometer + Micrometer Tracing + структурированные логи.

Spring Boot Actuator

Стандартные эндпоинты управления и наблюдения. Подключение:

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-actuator")
}

По умолчанию открыты /actuator/health и /actuator/info. Остальное — закрыто, нужно явно открывать:

management.endpoints.web.exposure.include=health,info,metrics,prometheus,env,loggers
management.endpoint.health.show-details=when_authorized

/actuator/health

Health check — что сервис «жив». По умолчанию состояние = aggregate из всех зарегистрированных HealthIndicator-ов (datasource, mongo, redis, kafka...).

{
  "status": "UP",
  "components": {
    "db": {"status": "UP", "details": {...}},
    "diskSpace": {"status": "UP"},
    "ping": {"status": "UP"}
  }
}

В Kubernetes используется в readiness и liveness probes:

livenessProbe:
  httpGet: { path: /actuator/health/liveness, port: 8080 }
readinessProbe:
  httpGet: { path: /actuator/health/readiness, port: 8080 }

/health/liveness — «процесс жив». /health/readiness — «готов принимать трафик». Различие важное: liveness fail → pod рестартанётся, readiness fail → pod исключается из service, но остаётся (например, под warmup).

Custom HealthIndicator

@Component
public class PricingServiceHealthIndicator implements HealthIndicator {

    private final PricingClient client;

    @Override
    public Health health() {
        try {
            client.ping();
            return Health.up().build();
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
}

Появляется в /actuator/health/pricingService автоматически.

Security для Actuator

Actuator-эндпоинты дают много информации (env, beans, mappings, configprops) — закрывать обязательно:

@Bean
public SecurityFilterChain actuatorChain(HttpSecurity http) throws Exception {
    return http
        .securityMatcher("/actuator/**")
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/actuator/health/**", "/actuator/info").permitAll()
            .anyRequest().hasRole("ACTUATOR"))
        .httpBasic(Customizer.withDefaults())
        .build();
}

Health endpoint обычно открыт, остальное — под аутентификацией.

Micrometer — абстракция над метриками

Micrometer — facade-библиотека для метрик (как SLF4J для логов). Один код, разные backends: Prometheus, Datadog, CloudWatch, New Relic, и т.д.

Три основных типа метрик:

Counter — монотонно растущая величина

@Component
@RequiredArgsConstructor
public class OrderMetrics {

    private final MeterRegistry registry;

    public void onCreated(String category) {
        registry.counter("orders.created", "category", category).increment();
    }
}

Запросы вроде «сколько заказов в категории "сладости" за час» → query на Prometheus: rate(orders_created{category="sweets"}[1h]).

Timer — измерение времени

@Bean
public Timer.Builder orderProcessingTimer() {
    return Timer.builder("order.processing");
}

@Component
public class OrderService {

    private final MeterRegistry registry;

    public void process(Order order) {
        Timer.Sample sample = Timer.start(registry);
        try {
            // ... обработка
        } finally {
            sample.stop(registry.timer("order.processing", "status", "ok"));
        }
    }
}

Запросы: p50/p95/p99 латентности, средняя длительность, бакеты гистограммы.

Gauge — текущее значение

@Component
public class QueueMetrics {

    public QueueMetrics(MeterRegistry registry, MessageQueue queue) {
        Gauge.builder("queue.size", queue, MessageQueue::size)
            .register(registry);
    }
}

Используется для «количество подключений», «размер очереди» — значения, которые меняются в обе стороны.

Стандартные метрики из коробки

Без какого-либо кода Actuator + Micrometer публикуют:

  • jvm.memory.used, jvm.gc.pause — память и GC.
  • process.cpu.usage, system.cpu.usage — CPU.
  • http.server.requests — все HTTP-запросы с тегами (uri, method, status, exception).
  • jdbc.connections.active, hikaricp.connections.idle — пул соединений БД.
  • kafka.consumer.records-lag — Kafka consumer lag (с spring-kafka).
  • rabbitmq.connections.active — RabbitMQ.

В /actuator/prometheus — выгрузка в Prometheus-формате, готовая для скрейпинга.

Distributed tracing

Когда запрос проходит через 3-5 сервисов, обычные логи бесполезны — события из разных сервисов перемешаны. Нужен trace_id, проходящий через все вызовы.

С Spring Boot 3 стандарт — Micrometer Tracing поверх OpenTelemetry или Brave (Zipkin):

dependencies {
    implementation("io.micrometer:micrometer-tracing-bridge-otel")
    implementation("io.opentelemetry:opentelemetry-exporter-otlp")
}
management.tracing.sampling.probability=1.0           # в проде 0.1 или меньше
management.otlp.tracing.endpoint=http://otel-collector:4318/v1/traces

После настройки:

  • Каждый incoming HTTP-запрос получает traceId (если нет — генерируется).
  • Исходящие RestClient / WebClient / Feign автоматически добавляют traceparent header.
  • В Kafka/AMQP-сообщениях traceparent в headers.
  • В логах MDC уже содержит traceId, spanId.
# в логах:
2026-05-18 10:23:45 INFO  [trace=abc12345 span=def67890] Processing order 42

В Jaeger / Tempo / Honeycomb видим весь путь запроса от input'а до всех downstream-сервисов.

Custom spans

Иногда нужен span на конкретную бизнес-операцию (не только HTTP/JDBC):

@Component
@RequiredArgsConstructor
public class OrderService {

    private final ObservationRegistry registry;

    public void process(Order order) {
        Observation.createNotStarted("order.process", registry)
            .lowCardinalityKeyValue("category", order.category().name())
            .observe(() -> {
                // ваш код
            });
    }
}

Observation — высокоуровневая абстракция, под капотом создающая trace span + tags для metrics.

Структурированное логирование

JSON-логи — стандарт для production: парсятся ELK / Loki / Splunk без regex'ов.

<!-- logback-spring.xml -->
<configuration>
    <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <includeMdcKeyName>traceId</includeMdcKeyName>
            <includeMdcKeyName>spanId</includeMdcKeyName>
            <includeMdcKeyName>correlationId</includeMdcKeyName>
        </encoder>
    </appender>

    <springProfile name="prod">
        <root level="INFO">
            <appender-ref ref="JSON"/>
        </root>
    </springProfile>
    <springProfile name="!prod">
        <root level="INFO">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>
</configuration>

С Spring Boot 3.4+ есть встроенная поддержка structured logging без logstash-encoder:

logging.structured.format.console=ecs   # ECS (Elastic Common Schema) формат

Что НЕ нужно метрить

Нерасчётливые метрики могут стать дороже самой обработки.

  • Не делать tag со значением высокой кардинальности (user_id, order_id, correlation_id) — Prometheus создаст separate time series для каждого. Тысячи пользователей → миллионы time series → out of memory.
  • Бизнес-данные (баланс, количество товара) метрить можно, но через Counter/Gauge с низко-кардинальными тегами (category, region, status).

Принцип: tag = категория, value = что измеряем. Если хочется измерить per-user — это сюжет для logs, не метрик.

Что почитать дальше

  • Spring Boot Reference: Actuator.
  • Observability Style Guide — правила работы с логами, метриками, трейсами в наших Java/Spring-сервисах.
  • Resilience-паттерны — что мерить для понимания состояния сервиса.
  • Scheduled, Async, виртуальные потоки — где метрики помогают отследить корректность.