10 Logging Best Practices Every Java Backend Engineer Should Follow
This article presents ten practical rules for producing clean, searchable, and performance‑friendly logs in Java applications, covering unified formatting, stack traces, log levels, complete parameters, data masking, asynchronous writing, traceability, dynamic log levels, structured storage, and intelligent monitoring with concrete code snippets and configuration examples.
Introduction
This guide presents ten practical rules for producing clean, consistent, and maintainable logs in Java applications using Logback. The rules cover log format, exception handling, level usage, parameter completeness, data masking, performance, traceability, dynamic configuration, structured storage, and monitoring.
Rule 1 – Unified Log Format
Define a single pattern in logback.xml so every log entry contains the same fields. A typical pattern includes timestamp, trace identifier, thread name, log level, logger name and the message:
<!-- logback.xml core configuration -->
<pattern>%d{yy-MM-dd HH:mm:ss.SSS} |%X{traceId:-NO_ID}| %thread |%-5level |%logger{36} |%msg%n</pattern>This makes troubleshooting easier because all contextual information is present.
Rule 2 – Always Log Stack Traces
When catching an exception, pass the throwable to the logging call so the full stack trace is recorded.
try {
processOrder();
} catch (Exception e) {
log.error("Order processing failed, orderId={}", orderId, e); // e is mandatory
}Without the throwable the root cause is lost.
Rule 3 – Reasonable Log Levels
Use log levels according to the severity of the event:
FATAL : System is about to crash (e.g., OOM, disk full)
ERROR : Core business failure (payment error, order creation exception)
WARN : Recoverable problem (retry succeeded, degradation triggered)
INFO : Important business milestones (order status change)
DEBUG : Development‑time details (parameter values, intermediate results)
Misusing levels (e.g., logging a normal timeout as ERROR) obscures real issues.
Rule 4 – Complete Parameters
Log enough context to identify the who, what, where and why. Example of a detective‑style log:
log.warn("User login failed username={}, clientIP={}, failReason={}",
username, clientIP, "password attempts exceeded");The timestamp is supplied by the pattern defined in Rule 1.
Rule 5 – Data Masking
Mask sensitive fields before writing them to the log. A simple utility for mobile numbers:
public class LogMasker {
public static String maskMobile(String mobile) {
return mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
}
log.info("User registration mobile={}", LogMasker.maskMobile("13812345678"));Rule 6 – Asynchronous Logging for Performance
Synchronous writes can become a bottleneck under high concurrency (e.g., flash‑sale spikes). Use Logback’s AsyncAppender to off‑load I/O to a background thread.
Step 1 – AsyncAppender configuration
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold> <!-- never discard -->
<queueSize>4096</queueSize> <!-- depth = maxThreads × 2 -->
<appender-ref ref="FILE"/>
</appender>Step 2 – Logging code
// Good: let the framework handle queuing
log.debug("Received MQ message: {}", msg.toSimpleString());
// Bad: expensive computation before logging (still runs in the caller thread)
log.debug("Detailed content: {}", computeExpensiveLog());Step 3 – Sizing the queue
// Approximate memory usage
maxMemory ≈ queueSize × avgLogSize;
// Recommended depth = peakTPS × toleratedDelaySec
// Example: 10 000 TPS with 0.5 s tolerance → queueSize ≈ 5 000Monitor queue usage (alert at 80 %) and avoid large toString() calls that could cause OOM.
Rule 7 – End‑to‑End Traceability
Inject a unique traceId into the MDC at the entry point of each request and include it in the log pattern:
// Interceptor (or filter)
MDC.put("traceId", UUID.randomUUID().toString().substring(0,8));
// Pattern snippet
<pattern>%d{HH:mm:ss} |%X{traceId}| %msg%n</pattern>All logs across services then share the same identifier, enabling full‑chain correlation.
Rule 8 – Dynamic Log Level Adjustment
Expose a lightweight endpoint to change a logger’s level at runtime without restarting the JVM:
@GetMapping("/logLevel")
public String changeLogLevel(@RequestParam String loggerName,
@RequestParam String level) {
ch.qos.logback.classic.Logger logger =
(ch.qos.logback.classic.Logger) LoggerFactory.getLogger(loggerName);
logger.setLevel(ch.qos.logback.classic.Level.valueOf(level));
return "OK";
}This is useful for temporarily enabling DEBUG on a problematic component.
Rule 9 – Structured Storage
Store logs as JSON so downstream systems (e.g., Elasticsearch, Splunk) can index fields directly.
{
"event": "ORDER_CREATE",
"orderId": 1001,
"amount": 8999,
"products": [{"name": "iPhone", "sku": "A123"}]
}Structured logs eliminate parsing ambiguities and support powerful queries.
Rule 10 – Intelligent Monitoring
Integrate logs with the ELK stack (Elasticsearch, Logstash, Kibana) to aggregate, visualize and alert on patterns. Example alert thresholds:
ERROR logs > 100 entries within 5 min → phone alert
WARN logs sustained for 1 h → email notificationAutomated alerts prevent critical issues from remaining unnoticed for days.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
