Привет! Меня зовут - Евгений, работаю в финтехе и проектирую системы, которые обрабатывают миллионы запросов, интегрируются с десятками внешних сервисов и живут в 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, а спокойно пойдем домой.

А у вас в компании как? Как тестируете метрики и стандартизируете аудит?

Комментарии (1)


  1. FlyInRoughtl
    11.11.2025 15:40

    Хорошая статья чётко показано, как продуманная архитектура и культура разработки влияют на скорость и устойчивость системы. Особенно интересно про автоматизацию и контроль качества видно, что у T-Bank инженерный подход.