Опирается на правила:
R-OBS-LOG-1…R-OBS-LOG-6иR-OBS-LOG-X1…R-OBS-LOG-X6из Observability Style Guide → раздел 1. Logging.
Важно знать
- JSON в проде через
logstash-logback-encoderилиEcsEncoder; текстовый pattern только в dev. JSON парсится Loki/ELK/Datadog без regex.@Slf4jчерез Lombok — никакихLoggerFactory.getLoggerруками.- Параметризация через
{}— Slf4j ленив, не выполняетtoStringпри отключённом уровне.- Уровни осмысленные: ERROR — actionable, WARN — recoverable degradation, INFO — important business event, DEBUG — детали отладки (не в проде), TRACE — никогда в проде.
- MDC поля:
traceId/spanId(OTel auto),requestId(filter),userId(после JWT). Попадают в JSON через encoder.- PII в логах запрещены (
AUTH-16): email, phone, паспорт, токены, пароли. Маскировать или вообще не логировать.log.error("Failed: " + e.getMessage())без stack — теряет причину. Всегда 2-arg:log.error("Failed: {}", ctx, e).
Логи — основной способ восстановить, что произошло в проде. Если они не structured, не parameterized, не содержат traceId — расследование инцидента превращается в часы grep'а по миллиону строк. UCP формулирует правила так, чтобы любой log-entry был полезен через год.
JSON в проде, текст в dev
R-OBS-LOG-1: два профиля Logback — dev и prod.
<configuration>
<springProfile name="dev,test">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%thread] %X{traceId:-} %logger{30} - %msg%n</pattern>
</encoder>
</appender>
</springProfile>
<springProfile name="prod,staging">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>requestId</includeMdcKeyName>
<includeMdcKeyName>userId</includeMdcKeyName>
</encoder>
</appender>
</springProfile>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
В dev — читаемая строка для глаз. В проде — JSON одной строкой с полями @timestamp, level, logger_name, message, mdc.traceId, mdc.requestId, mdc.userId, stack_trace. Loki/ELK индексируют поля и дают фильтры без regex.
@Slf4j через Lombok
R-OBS-LOG-2: на классе ставим @Slf4j, никакого private static final Logger log = LoggerFactory.getLogger(...) руками.
@Component
@RequiredArgsConstructor
@Slf4j
public class OrderService {
private final OrderRepository orderRepository;
public Order confirm(Long orderId) {
log.info("Confirming order: orderId={}", orderId);
var order = orderRepository.findById(orderId).orElseThrow();
order.confirm();
return orderRepository.save(order);
}
}
Lombok генерирует private static final Logger log за нас. Boilerplate-кода меньше, имя класса всегда совпадает с logger-ом.
Параметризация через {}
R-OBS-LOG-3: всегда {} placeholders, никогда string-concat.
log.info("Order created: orderId={} customerId={}", order.id(), order.customerId());
Почему не "Order created: " + order.id():
- Конкатенация выполняет
toStringвсегда, даже когда уровень disabled. - Для INFO это не страшно. Для DEBUG/TRACE — катастрофа:
log.debug("Order full: " + order.toJson())тратит CPU на сериализацию миллион раз в секунду на проде, где DEBUG выключен.
Slf4j placeholders ленивы — toString вызывается только если уровень активен.
Уровни логов
R-OBS-LOG-4: семантика каждого уровня.
| Уровень | Когда |
|---|---|
ERROR | Actionable failure: незакрытое исключение, упавшая транзакция, недоступность внешнего ресурса с broken state. Всегда со stack trace. |
WARN | Recoverable degradation: Circuit Breaker открылся, retry attempt, fallback использован, deprecated API вызван. |
INFO | Important business event: «order confirmed», «user registered», application start/stop, batch start/end с count. |
DEBUG | Детали для отладки. На проде выключено по умолчанию, включается per-package при инциденте. |
TRACE | Сверх-детально. Прод никогда; даже staging редко. |
log.error("Failed to charge payment: paymentId={}", paymentId, ex);
log.warn("Circuit breaker OPEN for payment-provider, falling back to queue");
log.info("Order confirmed: orderId={} customerId={} amount={}",
order.id(), order.customerId(), order.amount());
log.debug("Order aggregate state after confirm: {}", order);
Главная ошибка — INFO на каждый HTTP-запрос. Это access-log, отдельный лог-канал, не INFO в каждом handler-е. Без этой дисциплины 80% объёма прод-логов — шум.
MDC поля автоматически в JSON
R-OBS-LOG-5: каждая запись содержит:
traceId,spanId— автоматически через OpenTelemetry Logback appender (io.opentelemetry.instrumentation.logback-mdc-1.0).requestId—MdcFilterpopulates изX-Request-Idheader или генерирует UUID.userId— populates после JWT-валидации в Security filter chain.
{
"@timestamp": "2026-05-25T22:30:00.123Z",
"level": "INFO",
"logger_name": "ru.vikulinva.order.OrderService",
"message": "Confirming order: orderId=12345",
"mdc": {
"traceId": "5e92c8a3b1f4d2e6a7c8e9f0a1b2c3d4",
"spanId": "1f2e3d4c5b6a7980",
"requestId": "0193a8f3-7c21-7e3f-9b4a-...",
"userId": "user-42"
}
}
Это даёт связку «лог-запись → distributed trace → user». Без MDC — каждое расследование с нуля. Подробнее — Context propagation.
Логи на границах
R-OBS-LOG-6: логируем там, где сервис общается с внешним миром.
- Inbound REST request — access-log (
spring.mvc.log-request-details: false, отдельный loggerorg.springframework.web.servlet.DispatcherServlet). INFO на entry/exit только для критичных commands (платежи, money). - Outbound HTTP — INFO на request («Calling payment-provider charge"), WARN на 4xx/5xx, ERROR на network failure.
- Domain events — INFO на publish: «Published OrderCreated event: orderId=...».
- Schedulers — INFO на start/end batch с count: «Outbox-relay published 100 events in 50ms».
В середине бизнес-логики логи только если что-то решено или произошёл WARN. Не «Entering method», не «Loaded N rows» — это шум.
Что запрещено
PII в логах
R-OBS-LOG-X1 — критическое нарушение, см. AUTH-16. Email, телефон, ФИО, адрес, паспорт, банковские реквизиты, JWT-токены, пароли — никогда в логах в открытом виде.
// ПЛОХО
log.info("User registered: email={} phone={} passport={}",
user.email(), user.phone(), user.passport());
// ХОРОШО — только internal id
log.info("User registered: userId={}", user.id());
// ХОРОШО — если PII нужен для расследования, маскировать
log.info("Email verification sent: userId={} emailMask={}",
user.id(), maskEmail(user.email())); // u***@example.com
Logs пайплайн менее защищён, чем main DB; ретеншн дольше; доступ шире (вся команда, не только DBA). PII в логах = compliance incident.
System.out.println и printStackTrace
R-OBS-LOG-X2: эти методы пишут в stdout без MDC, без формата, без уровня. Не попадают в structured pipeline.
// ПЛОХО
System.out.println("Order: " + order);
e.printStackTrace();
// ХОРОШО
log.info("Order: {}", order);
log.error("Failed", e);
В UCP-проектах System.out блокируется ArchUnit-правилом или Checkstyle-rule. Любое legacy-API использование printStackTrace — на ревью.
String-concat в log args
R-OBS-LOG-X3: см. секцию «Параметризация». Особенно опасно для DEBUG/TRACE с тяжёлой сериализацией.
log.error без stack trace
R-OBS-LOG-X4: log.error("Failed: " + e.getMessage()) теряет stack — невозможно понять, где упало.
// ПЛОХО — message без stack
log.error("Failed to charge: " + e.getMessage());
// ХОРОШО — 2-arg form, stack попадает в JSON через encoder
log.error("Failed to charge: orderId={}", orderId, e);
Slf4j распознаёт последний аргумент-Throwable и передаёт его в encoder. JSON получает поле stack_trace.
Полный request body в логах
R-OBS-LOG-X5: для money/PII-эндпоинтов логировать только идентификаторы.
// ПЛОХО — payload может содержать card details / PII
log.info("Charge request: {}", chargeRequest);
// ХОРОШО — только id и amount
log.info("Charge request: orderId={} amount={}",
chargeRequest.orderId(), chargeRequest.amount());
INFO на каждый HTTP-запрос
R-OBS-LOG-X6: «Handling request X» в каждом handler-е → шум. Access-log делает это отдельно.
Что запрещено — таблица
| Антипаттерн | Правило | Что взамен |
|---|---|---|
| PII в логах (email, phone, паспорт, токены) | R-OBS-LOG-X1 | маскировать или только id |
System.out.println / e.printStackTrace() | R-OBS-LOG-X2 | @Slf4j + log.error |
| String-concat в log args | R-OBS-LOG-X3 | {} placeholders |
log.error("Failed: " + e.getMessage()) | R-OBS-LOG-X4 | log.error("Failed: {}", ctx, e) |
| Полный request body для money | R-OBS-LOG-X5 | только orderId + amount |
| INFO на каждый HTTP-запрос | R-OBS-LOG-X6 | access-log отдельно |
| Текстовый pattern в проде | R-OBS-LOG-1 | JSON encoder для prod profile |
LoggerFactory.getLogger(...) руками | R-OBS-LOG-2 | @Slf4j через Lombok |
Логи без MDC traceId/requestId | R-OBS-LOG-5 | encoder includes MDC keys |
Куда дальше
- Observability → раздел 1. Logging — нормативные формулировки
R-OBS-LOG-*. - Metrics — Micrometer + Prometheus, RED/USE.
- Tracing — OpenTelemetry, traceparent propagation.
- Context propagation (MDC) — как
traceId/requestId/userIdпопадают в MDC. - Configuration — Logback-spring.xml профили, management port.
- Auth → AUTH-16 — почему PII в логах критическое нарушение.
- Error handling — exception hierarchy, что попадает в
log.error.