
Здравствуйте, я Евгений, инженер в сфере финтеха. Я проектирую масштабируемые системы с миллионами запросов, интегрирую их с разнообразными внешними сервисами и разворачиваю в Kubernetes. Кроме того, я преподаю Java и Spring Boot и помогаю студентам избегать чужих ошибок, опираясь на собственный опыт.
У меня более десяти лет опыта в разработке, и за это время я неоднократно сталкивался с одной и той же проблемой: отсутствие системного подхода к наблюдаемости. Логи, метрики и трассировки появляются вскользь — что-то подключили для отладки, что-то пришло вместе со сторонней библиотекой, в продакшене настраивают в режиме аврала. В итоге инженерам приходится часами разбираться в инцидентах, а команды продуктов теряют скорость.
В этой статье я поделюсь нашим подходом: как мы выстраиваем наблюдаемость в распределённых системах и почему OpenTelemetry — это не просто инструменты, а философия, которой мы следуем.
Наблюдаемость как часть культуры, а не набор утилит
Распространённая ошибка: «добавим Micrometer, отправим метрики в Prometheus — и дело сделано». На практике этого недостаточно.
Часто наблюдаемость воспринимают как ещё одну зависимость в pom.xml, однако на самом деле это инженерная привычка, пронизывающая все этапы разработки и эксплуатации:
- Логи нужны не только для отладки ошибок, но и для воссоздания последовательности бизнес-сценариев.
- Метрики важны не только для дашбордов в Grafana, но и для понимания нагрузки и выявления узких мест.
- Трассировки — это не игрушка SRE, а ключ к пониманию работы распределённой системы в реальном времени.
Если эти практики не встроены в повседневную работу, мы обречены натыкаться на одни и те же проблемы снова и снова.
OpenTelemetry служит основой нашего подхода к наблюдаемости. Это не просто библиотека для метрик, логов и трассировок, а единая концепция, лежащая в основе Micrometer, Prometheus, Grafana и Jaeger. Важно не просто подключить зависимость, а вплетать практики наблюдаемости в процессы разработки, проектирования и code review.
Травмирующий случай из практики
Представим упрощённую архитектуру платформы.

