Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 23 из 25 ~18 мин +50 XP

Логи и метрики: путь к наблюдаемости

Наблюдаемость go-сервиса начинается там, где зелёный график перестаёт отвечать на вопрос «почему»: метрика пятисотых выросла, а причины в ней нет. Разберём три столпа — логи, метрики, трейсы, корреляцию trace_id в полях zap, мост opentelemetry для логов и алерты, которые будят дежурного по делу, а не по каждой Error.

Содержание урока · 13 разделов

Пятница, вечер: график кода 500 ползёт вверх, дежурный открывает логи — и не может связать их с запросами: у записей нет идентификаторов трейса, у трейсов нет ссылок на логи, у метрик нет вообще ничего, кроме числа. Это антипример наблюдаемости: сигналы есть, а ответа на вопрос «что сломалось и с чем» они не дают. Наблюдаемость — свойство системы отвечать на новые вопросы без новых деплоев, и держится она на связке трёх сигналов. Логи — события с деталями: что именно произошло в этом запросе. Метрики — агрегаты: сколько, как часто, с какой латентностью. Трейсы — путь запроса через сервисы. Задача урока — соединить то, что уже умеет ваш zap из прошлых уроков, с остальными двумя сигналами и получить систему, где инцидент разбирается, а не переживается.

Три сигнала и их роли#

СигналОтвечает на вопросФормаЦена
Логичто именно произошлособытия с полямивысокая: каждая запись
Метрикисколько и как часточисла и гистограммынизкая: агрегаты
Трейсыгде запрос потерял времяспаны с длительностямисредняя: сэмплирование

Корреляция: trace_id в полях лога#

Ключ к связке — идентификатор, общий для всех сигналов. В трейсах это trace_id: hex-строка из 32 символов, которая живёт в span-контексте и путешествует между сервисами в заголовке traceparent. Логирующий middleware достаёт контекст пропагатором OpenTelemetry, берёт SpanContext и кладёт trace_id в логгер через With — дальше каждая запись запроса несёт его как обычное поле. Не путайте с request_id из урока про request id: тот генерируется вашим приложением для локальной хронологии, trace_id приходит из системы трассировки и соединяет сервисы между собой. В зрелом проекте живут оба: request_id отвечает за порядок внутри сервиса, trace_id — за путь между ними, и в записи они сосуществуют без конфликтов. Достаётся идентификатор строго из контекста: единственный источник правды — span, который принесла входящая цепочка, а не придуманное на месте значение.

Посредник с trace_id

