Библиотеки observability для Go: логи, метрики и трассы
Библиотеки наблюдаемости для Go помогают небольшой команде получать полезные логи, метрики и трассы без лишних обёрток. Этот план охватывает настройку, ошибки и проверки.

Почему наблюдаемость кажется больше самого приложения
Небольшой Go-сервис может уместиться в нескольких файлах. А вот всё, что связано с наблюдаемостью, часто уже нет. Вы добавляете логи, потом метрики, потом трассировку, потом экспортёры, дашборды, алерты, labels, правила семплирования и выбор названий. Очень быстро поддерживающий код начинает выглядеть больше, чем сама функция, которую вы хотели выпустить.
Так происходит потому, что каждый инструмент решает только часть задачи, а инциденты никогда не приходят аккуратными кусками. Медленный checkout — это не просто скачок метрики. Это ещё и trace с пропавшим span, лог без request ID и дашборд, который отвечает только на половину вопроса. Сервис остаётся простым. Обвязка вокруг него быстро разрастается.
Команды только ухудшают ситуацию, когда сначала начинают писать обёртки для всего. Свой logger, самодельный слой метрик, пакет-помощник для tracing, общие теги, middleware-помощники. В первый день это кажется аккуратным. Через неделю люди уже отлаживают обёртки вместо приложения. И всё равно не могут ответить на главный вопрос: что сломалось, где и у кого?
Небольшой команде не нужен внутренний observability-проект. Нужны быстрые ответы в плохой час. Если одного человека подняли по тревоге, он должен быстро найти упавший запрос, увидеть, какой dependency замедлился, и понять, затронута ли одна tenant или вообще все.
Начните с вопросов, на которые хотите отвечать, когда что-то идёт не так:
- Какой запрос упал и какая была ошибка?
- Латентность выросла в приложении или в dependency?
- Это новая проблема или она началась после deploy?
- Она затрагивает всех пользователей или только небольшую часть?
Эти вопросы помогают не обманывать себя. Они останавливают разрастание инструментов ещё до того, как оно начнётся.
Хороший стек для небольшой Go-команды должен быть скучным в лучшем смысле. Он даёт логи, метрики и трассы, которые связаны с одним и тем же запросом или задачей. И не заставляет сначала придумывать платформу. Если команда тратит неделю на обвязку, прежде чем увидеть хотя бы один полезный график, стек слишком тяжёлый.
Что должен покрывать лёгкий Go-стек
Небольшой команде не нужна огромная observability-настройка. Ей нужны быстрые ответы. Хорошие библиотеки observability для Go должны показывать, что произошло в одном событии, что изменилось со временем и где застрял один конкретный запрос.
Логи отвечают за первую часть. Одна запись лога должна ясно объяснять одно событие: запрос начался, вызов payment не удался, retry сработал, пользователь получил 500. В Go лучше всего работает структурированное логирование, потому что можно фильтровать по полям вроде request_id, route, user_id и error, не продираясь через стену текста.
Метрики отвечают за вторую часть. Они показывают не историю, а паттерны. Если доля ошибок выросла с 0,2% до 4% или p95 latency удвоилась после deploy, метрики покажут это за несколько секунд. Для большинства приложений не нужно много метрик. Начните с количества запросов, количества ошибок, latency, глубины очереди, если вы используете задачи, и нескольких системных показателей вроде памяти или goroutines.
Трассы отвечают за третью часть. Они ведут один запрос через приложение и показывают, куда ушло время. Это особенно важно, когда handler выглядит нормально, а реальная задержка сидит в запросе к базе, проверке auth или медленном вызове другого сервиса. OpenTelemetry Go здесь полезен, потому что может переносить этот контекст через HTTP-handlers, database calls и фоновые задачи.
Часть, которую многие команды упускают, — это общий путь request across all three signals. Если checkout-запрос падает, вы должны перейти от всплеска в метриках к медленной трассе, а затем к точным строкам логов для того же запроса. Это работает только тогда, когда один и тот же ID проходит по всему пути.
Если пропустить эту точку соединения, каждый сигнал станет отдельной загадкой. Если сохранить её, лёгкий стек ощущается гораздо мощнее, чем есть на самом деле. Prometheus client Go для метрик, структурированные логи и трассы, связанные одним request ID или trace ID, закроют потребности большинства команд надолго.
Выбирайте библиотеки с минимумом связок
Лёгкой команде лучше жить с меньшим количеством движущихся частей. В этом и смысл выбора библиотек observability для Go, которые уже хорошо сочетаются друг с другом, а не прячут всё за самодельными обёртками, которые потом придётся отлаживать.
Для логов начните с slog, если хотите идти путём стандартной библиотеки. Он простой, читаемый и достаточно хорош для большинства сервисов. Выбирайте zerolog, если для вас особенно важны скорость и низкое число выделений памяти. Оба варианта дают structured logging, который Go-команды могут поддерживать единообразно во всех сервисах. Ошибка не в выборе между ними. Ошибка — добавлять собственный слой логирования до того, как вы поймёте, какие поля вам вообще нужны.
Для трассировки используйте OpenTelemetry Go напрямую. Он даёт spans, trace ID и передачу контекста через HTTP-handlers, database calls и исходящие запросы. Это важнее, чем модные функции tracing. Если запрос ломается в billing после прохождения signup, вам нужна одна trace, чтобы увидеть весь путь без ручной проводки в каждом пакете.
Для метрик сервиса Prometheus client for Go всё ещё остаётся самым простым вариантом по умолчанию. Counters, histograms и gauges закрывают большинство задач. Вам не нужны двадцать типов метрик. Нужны несколько названий, которые не меняются и честно показывают частоту запросов, latency, ошибки, глубину очереди и здоровье фоновых задач.
Если это уместно, используйте одинаковые названия полей в логах, трассах и labels метрик. Небольшого набора обычно хватает:
request_idtrace_iduser_idrouteservice
Этого достаточно для большинства отладочных задач.
Подключайте exporter только после того, как локальный вывод уже работает. Сначала выводите JSON-логи в stdout. Затем откройте /metrics и проверьте числа вручную. Сначала печатайте spans в консоль или отправляйте их в локальный collector, а уже потом отправляйте всё в удалённый backend. Такой порядок экономит время, потому что можно исправить названия, cardinality и недостающий контекст, пока настройка ещё маленькая.
Если начать со slog или zerolog, OpenTelemetry и Prometheus client, можно за один проход получить полезные логи, трассы и метрики. Такой стек скучный в хорошем смысле. Он оставляет команде меньше адаптеров для поддержки и больше времени на исправление реальных багов.
Настройте это за один проход
Подключайте observability на краю приложения. В Go это обычно означает один HTTP middleware, который запускает trace, собирает logger и записывает время каждого запроса. Большинство библиотек observability для Go нормально работают вместе, если context.Context передаёт одни и те же значения до самого низа.
Middleware должен создавать или принимать request ID, добавлять имя route и включать только безопасные поля, такие как tenant ID, план или feature flag. Пропускайте сырые email, tokens, card data и полные SQL-строки. Если handler нужно что-то залогировать или traced-ить, он должен брать logger и данные span из context, а не собирать их заново.
Один запрос, три сигнала
Сначала используйте Prometheus client Go для небольшого набора метрик. Записывайте:
- counter для общего числа запросов
- histogram для latency запроса
- counter для ошибок
Добавляйте labels по method, route и классу статуса вроде 2xx или 5xx. Не используйте labels по полному URL, user ID или request ID. Такие labels взрывают cardinality и быстро делают дешёвые системы дорогими.
Запускайте root span в middleware с OpenTelemetry Go. Внутри handler передавайте тот же context в код базы данных и в любой исходящий HTTP client. Child spans должны появляться автоматически, если ваш DB driver и HTTP transport используют этот context. Если нужно решить, куда тратить время в первую очередь, сначала trace-ьте обращения к базе и внешним API. Именно там обычно и прячется большая часть latency.
Structured logging Go работает лучше всего, когда каждая строка лога содержит один и тот же request ID и route. Тогда одна строка с ошибкой приведёт вас к trace, а trace объяснит медленный всплеск метрики. Этого уже достаточно, чтобы отладить многие production-проблемы без обёрток для каждого пакета.
Проверьте неприятный сценарий
Прогоните один заведомо падающий запрос от начала до конца, прежде чем считать настройку завершённой. Хороший тест — signup-запрос, где создание пользователя проходит успешно, но payment-вызов зависает по таймауту.
Проверьте пять вещей:
- ответ возвращает правильный статус
- логи используют один request ID
- гистограмма latency записывает медленный запрос
- счётчик ошибок увеличивается один раз
- trace показывает spans для handler, базы данных и исходящего HTTP
Если одного из этих элементов нет, исправьте путь сразу. Небольшая команда может жить с маленьким дашбордом. Но она не может жить без контекста, когда в production случается сбой.
Держите данные чистыми и недорогими
Плохая observability быстро становится дорогой. Большинство команд сначала не упираются в инструменты. Они тонут в шумных labels, гигантских логах и трассах, которые все выглядят одинаково.
Начните с labels. В OpenTelemetry Go и Prometheus client Go каждое дополнительное значение label создаёт больше time series. Сырые user ID, email, UUID или полные тексты ошибок могут за день взорвать хранилище. Используйте маленькие фиксированные наборы вроде route=/signup, plan=pro, region=eu, result=ok.
Если значение может расти без ограничений, не кладите его в labels метрик. Положите его в лог или атрибут trace, и даже там держите его коротким.
С логами нужна та же дисциплина. Structured logging Go работает хорошо, когда каждое поле отвечает на реальный вопрос. Логируйте факты вроде account_id, order_id, retry_count, status_code и имени provider. Не сваливайте в каждую строку целые request bodies, tokens или большие payload от третьей стороны.
Такая привычка сразу создаёт две проблемы: более высокие счета и более сложный поиск. Пятирочная запись лога с понятными полями обычно лучше, чем blob на 500 KB, который никто не хочет читать.
Простое разделение
Используйте каждый сигнал для своей задачи:
- Метрики показывают тренды и срабатывают на изменения
- Логи фиксируют факты о событии
- Трассы показывают путь одного запроса
- IDs связывают всё это вместе
С trace нужно обращаться особенно аккуратно, потому что они быстро засоряются. Активный, здоровый трафик семплируйте жёстко. Редкие ошибки, медленные запросы и новые пути кода оставляйте на полной скорости, пока не доверяете им полностью. Если checkout ломается один раз на 2000 запросов, этот trace важнее, чем очередная обычная проверка здоровья.
Удаляйте дублирующиеся поля как можно раньше. Если middleware уже логирует request_id, route, method и status, handler не должен заново писать то же самое под новыми именами. Выберите одно имя поля и придерживайтесь его. user_id и userid не должны существовать одновременно.
Хорошие библиотеки observability для Go сильно упрощают жизнь, но даже у хороших defaults всё равно должен быть человеческий выбор. Signup-flow часто достаточно одного counter по route и result, одной trace для запроса и нескольких логов вокруг payment-шагa. Этого хватает, чтобы увидеть настоящую проблему, не платя за хранение всего мира.
Реальный пример: signup и billing
Signup-flow достаточно простой, чтобы его понять, и при этом достаточно насыщенный, чтобы показать, почему логи, метрики и трассы лучше работают вместе. Один запрос может затронуть приложение, базу данных, email-сервис и payment-provider меньше чем за пять секунд.
Запускайте одну trace в момент, когда пользователь отправляет форму. Оставляйте child spans для create_account, send_email и create_subscription, чтобы видеть полный путь и время, которое ушло на каждый шаг.
Метрики делают эту trace полезной на масштабе. Записывайте latency для каждого шага с labels вроде step=create_account, step=send_email и step=create_subscription. Через день трафика вы сможете быстро отвечать на простые вопросы: какой шаг чаще всего медленный и какой чаще остальных ломается?
Логи дают недостающую деталь, когда ломается один запрос. Для каждой ошибки записывайте одни и те же стабильные поля: order_id, tenant_id, trace_id и понятный error_reason. Если payment-вызов не сработал, потому что провайдер вернул timeout, так и скажите. Если код отклонил запрос, потому что нет плана, скажите именно это.
Реалистичный плохой день выглядит так: signup ощущается медленным, но только для платных планов. Trace показывает, что работа с базой завершается за 40 мс, вызов email — за 120 мс, а span оплаты занимает 3,8 секунды. Метрика latency для create_subscription подскакивает с обычных 250 мс до более чем 3 секунд на p95. Логи для тех же запросов содержат один и тот же tenant_id и показывают error_reason="payment timeout after retry".
Теперь вы знаете, где искать, без догадок. Вам не нужны пять обёрток или собственная модель событий. OpenTelemetry Go может передавать данные trace и span, Prometheus client Go — отслеживать latency шагов и количество сбоев, а structured logging Go — делать историю запроса читаемой.
Ещё одно правило помогает очень сильно: логируйте бизнес-ID, а не частные данные. order_id и tenant_id помогают связывать сигналы. Сырые данные карты, полные email-письма и расплывчатые сообщения вроде «что-то пошло не так» — нет.
Ошибки, которые съедают неделю
Больше всего времени обычно уходит на лишний код, а не на нехватку инструментов. Небольшому Go-сервису чаще всего нужен один logger, один metrics package и одна tracing-настройка. Неделя исчезает, когда команды добавляют слои, которые им не нужны.
Команды часто тратят дни на обёртки вокруг каждого вызова лога и каждой записи метрики. Сначала это кажется аккуратным, но такие обёртки скрывают поля, усложняют копирование примеров и добавляют ещё один пакет для поддержки. Начните с API библиотеки как есть. Добавьте небольшой helper только для повторяющихся полей вроде имени сервиса, окружения и component.
Иной тип потерь создают названия метрик. Люди придумывают целую систему именования до того, как вообще поймут, какие вопросы будут задавать в production. Потом у них оказываются двадцать counter-метрик и ни одного ясного ответа на простые вопросы вроде
Быстрые проверки перед релизом
Перед тем как выпускать, прогоните несколько проверок на одном реальном пути запроса, а не на моках. Хорошие библиотеки observability для Go должны делать это скучным. Если всё ещё нужны собственные скрипты, лишние обёртки или слишком много дашбордов, настройка слишком тяжёлая.
Начните с обычного запроса. Он должен каждый раз писать структурированный log, даже когда ничего не ломается. Одной строки достаточно, если там есть базовые поля: method, route, status, duration_ms, service и trace_id. Если один сервис пишет userId, а другой user_id, исправьте это сразу. Небольшой дрейф в названиях быстро превращает простую отладку в угадайку.
Основной дашборд должен отвечать на три вопроса на одном экране: сколько у вас трафика, сколько запросов падает и как долго выполняются запросы. Request rate, количество ошибок и p95 latency обычно закрывают эту задачу. Когда вы отправляете тестовый трафик, все три показателя должны двигаться логично.
Перед релизом используйте короткий чеклист:
- Отправьте успешный запрос и убедитесь, что он создаёт структурированную строку лога.
- Откройте дашборд и проверьте, что трафик, ошибки и latency видны вместе.
- Спровоцируйте одну ошибку 500 и убедитесь, что trace_id ведёт и к trace, и к соответствующей строке лога.
- Намеренно замедлите один вызов базы данных и проверьте, что задержка видна как отдельный span.
- Сравните поля логов между сервисами и оставьте везде одинаковые названия.
Проверка на 500 важнее, чем многие думают. Если метрика показывает всплеск ошибок, но вы не можете перейти к trace, а потом к логу, во время реального инцидента вы потеряете время. Цепочка должна ощущаться прямой: alert, trace, log, fix.
Проверка медленной базы ловит ещё один частый промах. Запрос может выглядеть медленным на верхнем уровне, но без span вокруг query вы всё равно не поймёте почему. Если вызов базы занимает 800 мс, trace должен показать это явно.
Если любая из этих проверок не проходит, не добавляйте сначала ещё код. Приведите в порядок названия полей, передавайте context до самого конца и держите путь от запроса к ответу коротким. Вы должны уметь спросить: «что сломалось у этого пользователя?» — и получить ответ за две минуты.
Что делать дальше
Когда стек уже работает, не поддавайтесь желанию добавить ещё десять дашбордов. Дайте ему поработать несколько дней и посмотрите на обычный трафик. Сначала нужен baseline: обычный объём запросов, нормальная доля ошибок, частые поля в логах и те spans, которые люди реально открывают во время отладки.
С алертами подождите, пока этот baseline не станет достаточно стабильным. Если создать алерты слишком рано, команда перестанет обращать на них внимание уже после первой шумной недели. Начните с небольшого набора: доля ошибок, latency на одном-двух user-facing путях, сбои exporter и здоровье сервиса.
Примерно через неделю реального трафика пересмотрите шум очень придирчиво. Удалите метрики, которые никто не читает. Уберите поля из логов, которые повторяют один и тот же факт в трёх местах. Если traces семплируются слишком часто, снизьте rate до того, как начнут расти расходы на хранение. Небольшие команды лучше работают с меньшим числом сигналов, которым доверяют.
Запишите правила именования простым языком. Одной короткой страницы достаточно. Держите её скучной и конкретной, чтобы каждый инженер использовал одни и те же слова.
- Поля логов вроде request_id, user_id, route и status_code
- Метрики вроде http_requests_total и signup_duration_seconds
- Имена span в trace вроде POST /signup и create invoice
Эта маленькая привычка экономит часы позже. Дашборды становятся понятнее, поиск работает лучше, а передача дел — менее хаотичной, когда названия остаются одинаковыми.
Если вы выбрали простые библиотеки observability для Go вместо собственной прослойки, сохраняйте тот же подход по мере роста приложения. OpenTelemetry Go, Prometheus client Go и один пакет structured logging Go обычно хватает надолго. Добавляйте обёртки только после того, как одна и та же проблема появится дважды. Многие команды слишком рано добавляют абстракции и потом поддерживают проводку, которая им никогда не была нужна.
Некоторые команды всё же хотят ещё одну пару глаз после первого запуска. Консультация Fractional CTO от Oleg Sotnikov может проверить стек, убрать лишнюю проводку и помочь сохранить логи, метрики и трассы полезными, не превращая их в отдельный побочный проект. Такой разбор особенно помогает, когда команда маленькая и каждый дополнительный час имеет цену.