Опирается на правила: R-OBS-LOG-1R-OBS-LOG-6 и R-OBS-LOG-X1R-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: семантика каждого уровня.

УровеньКогда
ERRORActionable failure: незакрытое исключение, упавшая транзакция, недоступность внешнего ресурса с broken state. Всегда со stack trace.
WARNRecoverable degradation: Circuit Breaker открылся, retry attempt, fallback использован, deprecated API вызван.
INFOImportant 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).
  • requestIdMdcFilter populates из X-Request-Id header или генерирует 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, отдельный logger org.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 argsR-OBS-LOG-X3{} placeholders
log.error("Failed: " + e.getMessage())R-OBS-LOG-X4log.error("Failed: {}", ctx, e)
Полный request body для moneyR-OBS-LOG-X5только orderId + amount
INFO на каждый HTTP-запросR-OBS-LOG-X6access-log отдельно
Текстовый pattern в продеR-OBS-LOG-1JSON encoder для prod profile
LoggerFactory.getLogger(...) рукамиR-OBS-LOG-2@Slf4j через Lombok
Логи без MDC traceId/requestIdR-OBS-LOG-5encoder 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.