1. Structure of a log message
Let’s imagine that logs are not just a “stream of consciousness” of your program, but a valuable journal where, a month or a year later, you or your colleague can find the answer to the question: “What the heck happened here?”. For that to be possible, every log message must be structured. Typically (and this is the standard in most libraries) each message contains:
- Event time — when it happened.
- Level — how important it is (INFO, ERROR, etc.).
- Logger name — usually the class or component name.
- Message text — what exactly happened.
- Stack trace (if there’s an error) — to understand where and why.
Here is an example of a well-formatted log line (Log4j/SLF4J):
2024-06-16 18:42:07,123 INFO com.example.MainApp - User logged in: username=vasya
And if an error occurred:
2024-06-16 18:42:10,456 ERROR com.example.LoginService - User authentication error: vasya
java.lang.IllegalArgumentException: Invalid password
at com.example.LoginService.checkPassword(LoginService.java:42)
...
Why does this matter?
When an application runs for a long time, logs can take up gigabytes. If messages aren’t structured, finding a problem becomes a “guess the tune by the sound of a fan” challenge.
2. Formatting messages
Why you shouldn’t do this:
logger.info("User " + username + " logged in");
This looks simple, but there is a catch: even if the current logging level is ERROR, the string inside the parentheses will still be constructed (concatenation happens), which wastes resources. In large systems, where logs are thousands of lines per second, this can lead to real delays.
The right way: templates and parameters
Modern libraries (for example, SLF4J and Log4j 2) support parameterized templates:
logger.info("User {} logged in", username);
Here the string will be assembled only if the logging level allows this message to be output. If the level is, for example, WARN, then the string won’t even be computed — saving resources and nerves.
Bonus: if you pass several parameters, they are substituted in order:
logger.info("User {} performed action {} on object {}", username, action, objectId);
Logging exceptions (stack trace)
If you catch an exception, don’t manually append a stack trace to the message:
// DON'T DO THIS:
logger.error("Error: " + ex.getMessage() + "\n" + Arrays.toString(ex.getStackTrace()));
The correct way:
logger.error("Error processing request", ex);
SLF4J and Log4j will nicely append the stack trace to the log for you.
Example: comparing approaches
// Bad (concatenation always executes)
logger.debug("Object: " + expensiveToString(obj));
// Good (lazy formatting)
logger.debug("Object: {}", obj);
3. Choosing logging levels
If everything in your logs is at ERROR, that’s not logging anymore, it’s a “red light.” If everything is at DEBUG, you’ll drown in details. Let’s figure out when to use which level.
| Level | Purpose | Sample message |
|---|---|---|
|
Critical failures that cause the system to malfunction or not work at all | “Database connection error” |
|
Important warnings that aren’t critical but require attention | “Failed to find user, using guest” |
|
Routine events that reflect normal application operation | “User registered: vasya” |
|
Detailed information for debugging; not needed in production | “Method checkPassword called with parameters ...” |
|
The most detailed information, usually for deep diagnostics | “Processing loop start: i=0” |
Typical message examples
- ERROR — failed to write file, unhandled exception caught, service unavailable.
- WARN — deprecated API, suspicious user behavior, attempt limit exceeded.
- INFO — user logged in/out, order processing completed, application startup.
- DEBUG — request parameters, variable values, intermediate computation results.
- TRACE — method entry/exit, internal loops, algorithm details.
Tip:
In production, you usually enable only INFO and above, sometimes WARN and ERROR. DEBUG and TRACE — only when hunting tricky bugs.
4. Best practices (logging best practices)
Don’t log sensitive data
Passwords, tokens, credit card numbers — none of this belongs in logs. Even if it seems like “the log file is just for me,” remember GDPR and the colleague who might accidentally post the log in a public chat.
// Bad:
logger.info("User {} logged in with password {}", username, password);
// Good:
logger.info("User {} logged in", username);
Don’t overuse the ERROR level
If you write everything via logger.error, then when a real catastrophe happens, nobody will notice — everyone’s used to the “red lights.” Use ERROR only for situations where the application truly cannot continue or business logic is violated.
Log exceptions with the full stack
Don’t log only ex.getMessage(), or you’ll never know where the error occurred. Pass the exception as the second parameter to the logger.
logger.error("Error processing request", ex);
Use unique identifiers (event correlation)
In large systems, it’s useful to assign each request, user, or operation a unique identifier. This helps “stitch together” events from different parts of the system.
logger.info("Order processing started: orderId={}", orderId);
logger.info("Order processed successfully: orderId={}", orderId);
Don’t log everything
If there’s too much logging, it becomes useless. Don’t log every line of code, or you won’t be able to find what you need.
Format messages clearly
Write messages so that not only the code author understands them, but also the person reading the logs six months later. Avoid abbreviations, obscure shortcuts, and “inside jokes.”
5. Practice: configuring log format and levels
Example of format configuration in Log4j2 (log4j2.xml)
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
What does this mean?
- %d{...} — event time.
- %-5level — level (ERROR, INFO, etc.).
- %logger{36} — logger name (usually the class).
- %msg — the message itself.
Code example with different logging levels (SLF4J)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogDemo {
private static final Logger logger = LoggerFactory.getLogger(LogDemo.class);
public static void main(String[] args) {
logger.info("Application started");
logger.debug("Variable x value: {}", 42);
try {
throw new IllegalArgumentException("Uh-oh!");
// ...
} catch (Exception ex) {
logger.error("An error occurred during startup", ex);
}
}
}
Demonstrating the difference between levels
If the logger is configured at INFO level, messages at DEBUG and below won’t be printed. Try changing the level to debug in the config — you’ll see the details.
6. Common mistakes
Mistake No. 1: String concatenation in logs. Very often beginners write this:
logger.debug("User: " + user.getName() + ", role: " + user.getRole());
As a result, even with DEBUG disabled, these strings are still constructed, causing unnecessary overhead. Use parameters!
Mistake No. 2: Logging without the exception stack. They log only a message:
logger.error("Error: " + ex.getMessage());
As a result, there’s no information in the logs about where the error occurred. Pass the exception as the second parameter!
Mistake No. 3: Logging everything at ERROR level. If everything is red — nothing is red. Use levels as intended, or important errors will be buried among “trivia.”
Mistake No. 4: Logging sensitive data. Never put passwords, tokens, or card numbers in logs. Even if you think nobody will see it, life loves surprises.
Mistake No. 5: Unclear messages. If a log message looks like “ERR42: fail,” in a month you won’t remember what it means. Write clearly and with sufficient detail.
Mistake No. 6: Missing unique identifiers. In complex systems, without orderId, userId, and other identifiers, you won’t be able to “stitch” events together and understand what happened to a specific user or order.
GO TO FULL VERSION