1. Профильная стратегия логирования
Если воспринимать логирование как «печать в консоль», то стратегия кажется чем-то из мира корпоративных ритуалов. Но как только сервис становится чуть живее (профили, конфиги, разные режимы запуска, разные люди, которые читают логи), выясняется: без стратегии лог превращается в шум, а шум — это разновидность технического долга с очень противным характером.
Logging-strategy — это договорённость о том, как мы используем логи в проекте: какие уровни считаем нормой, какие пакеты «шумные», где мы хотим подробности, какие поля считаем обязательным контекстом, и как делаем временную диагностику. Это не только YAML. Это ещё и стиль сообщений, и дисциплина вокруг MDC, и правило «не логируем секреты», и понимание, что DEBUG — не стиль жизни.
Почему стратегия должна быть профильной? Потому что у нас реально разные режимы существования сервиса. В local мы хотим быстро понимать, что происходит, и иногда лезть в детали своего кода. В dev обычно важна более стабильная форма (часто structured), потому что логи могут смотреть не только вы, но и коллега/QA/сосед по стенду. В prod (даже если «прод» у нас учебный) важнее предсказуемость, сдержанность и отсутствие лишнего шума: там лишний лог — это не просто строка, а ресурс и риск.
При этом очень важно удержать мысль: профильная стратегия — это не «в каждом профиле новый язык сообщений». Сообщения не должны становиться шизофреническими: сегодня «Catalog started, yay!», завтра «КАТАЛОГ ЗАПУЩЕН!!!», послезавтра «boot.sequence.completed». Меняется детализация и форма, но не смысл и не культура.
2. Профили local/dev/prod
Когда уровни, формат вывода, MDC и временная диагностика уже понятны по отдельности, профили легко начать воспринимать как три разных приложения. И дальше начинается конфигурационный сериал: в одном профиле одно, в другом другое, а в третьем забыли половину настроек. Нам нужно избежать именно этого.
Профили в нашем курсе — это три режима эксплуатации одного и того же catalog-service. Код один и тот же, доменная модель одна, endpoint’ы те же. Профиль меняет только то, что действительно зависит от среды: детализацию логов, формат вывода и жёсткость диагностических настроек.
Удобно держать в голове короткую модель:
| Профиль | Главная цель логов | Как мы читаем логи | Что чаще всего меняем |
|---|---|---|---|
| local | Быстрая отладка своего кода | глазами в консоли | DEBUG для своих пакетов, plain text |
| dev | Диагностика «на стенде» и стабильность | глазами + «по полям» | structured console (ECS/Logstash), точечный DEBUG |
| prod | Предсказуемость и минимальный шум | «по полям», осторожно | structured console, сдержанные уровни |
Эта таблица не означает, что structured logging запрещён в local. Он вполне может быть полезен. Но для обучения и для ежедневной «быстрой работы» plain text иногда банально удобнее: вы видите строку, вы видите смысл, вы не пытаетесь глазами читать JSON как художественную литературу.
И ещё один важный принцип: база должна быть общей, а профиль — это надстройка. То есть application.yaml задаёт «разумный дефолт», а profile-файлы делают аккуратные override’ы, а не переписывают всё заново.
3. Логгеры для уровней
Очень легко превратить настройку уровней логирования в игру «угадай антагониста»: то org.springframework шумит, то tomcat что-то бормочет, то внезапно логгер с загадочным именем o.s.b.w.e.t.TomcatWebServer заговорил. Чтобы не жить в этом хаосе, важно определить: какие логгеры для нас «свои», какие «чужие», а какие «нейтральные соседи по подъезду».
Для catalog-service мы обычно выделяем несколько зон:
Первая зона — наше приложение, то есть базовый пакет com.example.catalogservice (или тот, который вы используете в проекте). Это главный рычаг: если вы включите DEBUG на этот пакет в local, вы получите подробности по своему коду, не превращая логи Spring’а в водопад.
Вторая зона — фичевые пакеты, например com.example.catalogservice.catalog. Это удобно, когда вам нужно усилить диагностику только для каталога, но не для конфигурации, не для actuator-компонент, не для поддержки.
Третья зона — инфраструктурные пакеты внутри нашего кода, например com.example.catalogservice.config и com.example.catalogservice.actuator. Они часто ведут себя по-разному: конфиг обычно интересен только когда ломается (поэтому в prod можно поднять до WARN), actuator тоже не обязан шуметь постоянно.
Четвёртая зона — framework и контейнер: org.springframework, org.apache.catalina и прочие. Мы почти никогда не хотим дебажить Spring «по умолчанию» (особенно новичком), потому что это быстро превращается в «я открыл лог — и потерялся». Это делается точечно и временно, обычно через endpoint loggers, и только когда есть конкретная гипотеза.
Если зафиксировать это как простую схему, получится примерно так:
flowchart TD A["root"] --> B["com.example.catalogservice"] B --> C["...catalog"] B --> D["...config"] B --> E["...actuator"] A --> F["org.springframework"] A --> G["org.apache.* / tomcat"]
Смысл: root задаёт «погоду в доме», пакеты приложения могут быть детальнее, а Spring/Tomcat — обычно сдержаннее, пока мы не пошли в диагностику осознанно.
4. Уровни логирования в YAML и наследование
Настраивать уровни логирования через logging.level.* — это один из самых простых и полезных навыков в Spring Boot. При этом он регулярно ломает мозг новичку, потому что кажется: «я поставил DEBUG, а оно всё равно не печатает» или «я ничего не ставил, а оно внезапно печатает слишком много». Обычно это вопрос наследования уровней и того, где именно вы переопределили настройку.
В Spring Boot (и в большинстве нормальных logging-систем) действует логика: более конкретный логгер может переопределить более общий. Если root=INFO, а com.example.catalogservice=DEBUG, то для классов в этом пакете будет действовать DEBUG. Если вы дополнительно поставите com.example.catalogservice.config=WARN, то конфиг-пакет снова станет тише, даже если весь ваш код в DEBUG. Это очень удобная модель, потому что позволяет держать один общий фон и несколько «окошек» для деталей.
Начнём с базового application.yaml, где мы зададим общий baseline, одинаковый во всех профилях:
# src/main/resources/application.yaml
logging:
level:
root: INFO # Общий уровень «по умолчанию» для всего приложения
com.example.catalogservice: INFO # Базовый уровень для нашего кода (дальше профили делают override)
org.springframework: WARN # Приглушаем шум Spring, чтобы видеть сигнал от приложения
В этом кусочке есть важная мысль: мы не начинаем жизнь с DEBUG на root, потому что это почти всегда «тревожная кнопка». Мы также слегка приглушаем Spring до WARN, потому что в учебном проекте обычно важнее видеть наши события, а не подробный внутренний разговор фреймворка.
Теперь в local мы хотим добавить «подробности про наш код», не трогая всё остальное:
# src/main/resources/application-local.yaml
logging:
level:
com.example.catalogservice: DEBUG
И вот тут новичков часто спасает мысль: нам не нужно копировать root и Spring уровни ещё раз. Мы делаем только override. root: INFO остаётся из базового файла.
Для dev можно сделать более точечно: например, дебажить только каталог, но не всё приложение:
# src/main/resources/application-dev.yaml
logging:
level:
com.example.catalogservice: INFO
com.example.catalogservice.catalog: DEBUG
Да, здесь я специально вернул общий пакет на INFO. Это помогает избежать ситуации «в dev шумит всё подряд», а вам нужно только понять логику фильтрации каталога или загрузки данных.
А в prod обычно делают ещё строже и тише: например, конфиг-пакет держим на WARN, чтобы он не печатал лишнего, но если там что-то реально пошло не так — мы это увидим:
# src/main/resources/application-prod.yaml
logging:
level:
com.example.catalogservice: INFO
com.example.catalogservice.config: WARN
Обратите внимание: это не означает, что мы «не хотим знать про конфиг». Наоборот: мы хотим знать только важное. WARN в конфиге обычно означает «что-то потенциально неправильное», и это хороший сигнал для prod-режима.
5. Формат вывода: plain text и structured
Формат вывода мы уже отделили от самого log.info(...). Здесь осталось зафиксировать policy: в local приоритет у быстрого чтения глазами, а в dev и prod — у стабильных полей и machine-readable формы. Поэтому для catalog-service structured baseline уже выбран — ecs; GELF и Logstash нам полезны как форматы, которые нужно уметь узнавать, но проект между ними не прыгает без причины.
В local plain text часто удобнее просто потому, что вы читаете консоль в IDE и хотите быстро увидеть смысл строки. В dev и prod structured формат даёт более предсказуемую форму логов и делает поведение между средами похожим.
В Boot 4 structured console включается одной настройкой. В dev просто держим уже выбранный baseline:
# src/main/resources/application-dev.yaml
logging:
structured:
format:
console: ecs # Structured-логирование в консоль: поля удобнее парсить и сравнивать между средами
А в prod оставляем тот же ecs, чтобы не менять орфографию логов между средами:
# src/main/resources/application-prod.yaml
logging:
structured:
format:
console: ecs
Почему именно ECS остаётся baseline? Потому что он даёт достаточно «собранную» структуру, и в примерах он хорошо читался. Но если вам ближе Logstash JSON — это тоже нормальный формат для распознавания и чтения чужих логов. Важно не то, что аббревиатура красивая, а то, что вы держите один формат и не прыгаете между ними случайно.
Ключевой принцип стратегии здесь такой: профиль меняет форму вывода, но не меняет прикладной код и смысл сообщений. То есть мы не пишем в коде if (profile == prod) log.info("...") else log.debug("..."). Это почти всегда путь к хаосу. Мы хотим управлять логами через конфигурацию.
6. Минимальная политика MDC для catalog-service
MDC мы уже довели до маленького и достаточного baseline: общий requestId, локальные доменные поля вроде courseSlug или track, и обязательный cleanup через встроенный MDC.putCloseable(...). Для catalog-service этого достаточно: отдельная обёртка вокруг MDC имеет смысл только если команда реально убирает ею повторяющуюся боль, а не просто переименовывает тот же паттерн.
На уровне policy здесь важны три правила:
- базовые ключи остаются стабильными: requestId, courseSlug, track;
- в MDC кладём только маленькие и безопасные значения, а не целые объекты и не секреты;
- cleanup должен быть автоматическим, чтобы контекст не протекал между запросами.
Когда в коде нужно временное поле, нам хватает уже знакомого try-with-resources с MDC.putCloseable(...). Профили могут менять уровни и формат вывода, но не сам набор базовых корреляционных ключей: иначе логи между local, dev и prod перестанут складываться в одну картину.
7. Runtime-диагностика
Когда базовые уровни уже зафиксированы в YAML, временная диагностика должна быть узкой и обратимой. Поэтому /actuator/loggers мы воспринимаем не как вторую систему конфигурации, а как короткий runtime-override: посмотрели effectiveLevel, усилили нужный пакет, собрали сигнал, вернули обратно.
В стратегии мы фиксируем простой ритуал. Сначала читаем состояние логгера, чтобы понять, что реально происходит. Именно тут полезны configuredLevel и effectiveLevel: первый — что явно задано, второй — что реально действует. Потом делаем override только для узкой области. Например, не root, а com.example.catalogservice.catalog. После того как нашли причину — возвращаем configuredLevel в null, чтобы вернуться к наследованию уровней из YAML.
И самое важное: runtime override не должен стать «новым способом конфигурировать прод». Если вы понимаете, что DEBUG нужен постоянно, значит, проблема в базовой стратегии или в том, что вы логируете не те сигналы на INFO/WARN. Постоянные решения должны возвращаться в YAML, иначе вы получаете «конфигурацию, живущую в чьей-то памяти».
В учебном catalog-service мы обычно разрешаем такую диагностику в local/dev, а в prod (даже если он условный) мы держим идею, что всё должно быть максимально предсказуемо и безопасно. При этом сама стратегия не обязана запрещать диагностику; она должна делать её осознанной и не превращать в религию.
8. Профильная стратегия catalog-service
Теперь соберём всё в одну картину так, чтобы вы могли открыть репозиторий catalog-service через неделю и быстро понять: «ага, вот почему в local так, в dev иначе, а в prod всё строго». Важно: мы не добавляем никаких новых технологий и не уходим в logback XML. Всё делаем через YAML и аккуратную дисциплину сообщений/контекста.
Structured baseline у проекта уже фиксирован на ecs, поэтому здесь мы не выбираем формат заново, а просто раскладываем эту policy по профилям.
Начнём с идеи, что application.yaml задаёт общий baseline (уровни + чуть меньше шума Spring), а profile-файлы добавляют только то, что действительно зависит от окружения (verbosity и structured).
Вот хороший «скелет» для базового файла:
# src/main/resources/application.yaml
spring:
application:
name: catalog-service # Имя сервиса: будет попадать в логи/метрики там, где это поддерживается
logging:
level:
root: INFO
com.example.catalogservice: INFO
org.springframework: WARN
Теперь local. Мы хотим быстро дебажить свой код и не страдать от JSON в консоли, если он мешает. Поэтому просто включаем DEBUG на наш пакет:
# src/main/resources/application-local.yaml
logging:
level:
com.example.catalogservice: DEBUG
dev. Мы хотим тот же structured console baseline — ecs — и точечный DEBUG только для каталога, а не для всего приложения:
# src/main/resources/application-dev.yaml
logging:
structured:
format:
console: ecs
level:
com.example.catalogservice: INFO
com.example.catalogservice.catalog: DEBUG
prod. Structured console оставляем тем же, но держим уровни сдержанными. Дополнительно можно приглушить config-пакет до WARN, чтобы он не разговаривал слишком много «в обычной жизни»:
# src/main/resources/application-prod.yaml
logging:
structured:
format:
console: ecs
level:
com.example.catalogservice: INFO
com.example.catalogservice.config: WARN
Если смотреть на это как на стратегию, а не как на три несвязанных YAML-файла, то она отвечает на важные вопросы.
Во-первых, «что меняется по профилям?». Меняется structured/plain и точечный уровень детальности (DEBUG в local, DEBUG только для каталога в dev, строгие уровни в prod). Во-вторых, «что не меняется по профилям?». Не меняются имена логгеров, смысл сообщений, и наша дисциплина вокруг MDC (маленький стабильный набор ключей + гарантированная очистка).
И третье: везде читается один и тот же общий принцип — мы не кричим в логах, мы даём сигнал. Для этого даже полезно чуть привести в порядок стиль сообщений, особенно в StartupSummaryRunner. Если ваш startup-лог сейчас выглядит как «Привет мир! Я запустился!», то в local это может быть мило, но в dev/prod это просто неинформативно. Лучше писать коротко и по делу: что стартовало, какие профили, сколько курсов загружено, включён ли maintenance-mode, какой лимит featured. (И да, всё это можно логировать на INFO в одном-двух сообщениях, без спама.)
9. Типичные ошибки при логировании
Ошибка №1: превращать профили в три разных набора правил и стилей сообщений.
Очень соблазнительно начать «подстраивать текст логов под профиль»: в local писать расслабленно, в dev добавлять «технические формулировки», а в prod делать сухие сообщения. На практике это ломает сопоставимость логов и делает дебаг сложнее. Профиль должен менять детальность и форму, а не смысл и язык.
Ошибка №2: включать DEBUG на root как первую реакцию на проблему.
root=DEBUG почти гарантированно превращает логи в водопад. Вы утонете в сообщениях Spring и контейнера быстрее, чем найдёте свою проблему. Гораздо полезнее усиливать только пакет приложения (com.example.catalogservice) или ещё уже — конкретную фичу (...catalog), а иногда и конкретный класс.
Ошибка №3: держать runtime override как «постоянную конфигурацию».
Endpoint loggers — отличный инструмент диагностики, но плохой способ «настроить проект навсегда». Если вы неделю живёте с runtime override, то это значит, что конфигурация перестала быть воспроизводимой: новый человек запустит сервис и получит другое поведение. Постоянные решения возвращаются в YAML.
Ошибка №4: использовать MDC как свалку данных и забывать очищать контекст.
MDC должен быть маленьким и безопасным. Если вы кладёте туда крупные объекты или чувствительные значения, вы либо засоряете логи, либо создаёте риск утечки. А если не очищаете MDC, то контекст начинает «протекать» между разными операциями, и логи становятся обманчивыми: вроде поле courseSlug есть, но к сообщению оно не относится.
Ошибка №5: смешивать plain text и structured logging в одном профиле без правила.
Иногда это происходит случайно: часть логов идёт structured, часть — обычными строками (например, из-за разного запуска или из-за хаотичных настроек). В итоге читать сложно и глазами, и «по полям». Лучше выбрать один основной стиль на профиль. В нашем дне это обычно: local — plain, dev/prod — structured.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