Бизнес: Клиенты не могут оформить заявку — срочно разберитесь!
Мониторинг: В одном сервисе поле называется applicationId, в другом — appId, в третьем — aId. Половина логов не содержит traceId. Регистр ошибок не единообразен: что-то уходит в WARN, что-то в ERROR. Форматы сообщений разбросаны. В Grafana все зелёное, а жалобы продолжаются…
Инженеры: У кого есть traceId? Что возвращает адаптер? В каком сервисе произошла ошибка? Почему нет stackTrace?
Спустя несколько часов…
Оказалось, из-за некорректного scope при интеграции возвращался 403. Найти источник помогла лишь частично настроенная трассировка в одном сервисе.
Задайте себе вопросы:
- Кто вызвал сервис и сколько времени это заняло?
- В каком месте цепочки произошёл сбой?
- Как быстро мы найдём точку возникновения исключения?
Без согласованных сквозных идентификаторов и единых форматов логов и метрик мы утонем в хаосе.
Четыре столпа наблюдаемости
Основные принципы нашего подхода:
1. Сквозные идентификаторы и единые соглашения. Все события связываются через единый traceId и бизнес-ключи (applicationId, productId). Названия полей стандартизированы во всех сервисах.
2. Упорядоченный и централизованный формат хранения. Логи в JSON с обязательными полями: timestamp, component, event, traceId, applicationId, productId. Метрики и аудит выгружаются в DWH или через очередь сообщений.
3. Безопасность и консистентность. Маскировка и шифрование чувствительных данных. Контроль порядка событий в многопоточном и реактивном коде: «рваные» трассировки хуже их полного отсутствия.
4. Прозрачность для разработчиков. Наблюдаемость работает «из коробки». Разработчик пишет бизнес-логику, а все остальное на себя берут фильтры, аспекты и библиотеки.
Чек-лист для самопроверки
На этапе проектирования:
- Сквозные идентификаторы и ключи согласованы заранее.
- Форматы логов, метрик, аудита и DWH утверждены.
- Маскирование или шифрование персональных данных настроено.
- Тела запросов и ответов исключены из логов, где это нужно.
- Логи, метрики, трассировки и аудит связаны сквозными ключами.
На этапе реализации и поддержки:
- Единый формат логов во всех сервисах.
- Минимизация overhead от инструментов мониторинга.
- Тесты на корректность метрик.
- Подключены централизованные хранилища (ELK, Loki, ClickHouse).
- Настроены дашборды для основных и проблемных сценариев.
Я убеждён в превентивном подходе, а не в постоянной борьбе с последствиями.
Исторический пример: ручка насоса Джона Сноу
В 1854 году в Лондоне свирепствовала холера. Врачи того времени винят «дурной воздух», но Джон Сноу установил: эпицентром заражения стала колонка на Брод-стрит. Его превентивная мера — снятие рукояти насоса — сразу же остановила эпидемию и заложила основы современной эпидемиологии.
Пример упорядочения телеметрии в коде
Рассмотрим, как упростить и централизовать логирование через AOP.
Типичный «плохой» пример:
fun badRestExample(userId: String): String {
logger.info("Calling external for $userId")
return RestTemplate()
.getForObject("https://ext/api/user/$userId", String::class.java) ?: ""
}
object LoggerHolder {
val logger = KotlinLogging.logger {}
}
Улучшаем с помощью аспекта и аннотации:
@ObservedEvent(log = true, audit = true, metrics = true, tags = ["integration:RestTemplate"])
fun getUserData(userId: String): String {
return restTemplate.getForObject("https://ext/api/user/$userId", String::class.java) ?: ""
}
При централизованном логировании запись приобретает единый вид:
{"timestamp":"2025-08-12T10:00:00Z","event":"getUserData.start","integration":"RestTemplate","userId":"123","traceId":"abc-123"}
{"timestamp":"2025-08-12T10:00:01Z","event":"getUserData.completed","integration":"RestTemplate","userId":"123","traceId":"abc-123"}
Ключевая цель — стандартизировать телеметрию, чтобы она не превращалась в беспорядок.
Простейшая мини-библиотека для «сквозного» наблюдения на основе AOP:
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ObservedEvent(
val log: Boolean = true,
val audit: Boolean = false,
val metrics: Boolean = true,
val tags: Array = []
)
Далее объединяем логику телеметрии внутри аспекта:
@Aspect
@Component
class ObservedEventAspect(private val observability: ObservabilityService) {
@Around("@annotation(observed)")
fun around(joinPoint: ProceedingJoinPoint, observed: ObservedEvent): Any? {
val span = observability.startTrace(observed.tags)
val timer = if (observed.metrics) observability.startTimer(joinPoint.signature.name) else null
observability.logInfo("${joinPoint.signature.name}.start")
return try {
joinPoint.proceed()
} catch (ex: Throwable) {
observability.logInfo("${joinPoint.signature.name}.error", "error" to ex.message)
throw ex
} finally {
if (observed.audit) observability.audit("${joinPoint.signature.name}.completed")
timer?.let { observability.endTimer(it, joinPoint.signature.name) }
observability.endTrace(span)
observability.logInfo("${joinPoint.signature.name}.end")
}
}
}
Разработчик пишет чистый бизнес-код, а аспекты и библиотеки берут на себя всю телеметрию.
Выводы
Наблюдаемость — это не просто пункт из чек-листа, а неотъёмлемая часть инженерной культуры. OpenTelemetry даёт инструменты, но без правильно выстроенных процессов мы вновь столкнёмся с хаосом.
Основные выводы:
- Наблюдаемость закладывается на этапе проектирования — чем раньше, тем дешевле исправлять.
- Она должна быть прозрачной и удобной для всех разработчиков, независимо от стажа.
- Стандартизация — единственный способ избежать хаоса. Начинайте с метрик.
- Тесты нужны не только для кода, но и для метрик и логов.
Так в пятницу вечером вам не придётся охотиться за traceId в логах, а можно спокойно уйти домой.
Как в вашей компании организована проверка метрик и аудит?



