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

Бизнес:
— Клиенты не могут оформить заявку, срочно почините!
Kibana:
...в одном сервисе поле applicationId, в другом appId, а в третьем aId...
...у половины логов нет traceId...
...кто-то пишет исключения в WARN, кто-то в ERROR
...кто-то в формате "Request with: $object", а кто-то "Call service".
...в Grafana все "зеленое", но бизнес жалуется...
Инженеры:
— А у вас видно что-то по этой заявке?
— Есть только время вызова, а у вас?
— А может исключение у адаптера?
— ...
A few hours later…
В итоге — 403 на интеграции из-за "scope", нашли в одном из приложений, где stackTrace не отливались.
Теперь зададим себе вопросы:
Кто вызвал кого и сколько это заняло?
Где именно разорвался процесс?
Как быстро мы найдем точку, где вызвано исключение?
Если у нас нет согласованных сквозных идентификаторов и стандартизированных логов или метрик — мы утонем в хаосе.
Квартет наблюдаемости
Вот мои киты наблюдаемости — я собрал их, опираясь на собственный опыт.
Сквозные ключи и стандарты — все события должны быть связаны единым traceId и бизнес-ключами (applicationId, productId). Названия полей согласованы во всех сервисах.
Единый формат и централизованное хранение — логируем в JSON с обязательными полями и тегами: timestamp, component, event, traceId, applicationId, productId. Аудит и метрики выгружаются в DWH или очередь.
Безопасность и порядок — маскируем и шифруем чувствительные данные. Следим за порядком событий в многопоточном и реактивном коде: рваные трейсы хуже, чем их отсутствие.
Прозрачность для разработчиков — наблюдаемость должна работать «по умолчанию». Разработчик пишет бизнес-код, все остальное делают фильтры, аспекты и библиотеки. Если разработчик думает, как здесь писать метрику, лог или аудит, значит, наши стандарты не работают.
Чек-лист для самопроверки
На стадии проектирования и аналитики:
Сквозные идентификаторы согласованы заранее.
Форматы для метрик, логов, аудита и DWH стандартизированы.
Маскирование или шифрование персональных данных включено.
Тела запросов и ответов исключены из логов, где это возможно и необходимо.
Логи, метрики, трейсы и аудит связаны через traceId или другие ключи.
На стадии реализации и поддержки:
Все логи в едином формате.
Минимизирован overhead от инструментов мониторинга (аспекты, маппинг, выгрузки).
Добавлены тесты на наличие метрик.
Подключено централизованное хранилище (ELK, Loki, ClickHouse).
Созданы дашборды для happy path и проблемных сценариев.
Я приверженец философии профилактики, а не борьбы за живучесть.
Исторический экскурс: ручка насоса Джона Сноу
В 1854 году в Лондоне свирепствовала холера. Причина считалась неочевидной (все винили «дурной воздух»), а масштаб — почти привычным злом.
Врач Джон Сноу вопреки общепринятому мнению выяснил, что источник заразы — водяная колонка на Брод-стрит. Его профилактическая мера — снять с насоса ручку — казалась современникам странной и недостаточной для столь крупной проблемы.
Такое простое действие не только немедленно остановило эпидемию в районе, но и положило начало современной эпидемиологии и системам санитарного контроля. Это классический пример того, как точечная и умная профилактика, основанная на поиске коренной причины, спасает жизни и огромные ресурсы, которые пришлось бы тратить на исправление последствий.
Примеры улучшения кодовой базы
На примере логирования покажу, как можно улучшить код-базу, централизовать отливку и в целом использовать инструменты observability более опрятно.
Есть типовой пример, как чаще всего используют средства наблюдаемости:
fun badRestExample(userId: String): String {
logger.info("Calling external for $userId")
return RestTemplate()
.getForObject("https://ext/api/user/$userId", String::class.java) ?: ""
}
component object {
val logger = KotlinLogging.logger {}
}
Как это можно сделать чище и централизованнее: выносим код для логирования как бы за скобки, используя AOP-принципы. Суть метода для нас стала чище и очевиднее:
@ObservedEvent(log = true, audit = true, metrics = true, tags = ["integration:RestTemplate"]) // ObservedEvent - метка для AOP логгера
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"}
Техническая реализация может быть любой, но целью должна быть стандартизация телеметрии. Без стандарта телеметрия превращается просто в растрату ресурсов и рабочего времени.
На правах автора статьи накидаю простенький и широкий вариант реализации такого инструмента централизации.
Мини-библиотека Observability. Для меня самый простой и очевидный способ «сквозного» наблюдения — это 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<String> = []
)
Когда мы понимаем, что нам нужно и что хотим видеть в наблюдаемости, можем собрать обработку логов в одном месте:
@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")
}
}
}
Разработчик пишет чистый бизнес-код, а вся телеметрия — в аспекте.
Важно, что во всех частных случаях реализации есть плюсы и минусы. Для AOP, например, есть много проблем с многопоточными обработками и производительностью при большом трафике. Тут не будет «серебряной пули».
Может быть много реализаций централизации — под специфику приложения или интеграции. Но ядро форматирования и поставки итогового текста в агрегирующую систему может быть единым.
Итог
Observability — это не галочка в чек-листе, а часть инженерной культуры. OpenTelemetry помогает, но если культура не выстроена — никто и ничто не спасет, будем обречены ходить по кругу и тонуть в рутине.
Мои выводы:
Наблюдаемость должна закладываться на этапе проектирования. Раньше подумаешь — дешевле исправишь.
Она должна быть прозрачной и для разработчиков, которые только пришли, и для тех, кто работает 10 лет.
Стандартизация — единственный способ избежать хаоса. Прод начинается с метрики.
Тесты нужны не только для кода, но и для метрик. Не всегда есть возможность быстро зарелизить две строки кода за 10 минут.
Тогда в пятницу вечером мы не будем «охотиться за traceId» в Kibana, а спокойно пойдем домой.
А у вас в компании как? Как тестируете метрики и стандартизируете аудит?
FlyInRoughtl
Хорошая статья чётко показано, как продуманная архитектура и культура разработки влияют на скорость и устойчивость системы. Особенно интересно про автоматизацию и контроль качества видно, что у T-Bank инженерный подход.