Прямой ответ
Структурированный лог — это запись в формате ключ-значение (на практике JSON), а не «простыня» текста: машина её парсит, человек фильтрует по полям за секунды. Минимальная рабочая схема для любого стека: одна строка JSON на событие с обязательными полями timestamp (UTC, ISO 8601), level, logger, message плюс контекст (request_id, user_id, длительности); уровни используются по назначению, а не «на всякий случай»; приложение пишет логи в stdout, а ротацией и хранением занимается платформа (Docker, systemd, облачный агент) — принцип 12-factor. Локально такие логи читаются через jq красивее любых GUI.
Почему текст проигрывает структуре
Классическая строка [2026-10-20 14:03:22] ERROR: Failed to process order 42 for user 7: timeout кажется читаемой — пока вам не нужно ответить на вопросы «сколько таймаутов за час», «все ли ошибки касаются одного пользователя», «какова медианная длительность обработки». По текстовым логам каждый такой вопрос — регулярное выражение и боль; по JSON — один фильтр: jq 'select(.level=="ERROR" and .err=="timeout")'. Дополнительный выигрыш — стабильность: текстовые сообщения меняются от релиза к релизу и ломают чужие парсеры, набор полей стабилен. Переход в типовом проекте — замена форматтера логгера (в Python — python-json-logger/structlog, в Node — pino, в JVM — logstash-encoder, в Go — zerolog/zap): код лог-вызовов почти не меняется.
Уровни: контракт, а не вкусовщина
Расплывчатость уровней — главная причина «логи невозможно читать». Рабочий контракт из пяти пунктов: ERROR — действие не выполнено, требуется внимание (пользователь не получил результат); WARN — выполнено, но аномально (повторная попытка удалась, диск близок к лимиту); INFO — факты жизни системы (старт, завершение значимой операции, внешние вызовы с длительностью); DEBUG — детали для разработчика (параметры, промежуточные состояния); в проде обычно INFO, при инциденте уровень временно снижается. Два антипаттерна: логировать ожидаемые ситуации (валидация ввода, 404) на ERROR — тонет сигнал реальных аварий; и «WARNING на всё» — уровень перестаёт нести информацию.
Контекст запроса: request_id решает
Полезность логов кратна их связности. Паттерн, который стоит внедрить первым: генерировать request_id на входе (или принимать заголовок X-Request-ID от шлюза) и прокидывать во все записи этого запроса — через context-механизм языка (contextvars в Python, async context в Node, MDC в JVM). Тогда разбор инцидента — это jq 'select(.request_id=="abc123")' вместо склеивания строк глазами по таймстампам. Сюда же: не логируйте секреты и персональные данные (маскируйте поля), и помните, что лог с телом запроса может унести .env-значение — перекликается с правилами из статьи про секреты.
Ротация и хранение: зона ответственности платформы
Приложение в контейнере пишет в stdout/stderr и больше ни во что — 12-factor. Ротацию делает Docker-демон (опции max-size/max-file в logging-драйвере — выставьте их, иначе логи съедят диск: дефолты щедрые), systemd — journald со своими лимитами, прод — агент, отгружающий в централизованное хранилище. Писать из приложения в файл с самостоятельной ротацией уместно только вне контейнеров (десктоп, демон без супервизора). Локально в dev-стеке полезно настроить драйвер в compose-файле — паттерн из статьи про Compose-стек; анализ накопленных логов — в материале про grep/jq/awk.
Логи и метрики: границы
Логи отвечают на «что именно произошло», метрики — на «как много и как быстро». Дублировать их вредно в обе стороны: счётчик ошибок обязателен в метриках (алерт по логам — антипаттерн), а каждую метрику логировать строкой — засорение. Практическое правило: агрегируемое — в метрики (Prometheus из статьи про локальный мониторинг), единичные события с деталями — в логи. Связка по request_id даёт полную картину: метрика показала всплеск, лог объяснил причину.
Практика внедрения в существующем проекте
Переход на структурированные логи в работающем проекте делается без «большого взрыва». Первый шаг — замена форматтера на уровне конфигурации логгера: код вызовов остаётся прежним, а вывод становится JSON; для Python это десяток строк в настройке logging, для Node — смена транспорта. Второй шаг — стандартизация полей: договоритесь о словаре (request_id, user_id, duration_ms, err) и зафиксируйте его в общем модуле-хелпере, чтобы каждая команда не изобретала свои имена. Третий — постепенное обогащение: новые фичи логируются по стандарту сразу, старые точки переводятся при касании кода.
Показатель успеха перехода конкретный: время ответа на вопрос «что случилось с запросом X» падает с минут ручного поиска по тексту до секунд одного фильтра. Если через месяц после перехода вы всё ещё читаете логи глазами построчно — стандарт не прижился: проверьте, что поля действительно проставляются (типичная дыра — контекст запроса забывают прокинуть в фоновые задачи и обработчики очередей), и что локально логи читаются удобно (человекочитаемый режим в dev).
FAQ
JSON в консоли нечитаем — как быть в разработке? Двойной форматтер: человекочитаемый цветной вывод локально (по переменной окружения), JSON — в контейнере/проде. Так делается одной настройкой во всех популярных лог-библиотеках.
Какой размер строки лога допустим? Держитесь сотен байт; гигантские тела запросов/ответов — в debug-уровень с обрезкой. Лог-системы обрезают длинные строки молча, и вы теряете конец записи.
Нужен ли trace-level? В большинстве проектов пяти уровней достаточно; trace появляется вместе с распределённой трассировкой (OpenTelemetry), где это отдельная сущность, а не уровень логгера.
Что логировать при внешних вызовах? Факт вызова, цель, длительность, код ответа и request_id — без тел и заголовков по умолчанию. Тела — в debug и с маскированием: это и диагностика, и защита данных.
Менять ли формат логов у легаси-сервиса целиком? Нет: начните с новых модулей и точек, которые трогаете. Половинчатый формат (часть JSON, часть текст) хуже любого из двух, поэтому переводите сервис целиком, но одним осознанным релизом, а не бесконечно.
Как быть с логами библиотек, которые пишут текстом? Перехватите их вывод штатным механизмом логгера вашего языка (в Python — общий корневой logger перехватывает большинство библиотек) и прогоняйте через свой форматтер; для особо упрямых остаётся текстовый вывод под отдельным именем логгера.
Источники: документация logging Python, 12-factor logs, проект bunyan (ссылки в шапке).