skip to content

Logging initializes before most of the Spring context. What does that imply for configuration, and how would you standardize logging backends across many services?

level: principalimportance: nice to knowfreq 25%

answer

  1. logging inits at env-prepared, before context refresh
  2. only Environment available, not beans
  3. profiles/springProperty for static; MDC for dynamic
  4. SLF4J-only rule + shared config fragment
  5. backend via BOM; JSON-to-stdout for containers

basics

~20 s

Because logging boots very early, only the Environment (profiles, properties) is available to config — not application beans. Standardize by keeping app code on SLF4J, sharing a common logback-spring.xml (or log4j2-spring.xml) fragment, and centralizing the backend choice and versions via the BOM.

solid answer

~40 s

Spring Boot initializes its LoggingSystem during the bootstrap/environment-prepared phase, before the ApplicationContext refresh. That's why <springProperty> can read the Environment but a logging config can't depend on a @Bean or context-time value. Practical implications: keep dynamic values in application.yml and surface them via <springProperty>; expect a brief default-pattern preamble before your config takes over; use <springProfile> for per-environment differences rather than programmatic switching. To standardize across many services, mandate coding to SLF4J only, ship a shared logging config fragment (included via classpath resource) that defines appenders and patterns, choose one backend org-wide (Logback default, or Log4j2 with a one-place exclusion), and pin/patch versions through the Spring Boot BOM. For containers, prefer structured JSON to stdout and let the platform handle files/rotation.

code

java · 14 lines
java
// shared-logging.xml: an org-wide fragment services <include>
// <included>
//     <springProperty scope="context" name="svc"
//                     source="spring.application.name" defaultValue="app"/>
//     <appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
//         <!-- structured line so a log aggregator can parse fields uniformly -->
//         <encoder>
//             <pattern>{"ts":"%d{ISO8601}","lvl":"%level","svc":"${svc}","traceId":"%X{traceId}","logger":"%logger","msg":"%message"}%n</pattern>
//         </encoder>
//     </appender>
//     <root level="INFO"><appender-ref ref="JSON_CONSOLE"/></root>
// </included>
//
// In each service's logback-spring.xml: <include resource="shared-logging.xml"/>

go deeper

for a junior

Just know logging starts very early and reads properties/profiles.

for a middle

Explain that only the Environment (not beans) is available and use profiles for env differences.

for a senior

Design shared config, MDC correlation, and the static-vs-runtime boundary.

for a principal

Set org-wide policy: SLF4J-only rule, one backend via BOM, shared JSON-to-stdout config, and a coordinated security-patch path.

## Why logging is early Spring Boot must produce log output during its own startup, so it initializes the logging system very early — around the `ApplicationEnvironmentPreparedEvent`, after the `Environment` (property sources, active profiles) is ready but **before** the `ApplicationContext` is refreshed and beans are created. The class responsible is the `LoggingSystem` abstraction (`LogbackLoggingSystem` / `Log4J2LoggingSystem`), driven by `LoggingApplicationListener`. ### Consequences for configuration 1. **Only environment-level data is available.** `<springProperty source="...">` can bind property sources and profiles, but you cannot inject a value produced by a `@Configuration` bean or a runtime computation — those don't exist yet. 2. **A short default-config preamble is normal.** The very first lines (before your config is applied) use Boot's built-in defaults; then the system re-initializes with your `logback-spring.xml`/`log4j2-spring.xml`. 3. **Profile-based branching, not code.** Use `<springProfile>`/`<SpringProfile>` for per-environment appenders and levels because there's no bean lifecycle to hook into at this point. 4. **Property overrides still work.** `logging.level.*` can be applied from any property source Boot resolves early, including command-line args and environment variables — useful for turning up a package's level in production without redeploying config. ## Standardizing across many services ### 1. One facade, always Enforce that application and library code import only `org.slf4j.Logger`. This is the linchpin: it makes the backend an infrastructure decision, not a code decision, so any service can switch or standardize without code churn. (An ArchUnit/detekt rule can ban `ch.qos.logback.*` and `org.apache.logging.log4j.*` imports outside config.) ### 2. Pick one backend deliberately - **Logback** (default) is the low-friction choice; async via `AsyncAppender` when needed. - **Log4j2** if you genuinely need Disruptor-based async throughput or garbage-free logging; apply the Logback exclusion in one place (Gradle `configurations.all { exclude ... }`). Don't let it be accidental — a stray transitive binding causes 'multiple providers' non-determinism. ### 3. Share a config fragment Publish a small internal artifact containing a common `logback-spring.xml` fragment (appenders, patterns, MDC fields like traceId) that services `<include>` by classpath resource. This gives uniform log shape (crucial for centralized aggregation) while letting services add service-specific loggers. ### 4. Version hygiene / security Manage Logback/Log4j2 versions through the **Spring Boot dependency BOM** so upgrades are coordinated. The Log4Shell (CVE-2021-44228) episode is the cautionary tale: a logging library became a remote-code-execution vector; a standardized, BOM-driven upgrade path is an operational safeguard. ### 5. Container-friendly output In Kubernetes/containers, prefer **structured JSON logs to stdout** (Logback's `LogstashEncoder` or Spring Boot 3.4+ native structured logging via `logging.structured.format.console=ecs`) and let the platform collect/rotate. Avoid writing rotating files inside ephemeral containers. ### 6. Observability tie-in Enrich logs with **MDC** correlation IDs (trace/span from Micrometer Tracing) so log lines join traces. Because MDC is populated at request time (well after logging init), this is a runtime concern, not a config-init one — a good example of the boundary between static logging config and dynamic per-request context. ## Summary of the boundary - Static, environment-driven → logging config file (`-spring` variant) with profiles/properties. - Dynamic, per-request → MDC + code. - Backend selection → dependencies/BOM, invisible to app code thanks to SLF4J.

  • Why can't a logging config value come from a @Bean or @ConfigurationProperties object populated at context time?
    The LoggingSystem is initialized during the environment-prepared phase, before the ApplicationContext is refreshed and beans exist. Only the Environment (property sources, profiles) is available, which is exactly what <springProperty> reads.
  • In a containerized deployment, why prefer JSON logs to stdout over rolling files?
    Containers are ephemeral and the platform (Docker/Kubernetes) already captures stdout; structured JSON lets a central aggregator parse fields uniformly across services, avoiding lost files, disk pressure, and per-service rotation config.

saying these in an interview costs you the question

  • Expecting logging config to read application beans or context-time computed values
  • Letting the logging backend be chosen accidentally by transitive deps
  • Writing rotating log files inside ephemeral containers instead of stdout
  • Duplicating log patterns per service instead of sharing a fragment
  • Ignoring BOM-managed version patching after the Log4Shell lesson

context