GO
func TraceLogger(base *zap.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ctx := otel.GetTextMapPropagator().Extract(
                r.Context(), propagation.HeaderCarrier(r.Header),
            )
            sc := trace.SpanFromContext(ctx).SpanContext()
            logger := base
            if sc.IsValid() {
                logger = base.With(
                    zap.String("trace_id", sc.TraceID().String()),
                    zap.String("span_id", sc.SpanID().String()),
                )
            }
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Пропагатор читает traceparent из заголовков и вписывает span-контекст в ctx; SpanFromContext достаёт его обратно. Пока трейса нет — заголовок потерян или трассировка выключена, — IsValid отсекает нулевой SpanContext, и записи уходят без trace_id вместо пустышек. Поля прилипают к логгеру той же механикой With, что и у request_id.

Проверка: откуда trace_id

Откуда middleware берёт trace_id входящего запроса?

Что запишется без трейса?

Прочитайте код и выберите, что он выведет:

sc := trace.SpanFromContext(context.Background()).SpanContext()
fmt.Println(sc.TraceID().String())
fmt.Println(sc.IsValid())

Что будет, если клиент подделал traceparent#

Заголовок приходит от клиента, и пропагатор примет любой валидный по форме trace_id: не совпал с форматом — извлечение вернёт пустой контекст и сработает проверка IsValid; совпал — поле уйдёт в лог как есть. Это не уязвимость, а договорённость: trace_id — данные, а не доверенное значение, и фильтровать записи по нему безопасно только внутри одного инцидента. Практические следствия: маскировать поле не нужно — hex без секретов, но строить по нему авторизацию нельзя; длина фиксирована, раздуть запись им не получится; сервис, начинающий трейс первым, обязан генерировать новый идентификатор, а не принимать нулевой от клиента — IsValid и это ловит.

OpenTelemetry: лог-мост обзор#

Логи в OpenTelemetry — третий сигнал рядом с трейсами и метриками: у SDK есть LoggerProvider, у API — запись с телом, уровнем и атрибутами. Прикладной код при этом не переписывается: есть мосты, которые берут записи у существующего логгера. Для zap такой мост — otelzap из contrib: он оборачивает ядро и пересылает записи в LoggerProvider, дальше их собирает коллектор и маршрутизирует в хранилище. Обратная дорога тоже есть: SDK умеет приписывать трейсовый контекст событиям лога, поэтому поле trace_id может прийти как от вашего With, так и от моста. Здесь обзор сознательно короткий: связка «SDK, коллектор, экспортёр» — отдельная дисциплина со своей документацией, и для первого знакомства достаточно локального Go, коллектора рядом и пары записей в консоль.

Мост otelzap в два вызова

GO
import (
    "go.opentelemetry.io/contrib/bridges/otelzap"
    "go.opentelemetry.io/otel/log/global"
)

provider := global.GetLoggerProvider()
core := otelzap.NewCore("tasks", otelzap.WithLoggerProvider(provider))
logger := zap.New(core)

logger.Info("order accepted", zap.String("id", "a-1"))

NewCore строит zapcore.Core, который отправляет записи в LoggerProvider: имя instrumentation-библиотеки — tasks, поля становятся атрибутами. global.GetLoggerProvider возвращает провайдера, установленного SDK; без него мост тихо ставит no-op, и цена вызова стремится к нулю. В проде такое ядро обычно комбинируют через NewTee с файловым выводом.

Sampler: цена наблюдаемости#

Полный лог каждого запроса — роскошь, которая на гигантском трафике превращается в отдельную статью расходов, и трейсы решают ту же задачу сэмплированием: head-сэмплер режет поток на входе по вероятности, tail-сэмплер в коллекторе отбирает интересные — с ошибками и большими латентностями. У zap своя механика прореживания: NewSamplerWithOptions пропускает первые N однотипных записей за тик и дальше одну на тик — именно она включена в production-пресет. Гранулярность у сэмплирования и трейсов разная: логи прореживаются по сообщению, трейсы — по запросу целиком; идеи родственные, слои разные, и одно не заменяет другое. Дропы стоит видеть: хук SamplerHook отдаёт метрику отброшенных записей, и её график — честная цена прореживания, а не абстрактное «что-то потерялось».

Проверка: кто будит дежурного

Кто обычно триггерит алерт, а кто подтверждает причину?

Уровни как контракт для алертов#

Связка уровней с алертами — договорённость, заключённая заранее, а не спонтанно в три ночи. Error — сигнал аварии: на него смотрит правило бёрста. Warn — подозрительное поведение: ретраи, рой 404, медленные запросы у порога; на Warn стреляет только накопительный сигнал, и канал — чат, а не пейджер. Fatal — процесс умирает, и алерт ставят на отсутствие сигналов: метрику живости, а не лог, потому что последняя запись Fatal до тёплой системы сбора может не доехать вовсе. Info — не сигнал, а материал: по нему считают трафик и долю ошибок. У такого контракта есть побочный эффект: ревью уровня записи становится ревью её цены в алертах, и вечные споры «Info или Warn» заканчиваются быстрее, потому что уровень теперь стоит денег.

Алерты по логам: Error-бёрсты#

Бывают события, у которых нет метрики: редкая ошибка интеграции, аномальный код ответа, сообщение уровня Fatal. Для них работают алерты по потоку логов: счётчик Error за окно, порог, окно тишины. Правило реакции простое: одиночная ошибка — не инцидент, бёрст — инцидент, и порог подбирается по истории, а не по желанию «наконец-то узнавать всё». Тонкая настройка — дедупликация: пятьсот одинаковых записей из одного цикла — это один инцидент, и алерт должен стрелять один раз; отпечаток собирают из имени сервиса, сообщения и ключевых полей. Полный конвейер — централизованное хранилище, правила поверх него и канал с эскалацией — тема следующего урока, а здесь важно направление: логи не только читают глазами, на них ещё и сигналят.

Правило бёрста как правило поверх логов

GO
# Loki ruler: бёрст ошибок сервиса задач
groups:
  - name: tasks-errors
    rules:
      - alert: TaskErrorBurst
        expr: sum(increase({service="tasks", level="error"}[5m])) > 20
        for: 2m
        labels:
          severity: page
        annotations:
          summary: "20+ ошибок за 5 минут в tasks"

Правило считает записи уровня error за пять минут и уходит в пейджинг только после двух минут непрерывного превышения: одиночные всплески гасятся фазой for. Лог-запрос берёт поля, которые zap записал в JSON, — service и level. Порог 20 и окно 5m — стартовые значения: их правят по истории инцидентов, а не по вкусу. Если метки level у записей нет — правило молчит: sum по пустому набору вернёт ноль, и алерт не стрельнёт вовсе. Отсюда привычка сверять селектор с реальными полями JSON: пробный запрос в UI Loki занимает минуту и экономит вечер тишины.

SLO-мышление: зачем всё это#

Наблюдаемость имеет смысл, когда есть цель. SLO фиксирует обещание уровня сервиса: например, 99,9% запросов списков задач отвечают быстрее 300 миллисекунд за месяц. Остаток — error budget, бюджет ошибок, который можно потратить на эксперименты и релизы. Метрики следят за бюджетом и триггерят разговор, когда он тает; логи — материал разбора, куда именно ушла доля. Из этого растут решения всего курса: Error — для аварий, Warn — для подозрительного, тишину в штатном сценарии проверяют тесты, сэмплинг экономит бюджет хранения. Алерты на каждую запись Error ломают модель: доля ошибок — дело бюджета, а не каждой строчки. Дежурный просыпается на тренд, а не на единичный сбой, и тем самым сохраняет способность вообще просыпаться на настоящие аварии. Отсюда и отказ алертить каждую строчку: строчка — материал, тренд — сигнал.

Минимальный стек для старта#

  • Prometheus: метрики и алерт-правила, дешёвые счётчики рядом с логами.
  • OTel Collector: приём трейсов и лог-мостов, маршрутизация в хранилища.
  • Loki или ELK: централизованный поиск по полям JSON.
  • Дашборд латентности и ошибок рядом с топом сообщений логов.
  • Канал алертов с эскалацией: пейджинг на бёрсты, чат на остальное.
  • График SamplerHook: доля дропнутых записей — цена сэмплинга на одном экране с трафиком.

Соберите извлечение идентификатора

Восстановите посредник: извлечение, проверка, поле.

ctx := otel.GetTextMapPropagator().Extract( r.Context(), propagation.HeaderCarrier(r.Header), ) sc := trace.(ctx).SpanContext() if sc.() { logger = base.With(zap.String("trace_id", sc.().String())) }

Соберите связку запрос — лог

Расставьте шаги посредника по порядку.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: наблюдаемость

Термин

Три столпа

нажмите, чтобы перевернуть
Определение

Логи — события с полями, метрики — агрегаты, трейсы — путь запроса; связка отвечает на «что, сколько, где».

1 / 5

Что дальше по курсу#

Хранение, поиск и полный конвейер алертов — тема урока про эксплуатацию: там Loki и ClickHouse разбираются по одной строке и появляются retention и 152-ФЗ. Прореживание потока в цифрах — урок про сэмплирование, разделение request_id и trace_id — урок про request id. Карта курса — на странице курса zap.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Наблюдаемость — способность отвечать на новые вопросы без новых деплоев.
  • Логи говорят «что», метрики — «сколько», трейсы — «где по пути».
  • trace_id приходит из span-контекста через пропагатор и уходит в поля With.
  • Без IsValid в записях появляются нулевые идентификаторы-пустышки.
  • Мост otelzap отправляет записи zap в LoggerProvider, не меняя энкодер.
  • Метрики триггерят, логи подтверждают: алерт — на бёрст, не на строчку.
  • SLO переводит уровни и алерты из вкусовщины в бюджет ошибок.

Домашнее задание

Добавьте в свой сервис из middleware-уроков посредник TraceLogger: пропагатор, IsValid, поле trace_id. Проверьте тестом с observer, что записи запроса с заголовком traceparent несут поле, а без него — не несут ничего. Затем посчитайте за день долю Error-записей и напишите правило бёрста с порогом по этой доле. После этого включите на логгере сэмплер и повторите бёрст: сравните число записей в хранилище с числом вызовов в коде — разница и есть цена прореживания.

Личные заметкиПусто
Хранятся только в вашем браузере (Zap · Логи и метрики: путь к наблюдаемости)

Вопросы и ответы по уроку

Нужен ли полноценный OTel SDK, чтобы просто писать trace_id в логи?

Нет: пропагатор и trace-пакет работают и без экспортёров — контекст извлекается, IsValid проверяется, поле пишется. SDK нужен, когда трейсы должны куда-то улетать; логи с trace_id полезны и без них.

Что приоритетнее внедрить первым — метрики или трейсы?

Метрики: они дёшевы, сразу дают графики и алерты. Трейсы дороже и нужнее при росте числа сервисов: один сервис объясняется логами, цепочка — трейсом.

Можно ли алертить по Warn-бёрстам?

Можно и иногда нужно — например, по ретраям шлюза. Но Warn шумнее Error, поэтому порог выше, окно длиннее, а канал — чат, не пейджер.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Colly25 уроков~9 чпрактика25 уроков уже доступны

Colly: парсинг и веб-скрапинг на Go

Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.

  • Писать скраперы на Colly от первого Visit до обхода целого сайта
  • Вынимать данные через CSS-селекторы и складывать в структуры Go
  • Настраивать темп: задержки, параллелизм, лимиты на домен
  • Работать с капризными сайтами: заголовки, cookies, прокси