I have spent more hours than I care to admit staring at logs that said “Process started” followed immediately by “Error: null.”. Some other services that print every single step inside the methods, but missing the output values.
The logs should be written with a goal in mind: “what will I need if a problem occurs”. If your logs don’t provide a clear path to the root cause of a failure, they are just expensive noise.
To move beyond “println” debugging in production, you need a structured approach that prioritizes utility over aesthetics.
Why Log Levels Fail Without a Team Convention
Most developers use log levels, but few use them consistently. Consistency is the difference between a quick fix and a three-hour incident response.
- ERROR: Use this only when an operation truly fails and requires intervention or affects the user’s outcome. If an error doesn’t require someone to take action, it might just be a warning.
- WARN: Reserve this for unexpected behaviors that the system can recover from (e.g., a retried network request or a deprecated API call).
- INFO: These should track major state changes or business milestones. “User created” is a valid INFO log; “Entering method X” is not.
- DEBUG: This is the place for high-volume technical details. In production, this level is usually silenced unless you are actively troubleshooting a specific segment of the infrastructure.
Ensure everyone in the team knows this, to avoid going from all the production alerts and change the log level from ERROR to INFO because someone thought that an incorrect password must trigger an alert.
The Anatomy of a Useful Log Entry
A log entry consisting of a simple string is a liability. For logs to be machine-readable and searchable, they must be structured—ideally in JSON format. Every log should include the feature or package name, a clear description, and the relevant state variables.
Example: Structured Log in Java (SLF4J with Logback)
In Java, we often use the Mapped Diagnostic Context (MDC) to inject contextual information like Trace IDs or user IDs into every log line within a thread’s execution.
import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;public class PaymentService { private static final Logger logger = LoggerFactory.getLogger(PaymentService.class); public void processPayment(String orderId, String userId, long amount) { MDC.put("orderId", orderId); MDC.put("userId", userId); MDC.put("feature", "payment-processor"); try { captureFunds(amount); logger.info("Payment captured successfully for amount: {}", amount); } catch (Exception e) { logger.error("Failed to capture funds: {} - State: amount={}", e.getMessage(), amount, e); } finally { MDC.clear(); } }}
By using SLF4J’s parameterization {} and MDC, you enable your log aggregator (like ELK) to index specific attributes. Never log sensitive PII, but always log the identifiers necessary to reconstruct the event.
How to Propagate Trace IDs Across Spring Boot Services
In a microservices architecture, a single user request can hop across five different services. A log in the “Billing” service is useless if you cannot link it to the initial request in the “API Gateway.”
You must propagate a Trace ID through every service involved in a request lifecycle.
- Trace ID: A unique identifier for the entire end-to-end request.
- Span ID: A unique identifier for a specific operation within a single service.
When an error occurs in a downstream service, the Trace ID allows you to visualize the entire execution path. If you aren’t using Trace IDs, you aren’t debugging a distributed system; you are guessing.
For more details about the configuration, check this article.
Adopting a Utility-First Logging Philosophy
The goal of logging is to provide enough context to solve a problem without looking at the code. Before adding a log line, ask yourself: If I get a 2:00 AM alert, will this specific log help me fix the issue?
Avoid “pretty” logs that just track progress.
- Bad:
logger.info("Connected to database"); - Good:
logger.info("Database connection established to {}:{}", dbHost, dbPort);
Stop logging “success” messages for every minor operation. This creates “log fatigue” and increases your ingestion costs. Focus on the transitions and the failures. If your application is healthy, the logs should be relatively quiet; when things break, the logs should provide a high-resolution map of the failure.
Ensuring your logging strategy is documented and structured is a hallmark of a mid-level developer moving toward seniority. It demonstrates an understanding that code doesn’t live in a vacuum—it lives in a production environment where visibility is everything.


Leave a comment