Логи и мониторинг

Учимся видеть, что происходит в проде: пишем логи так, чтобы их можно было искать, строим метрики, настраиваем алерты и разбираем инцидент по шагам.

Cloud Logging собирает и хранит логи всех сервисов проекта, Cloud Monitoring — числовые метрики, графики и алерты. Вместе они отвечают на два вопроса: «всё ли хорошо прямо сейчас?» и «что именно сломалось полчаса назад?».

Зачем это на практике

Проект без наблюдаемости живёт по такому сценарию: пользователь пишет в поддержку «не могу оплатить», поддержка пишет разработчику, разработчик заходит в консоль, листает простыню логов, ничего не находит, просит «а можете ещё раз попробовать и сказать точное время?». Через два часа выясняется, что упал сторонний платёжный шлюз.

Проект с наблюдаемостью живёт иначе: в 14:02 в Telegram падает алерт «доля 5xx на shop-api выше 1%», разработчик открывает Log Explorer, за 30 секунд фильтрует ошибки, видит card_declined и таймауты к внешнему API, и в 14:10 уже знает причину. Разница не в уме разработчика — в подготовленной заранее инфраструктуре.

Про деньги: Cloud Logging бесплатен до заметного объёма (порядка 50 GiB приёма логов на проект в месяц), дальше берёт плату за каждый гигабайт. Стандартные метрики GCP (запросы, задержки, CPU) бесплатны, алерты тоже. Пугать должен именно объём логов — о нём в конце урока.

Пишите структурные логи, а не текст

Cloud Run, GKE, Cloud Functions забирают в Logging всё, что приложение печатает в stdout. Если печатать обычный текст, вы получите поле textPayload и сможете искать по нему разве что подстрокой. Если печатать JSON — Logging разберёт его в jsonPayload, и каждое поле станет фильтруемым.

Есть два зарезервированных ключа: severity (уровень: DEBUG, INFO, WARNING, ERROR, CRITICAL) и message (текст, который показывается в списке). Всё остальное — ваши поля.

import json


def log(severity, message, **fields):
    entry = {
        "severity": severity,
        "message": message,
        "time": "2026-07-14T10:00:00Z",
        **fields,
    }
    print(json.dumps(entry, ensure_ascii=False))


log("INFO", "заказ создан", order_id=1042, user_id=7)
log("ERROR", "оплата не прошла", order_id=1042, code="card_declined")

Результат:

{"severity": "INFO", "message": "заказ создан", "time": "2026-07-14T10:00:00Z", "order_id": 1042, "user_id": 7}
{"severity": "ERROR", "message": "оплата не прошла", "time": "2026-07-14T10:00:00Z", "order_id": 1042, "code": "card_declined"}

Одна строка = одна JSON-запись. Теперь в Log Explorer можно спросить «покажи все события по заказу 1042» — и получить ответ, а не 40 минут чтения простыни. В боевом коде вместо самописной функции берут библиотеку структурного логирования и обязательно кладут в запись идентификатор запроса (trace), чтобы склеивать события одного пользовательского обращения.

Log Explorer: язык запросов

Поиск по логам — это не «строка поиска», а маленький язык фильтров. Условия соединяются через AND, OR, NOT.

resource.type="cloud_run_revision"
resource.labels.service_name="shop-api"
severity>=ERROR
jsonPayload.order_id=1042
timestamp>="2026-07-14T13:50:00Z"

Здесь severity>=ERROR означает «ERROR и всё, что серьёзнее» — уровни сравнимы между собой. Полнотекстовый поиск тоже есть: просто напишите слово в кавычках, например "card_declined", — но по конкретному полю фильтр и быстрее, и точнее.

То же самое из терминала — удобно, когда нужно быстро посмотреть последние ошибки, не открывая браузер:

gcloud logging read \
  'resource.type="cloud_run_revision" AND severity>=ERROR' \
  --freshness=1h --limit=20 --format=json

# живой хвост логов сервиса
gcloud beta run services logs tail shop-api --region=europe-west1

Метрики и алерты

Логи отвечают на вопрос «что случилось». Метрики — на вопрос «сколько и как часто». Cloud Monitoring уже собирает за вас десятки метрик без единой строки кода: run.googleapis.com/request_count, request_latencies, использование CPU и памяти.

Свою метрику можно вырастить прямо из логов — это называется log-based metric: Logging считает, сколько записей подошло под фильтр, и превращает счёт в числовой ряд.

gcloud logging metrics create payment_failures \
  --description="Неудачные оплаты" \
  --log-filter='resource.type="cloud_run_revision" AND jsonPayload.code="card_declined"'

Дальше на метрику вешают alerting policy — правило «если значение выше порога дольше N минут, разбуди человека». Политику можно кликнуть в UI, а можно описать файлом и держать в git рядом с Terraform:

displayName: "Много 5xx на shop-api"
combiner: OR
conditions:
  - displayName: "5xx чаще 5 раз в минуту в течение 5 минут"
    conditionThreshold:
      filter: 'metric.type="run.googleapis.com/request_count" AND resource.type="cloud_run_revision" AND metric.labels.response_code_class="5xx"'
      comparison: COMPARISON_GT
      thresholdValue: 5
      duration: 300s
      aggregations:
        - alignmentPeriod: 60s
          perSeriesAligner: ALIGN_RATE
notificationChannels:
  - projects/my-shop/notificationChannels/1234567890

Обратите внимание на duration: 300s. Без него алерт сработает на единственный случайный 5xx в три часа ночи. Порог «держится N минут» отсекает шум — а шумный алерт быстро перестают читать, и это опаснее, чем отсутствие алерта.

