«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 автоматически добавляютtraceparentheader. - В 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, виртуальные потоки — где метрики помогают отследить корректность.