1. Минусы plain-text логов
На старте проекта обычные текстовые логи кажутся идеальными: открыл консоль — увидел «приложение запустилось», «курсы загрузились», «порт такой-то» — красота. Проблема появляется не потому, что текст плох, а потому что текст слишком «человеческий»: он отлично читается глазами, но плохо поддается стабильному разбору по полям, когда вы хотите искать, фильтровать и сравнивать записи не по догадке, а по фактам.
Представьте типичный жизненный сценарий catalog-service. Вы получили жалобу: «иногда не находится курс по slug». Вы включили больше логов, у вас пошли сообщения из разных пакетов, и вы хотите ответить на простой вопрос: где именно происходила ошибка и с какими параметрами. В plain-text формате вы часто упираетесь в то, что нужные куски данных спрятаны внутри строки сообщения, а формат сообщений может отличаться от класса к классу. В итоге вы либо «на глаз» листаете консоль, либо начинаете играть в регулярные выражения, что особенно весело, когда лог-сообщение слегка поменялось, и ваш «поиск по шаблону» внезапно перестал работать.
Есть и более приземленная проблема: вы очень быстро хотите фильтровать по уровню, по классу-логгеру, по имени сервиса, по потоку, по конкретному «корреляционному» идентификатору операции. В plain-text это возможно, но хрупко. Любая мелкая смена формата приводит к тому, что машина перестает понимать логи так же хорошо, как человек. А мы ведь пишем backend не только для людей. Иногда его читают и машины — просто потому, что машин в продакшене обычно больше.
Чтобы не звучало слишком «enterprise», давайте сформулируем приземлённо: plain-text лог — это когда все важные поля склеены в одну строку. Structured лог — это когда эти поля остаются полями, и вы не должны «выковыривать» уровень или логгер из текста, как изюм из булочки.
Небольшая табличка, чтобы зафиксировать смысл на пальцах:
| Задача | Plain-text лог | Structured лог |
|---|---|---|
| Быстро глазами понять, что происходит | Удобно | Часто неудобно, потому что JSON получается многословным |
| Найти все записи, где level=ERROR | Обычно через grep/шаблон, иногда хрупко | По полю level — стабильно |
| Фильтровать по логгеру/пакету | По совпадению строки | По отдельному полю |
| Автоматически анализировать логи программой | Сложнее, нужен парсинг текста | Проще, потому что это уже JSON или набор полей |
2. Structured logging и machine-readable line
Structured logging легко случайно понять неправильно, поэтому начнем с честной и простой модели. У каждого log event есть набор данных: время, уровень, имя логгера, сообщение, иногда исключение, иногда имя потока и другие служебные штуки. В plain-text формате эти данные превращаются в одну строку «для человека». В structured формате они остаются структурой — чаще всего в виде JSON-объекта — и печатаются как одна строка JSON, пригодная для парсинга машиной.
Термин machine-readable log line означает: каждая строка лога — это самостоятельный объект фиксированного формата, который можно разобрать без «угадывания». Если это JSON-лог, то одна строка — один JSON-объект. Да, выглядит как «в консоль вывалили словарь», но именно это и ценность: поля не потерялись и не растворились внутри текста.
Полезно представить себе путь лога так, очень упрощённо, без деталей Logback и без погружения в конфиги:
flowchart LR %% Упрощенная схема: важно понять, что structured — это про "как печатаем", а не про "как пишем log.info" A["Ваш код: log.info(...)"] --> B["Log event (поля: time, level, logger, message, ...)"] B --> C["Encoder/Formatter (plain или structured)"] C --> D["Вывод: консоль/файл"]
Обратите внимание на важную мысль: структурированность — это про формат вывода, а не про то, что мы «по-другому пишем log.info(...)». Вы создаете событие логирования точно так же, а вот «принтер» печатает его либо как человеко-читаемую строку, либо как машино-читаемый JSON.
Пример сверх-минимального structured лога, чтобы увидеть идею:
{
"timestamp": "2026-03-19T10:15:00Z",
"level": "INFO",
"logger": "com.example.catalogservice.catalog.service.CourseCatalogService",
"message": "Featured courses loaded: 4"
}
В реальности Spring Boot поддерживает несколько стандартных структурированных форматов, например ECS, GELF и Logstash, и они отличаются названиями полей и их группировкой. В этой лекции нам важна не таблица всех полей, а понимание ключевого эффекта: уровень, логгер, сообщение и время становятся отдельными полями, а не частью «склеенной» строки.
3. Формат вывода вместо log.info(...)
Самая приятная новость: чтобы перейти на structured logging, вам обычно не нужно переписывать прикладной код. Если вы уже логируете правильно — через SLF4J-логгер, с плейсхолдерами {} — то структурирование меняет в основном то, как лог будет выглядеть снаружи. То есть мы не переписываем сервисы и контроллеры, а аккуратно подкручиваем конфигурацию — как и положено в Spring Boot, где большая часть поведения управляется конфигом.
Давайте возьмем маленький, понятный фрагмент из контекста catalog-service. Например, наш сервис каталога логирует факт загрузки featured-курсов. Код при этом максимально обычный, без всякой structured-магии:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CourseCatalogService {
// SLF4J-логгер: код не "знает" про JSON, он просто создает log-события
private static final Logger log =
LoggerFactory.getLogger(CourseCatalogService.class);
public void logFeaturedCount(int count) {
// Плейсхолдер {} + отдельный аргумент: так параметр передается системе логирования, а не склеивается строкой
log.info("Featured courses loaded: {}", count); // сообщение останется тем же
}
}
Здесь важно не пропустить два момента. Во‑первых, мы используем плейсхолдер {} и передаем параметр отдельно, а не склеиваем строку через +. Это и быстрее, и чище, и лучше дружит с логированием как системой. Во‑вторых, никакого JSON в коде нет и не должно быть. Это принципиально: structured logging не должен превращать ваш доменный код в код под лог-формат.
Если вы сейчас включите structured logging, эта же запись log.info(...) поедет наружу уже в structured виде. То есть изменится «фотография», а не «модель». И это очень по-boot’овски: поведение меняется через конфигурацию, а не через переписывание десяти классов.
4. Включаем structured logging в Spring Boot 4
Теперь переходим к самому практическому: как включить structured logging так, чтобы он действительно включился, а не остался «идеей в голове». В Spring Boot 4 structured logging включается конфигурацией. Нас интересуют два свойства: одно отвечает за консоль, другое — за файл. Даже если вы пока пишете только в консоль, важно понимать, что это две независимые цели: консоль обычно читает человек, а файлы и потоки логов часто читает инструмент.
Вот компактная табличка по настройкам, чтобы не путать их:
| Куда пишем | Свойство | Что управляет |
|---|---|---|
| В консоль (stdout) | logging.structured.format.console | Формат вывода в консоль |
| В файл | logging.structured.format.file | Формат вывода в файл |
Для старта нам достаточно включить structured формат для консоли. Например, в src/main/resources/application.yaml можно добавить:
spring:
application:
name: catalog-service # имя сервиса попадет в structured-поля (например, service.name)
logging:
structured:
format:
console: ecs # включаем structured формат для консоли (stdout)
Это минимальное включение structured logging в консоль. Для catalog-service в курсе считаем ecs рабочим baseline structured console: на нём фиксируем дальнейшие примеры и финальную policy проекта. Значение ecs — это название одного из поддерживаемых форматов. GELF и Logstash нам ещё пригодятся, но уже как форматы, которые нужно уметь узнавать и читать, а не как плавающий выбор внутри проекта.
Обратите внимание: мы не пишем logback.xml, не подключаем странные энкодеры вручную и не влезаем в дебри конфигурации. Мы делаем ровно то, что Boot любит больше всего: описываем желаемое поведение декларативно.
Если вам нужно включить structured формат для файла, логика будет аналогичной — меняется только точка назначения:
logging:
structured:
format:
file: ecs # включаем structured формат для файла (обычно его забирает сборщик логов)
Мы сознательно не обсуждаем здесь настройку имени файла, ротацию и архивирование — это отдельная вселенная. Сейчас фиксируем главное: в Boot structured logging включается свойствами, и ваш Java-код при этом не обязан «знать», что логи стали JSON.
Наконец, важная практическая мысль: structured logging — это не «вечно включенная опция». Он должен включаться там, где он дает ценность. Если вы включили JSON-логи в консоль и внезапно почувствовали, что дебажить стало неудобнее, это не «вы неправы», это нормальный эффект. Просто вы включили инструмент не под ту задачу. Консоль обычно для глаз, structured формат — для стабильной обработки.
5. Сравнение: plain vs structured
Чтобы не было ощущения «я что-то включил, но не понимаю, что изменилось», давайте сравним два вывода для одной и той же записи log.info("Featured courses loaded: {}", count) из CourseCatalogService. С точки зрения кода ничего не поменялось. Поменялся только способ печати события логирования.
Пример того, как подобная запись может выглядеть в plain-text формате:
2026-03-19T10:15:00.123 INFO 18432 --- [catalog-service] c.e.c.c.service.CourseCatalogService : Featured courses loaded: 4
А теперь условный structured вариант:
{"@timestamp":"2026-03-19T10:15:00.123Z","log.level":"INFO","log.logger":"com.example.catalogservice.catalog.service.CourseCatalogService","service.name":"catalog-service","message":"Featured courses loaded: 4"}
Если вы сейчас думаете: «Выглядит хуже, чем раньше», то поздравляю — вы правильно читаете глазами. Structured лог зачастую реально хуже читается глазами, потому что он оптимизирован не под человека, а под стабильные поля. Зато у него появляется суперсила: уровень — это поле log.level, логгер — отдельное поле, имя сервиса — отдельное поле. Машина не должна угадывать, где именно в строке спрятано имя логгера, и не должна надеяться, что вы не поменяете порядок частей.
Иногда полезно показать JSON красиво, хотя в логах он обычно в одну строку. Это не новый формат, а просто визуализация того, что внутри:
{
"@timestamp": "2026-03-19T10:15:00.123Z",
"log.level": "INFO",
"service.name": "catalog-service",
"message": "Featured courses loaded: 4"
}
И вот здесь хорошо видно, ради чего всё это затевается: если вам нужно быстро отфильтровать все INFO от нашего сервиса, вы не парсите строку. Вы смотрите на поля.
6. Где полезен structured logging
С structured logging легко попасть в ловушку «раз это современно — включу везде». Но логирование — это не показ мод. Оно либо помогает диагностировать систему, либо мешает. Structured формат особенно полезен там, где логи должны быть стабильными входными данными для обработки: для поиска, агрегации, построения статистики, разборов по полям. Это может быть и локально, если вы хотите быстро отбирать записи по полям, и в средах, где логи куда-то собираются и анализируются.
Но есть и обратная сторона. Когда вы запускаете catalog-service в IDE и просто читаете консоль, JSON может превращать простой дебаг в чтение бухгалтерского отчета. Вам хочется увидеть короткую строку, а вы видите объект с полями. Это нормально. Structured logging не отменяет ценность plain-text вывода, он просто решает другую задачу.
Очень важно зафиксировать ещё одну мысль, чтобы не было магических ожиданий: structured logging не делает лог «умным» автоматически. Если вы написали плохое сообщение, оно останется плохим — просто в JSON. Вот пример плохого лога, который формально structured, но практически бесполезен:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class StartupSummaryRunner {
private static final Logger log = LoggerFactory.getLogger(StartupSummaryRunner.class);
public void run() {
// Проблема не в формате, а в смысле: по такому логу непонятно, что именно "Started"
log.info("Started"); // слишком мало смысла
}
}
А вот более полезный вариант — тот же уровень, тот же логгер, но сообщение уже несет конкретный сигнал:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class StartupSummaryRunner {
private static final Logger log = LoggerFactory.getLogger(StartupSummaryRunner.class);
public void run(int coursesCount) {
// Тут есть конкретный факт + параметр передается отдельно через {}, что корректно для логирования
log.info("Catalog started. Courses loaded: {}", coursesCount); // понятный сигнал
}
}
Structured формат не спасает от дисциплины: не логировать огромные объекты целиком только потому, что JSON якобы «для этого». Если вы начнете печатать весь список курсов в лог, вы получите гигантский шум, и никакая структурированность не вернет из него полезный сигнал.
И да — structured формат тоже не отменяет базовое правило безопасности. Если вы залогировали токен, то вы залогировали токен. Он будет не менее опасен, просто красиво упакован в JSON.
7. Типичные ошибки при включении structured logging
Structured logging — штука полезная, но новичкам он особенно часто «стреляет в ногу» не потому, что технология плохая, а потому что ожидания от неё не совпадают с реальностью. Если заранее знать типовые ошибки, можно сэкономить себе кучу времени, а заодно не превратить логи в странный артефакт, который никто не хочет читать.
Ошибка №1: включить structured logging и ожидать «красивую консоль».
Structured логи часто выглядят в консоли хуже, чем plain-text, и это нормально. Они не обязаны быть удобными для глаз. Перед включением стоит честно ответить: вы сейчас лог читаете глазами или вам важны поля для стабильного разбора. Если цель — быстро дебажить руками, plain-text иногда выигрывает.
Ошибка №2: думать, что JSON-форма заменяет хороший текст сообщения.
Если сообщение «не о чём» или слишком расплывчатое, structured формат лишь упакует эту расплывчатость в JSON. Смысл всё равно остается в сообщении. Loading... и Started почти всегда хуже, чем Catalog started. Courses loaded: 12 — независимо от формата.
Ошибка №3: логировать большие объекты целиком, потому что «раз уже JSON».
Печатать целый список курсов, целую конфигурацию или целый response «для удобства» — очень соблазнительно. Но это рождает шум, увеличивает объем логов и делает реальные проблемы менее заметными. Лучше логировать агрегаты и факты: количество, slug, флаги, важные решения, а не все данные мира.
Ошибка №4: смешать разные стили логов без ясного правила.
Например, часть окружений пишет plain-text, часть — structured, а часть — и то и другое вперемешку, потому что конфигурация разъехалась. В итоге вы не уверены, что именно увидите при запуске. Логи должны быть предсказуемыми: либо здесь plain, либо здесь structured, и это закреплено конфигом, а не памятью разработчика.
Ошибка №5: перепутать structured logging с настройкой уровней.
Structured logging — это не «DEBUG вместо INFO» и не «больше логов». Это форма вывода. Можно писать structured логи на INFO, можно на DEBUG, можно на ERROR. Уровни решают, сколько событий вы пишете; structured формат решает, как эти события выглядят и насколько легко их разбирать по полям.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