Отдельно стоит завести uptime check: Google раз в минуту дёргает ваш URL из нескольких регионов мира. Он ловит то, чего не видят внутренние метрики, — например, протухший TLS-сертификат или сломанную балансировку.

Сколько ошибок мы вообще имеем право допустить

Чтобы порог не был взят с потолка, команды считают бюджет ошибок: если вы обещали доступность 99,9%, то 0,1% запросов имеют право упасть — и это конкретное число.

total_requests = 1_000_000   # запросов за месяц
failed = 4_200               # из них ответили 5xx
slo = 0.999                  # обещанная доступность

budget = total_requests * (1 - slo)
spent = failed / budget * 100

print(f"Бюджет ошибок за месяц: {budget:.0f} запросов")
print(f"Фактически ошибок:      {failed}")
print(f"Бюджет израсходован на  {spent:.1f}%")

Результат:

Бюджет ошибок за месяц: 1000 запросов
Фактически ошибок:      4200
Бюджет израсходован на  420.0%

Бюджет перерасходован вчетверо — значит, следующий спринт уходит не на фичи, а на надёжность. В этом и смысл цифры: она превращает спор «релизить или чинить» в арифметику.

Как это работает: разбор инцидента по шагам

Сложился реальный сценарий: 14:02 пришёл алерт по 5xx. Что делает дежурный.

  1. Ограничить время. В Log Explorer выставить окно 13:50–14:10. Не «последний час» — узкое окно вокруг события, иначе утонете в записях.
  2. Отсечь лишнее. Фильтр по сервису и severity>=ERROR. Из тысяч строк остаются десятки.
  3. Найти начало. Отсортировать по возрастанию времени и посмотреть первую ошибку. Последняя ошибка — обычно следствие; интересна первая.
  4. Проверить, что менялось. Cloud Run хранит ревизии: если в 13:58 приехал деплой, а в 14:00 полезли 5xx, вы уже нашли причину. Логи деплоя — там же, в Logging.
  5. Склеить историю одного запроса. По полю trace (или своему request_id) достать все записи одного обращения — от входа до падения.
  6. Остановить кровь. Сначала откат, потом расследование. Cloud Run позволяет перевести трафик на предыдущую ревизию одной командой.
# посмотреть ревизии и увидеть, какая приехала перед инцидентом
gcloud run revisions list --service=shop-api --region=europe-west1

# вернуть весь трафик на предыдущую ревизию
gcloud run services update-traffic shop-api \
  --region=europe-west1 \
  --to-revisions=shop-api-00041-xyz=100

Под капотом Logging — это не «файл с логами», а индексированное хранилище (log buckets) с маршрутизацией. Каждая запись проходит через sinks: правила «эти логи в основное хранилище, эти в BigQuery для аналитики, эти выбросить». Именно sinks дают контроль над деньгами.

Частые ошибки

  • Логировать всё подряд на DEBUG в проде. Сервис на 500 RPS с подробным логом каждого запроса легко даёт десятки гигабайт в месяц — и бесплатный лимит кончается, а счёт растёт. Лечится exclusion-фильтром в sink: например, выбрасывать успешные health-check'и, которые не несут информации.
  • Секреты в логах. Печать всего тела запроса рано или поздно занесёт в Logging пароль, токен или номер карты. Логи хранятся, индексируются и доступны всей команде — считайте их публичными.
  • Алерт без duration. Срабатывает на любой чих, команда привыкает игнорировать уведомления, и настоящая авария проходит незамеченной.
  • Алерт есть, канала уведомления нет. Политика создана, но notificationChannels пуст — инцидент честно фиксируется в консоли, куда никто не смотрит.
  • Текстовые логи вместо JSON. Через полгода вы не сможете отфильтровать события по order_id, потому что его нет как поля.
  • Мониторить только CPU. Пользователю всё равно, какая у вас загрузка процессора. Смотрите на «золотые сигналы»: доля ошибок, задержка (обязательно p95/p99, не среднее), трафик, насыщение ресурсов.
  • Расследовать вместо отката. Пока вы ищете причину, пользователи получают 500-е. Сначала верните рабочую ревизию, потом разбирайтесь.

Итоги

  • Пишите в stdout JSON-строки с полями severity и message — Logging сам разложит их в jsonPayload, и логи станут искомыми.
  • Log Explorer — язык фильтров (resource.type, severity>=ERROR, свои поля), а не строка поиска. То же доступно через gcloud logging read.
  • Log-based metric превращает записи логов в числовой ряд, на который можно повесить алерт.
  • У алерта обязательны порог, duration и канал уведомления. Шумный алерт хуже, чем никакого.
  • Инцидент: сузить окно → отфильтровать ошибки → найти первую → проверить последний деплой → откатить трафик → расследовать.
  • Главная денежная утечка в наблюдаемости — объём логов. Exclusion-фильтры и разумный уровень логирования в проде экономят реальные деньги.
Проверьте себя
1. Почему приложение в Cloud Run стоит логировать JSON-строками, а не обычным текстом?
AJSON занимает меньше места в хранилище
BCloud Logging разберёт JSON в jsonPayload, и каждое поле станет фильтруемым в запросах
CТекстовые логи Cloud Run вообще не собирает
DJSON-логи бесплатны, текстовые оплачиваются
2. Что даёт параметр duration в условии алерта Cloud Monitoring?
AКак долго хранится инцидент в истории
BСколько времени даётся команде на реакцию
CАлерт сработает, только если условие держится указанное время — это отсекает случайные единичные всплески
DПериод, за который считается стоимость метрики