Level: Intermediate | Java SE 17
Table of Contents
- 2.1 Definition and necessity of logging
- 2.2 When to log and for whom
- 2.3 Case study: Carved Rock Fitness – orders problem
- 2.4 Added basic logging
- 3.1 Overview of java.util.logging package
- 3.2 Log Levels
- 3.3 Log Handlers
- 3.4 The different log methods
- 4.1 Logger to a file (FileHandler)
- 4.2 Log formatters
- 4.3 Log Filters
- 4.4 The LogManager and the configuration file
- 6.1 Current libraries
- 6.2 The Log4j library
- 6.3 The SLF4J library
- 6.4 Vulnerability Check
- 7.1 Project structure
- 7.2 Module 2 – Application without logging
- 7.3 Module 2 – Application with basic logging
- 7.4 Module 3 – Configuring a ConsoleHandler with FINE level
- 7.5 Module 4 – Final application with LogManager and configuration file
- 7.6 Module 5 – Good Code Practices
1. Course Overview
This course covers logging in Java. Logging is the act of writing information about events and situations that occur in an application, whether to a file or to the console. This information may contain errors, warnings, but also debugging information. The log is crucial for monitoring and troubleshooting an application.
Topics covered in this course
- What is logging and when to use it
- How to get a
Logger, set levels and handlers - Log formatters and filters
- How to use the
LogManagerand a configuration file - Good logging practices
- Common external logging libraries (Log4j, SLF4J)
Prerequisites
- Knowledge of Java basics
Applicable versions
This course was created for Java SE 17. The concepts apply to a wide range of Java versions, but some very old ones may differ.
2. What is Logging and how to use it
2.1 Definition and necessity of logging
logging is the act of writing information about application events to a file or the console. Logging occurs during execution of the application. When it is not running, there is no logging. The log can be replayed to see what happened in the application.
Documented events can be:
- Exceptions and errors
- Information about the happy path (happy path)
- Debug information (debug intel)
The log is usually written to a file and can be viewed after running the application. This course focuses on logs written by the application itself, programmed by the developer. There are other types of logs (JVM, garbage collector, server, system) written by other developers.
Why log?
Logging is of great importance for several reasons:
- Debugging and troubleshooting: When something abnormal happens and we cannot determine what is happening from the surface (front end), the log allows us to know what happened below. Without logging, we have no clue. With a good log, you get a lot of information about what’s happening in the application, which is often a shortcut to solving the problem.
Example experienced by the instructor: in a company, when creating accounts for people and companies, a check based on gender (male or female) determined whether the entity was a person or a company. This bug was only discovered through informational logs when the application became inclusive by adding other gender options.
-
Performance analysis: Without monitoring application performance (load, response time to endpoint calls, etc.), nothing objective can be said about performance. With logging, we can identify opportunities for improvement and detect potential risks before they become problems in production.
-
Audit: Logging can be a business requirement because it allows you to obtain information on certain decisions taken, events that have occurred, etc. For example, in a car’s software making automatic decisions, logging may be required by law to show the decisions made by the software (safety, liability). Without logging, this audit would be impossible.
2.2 When to log and for whom
For whom?
Logging is required for software applications. The people who directly benefit from it are:
- Software developers
- System administrators
- The Support Team
And indirectly, if these people do their job well thanks to the logs, the final beneficiaries are the customers and end users.
When to log? Six categories of situations
-
Exceptions and errors: When something goes wrong, we want to be able to reread the details later. This helps resolve a problem or address it before the customer notices it.
-
Adverse Events and States: Information about application events and states that are not errors but are worth tracing. This helps track the path of something that might go wrong later and can be used for performance analysis.
-
Debug info: Very similar to events and states, but with the specific intent of helping debug a problem in the program.
-
HTTP requests: Two possible cases:
- Incoming HTTP requests to our own application (request and response)
- Outgoing requests to an external API (request sent and response received)
-
Thread Usage: Useful for both troubleshooting (deadlock issues where two threads block each other by waiting indefinitely) and performance analysis (tracking active and available threads).
-
Front-end errors: In some situations, a front-end developer can call an error endpoint with the front-end log every time something goes wrong. This can be logged on the Java side on behalf of the front end, making it easier to track what happened in the end user experience and to more easily see if the problem is with the front end or the back end.
2.3 Case study: Carved Rock Fitness – order problem
Carved Rock Fitness is the hypothetical client used throughout this course. They have a problem with their online store: when you try to buy a product, the order is not sent. There is no error on the front-end side, but users cannot place orders after registration.
Without logging, it is impossible to know what is wrong.
The simulated Java SE application includes the following packages:
carvedrockfitness.user:User,UserController,UserRepository,UserService,UserStatuscarvedrockfitness.order:Order,OrderController,OrderRepository,OrderService,OrderStatuscarvedrockfitness.product:Product,ProductRepositorycarvedrockfitness.account:Accountcarvedrockfitness.CarvedRockFitness(main class withmain)
If we run the code without logging, we get an exception, but without context.
2.4 Added basic logging
To resolve this problem, we add basic logging. The procedure is as follows:
- Inside each relevant class, create a
private static final Logger - Obtain this logger with the static method
Logger.getLogger()by giving the name of the class - Use this logger throughout the class
Why static final?
static: we want the logger to belong to the class and not to an instance, and we do not want to create an object unnecessarilyfinal: we don’t want to have a duplicate logger, so we only create it once- Naming convention: as it is a constant, it must be written entirely in uppercase (
LOGGER)
Levels used in basic logging
Level.INFO: happy path, everything happens normallyLevel.WARNING: something went wrong but is not yet critical
After adding the basic logging, rereading the log we can see:
INFOmessages for the happy path- A
WARNING: “trying to place an order by a pending user” — revealing the cause of the problem: the user has the statusPENDINGand cannot place an order.
3. Adding a Logger and creating log messages
3.1 Overview of java.util.logging package
Since Java 1.4, Java has its own logging API. It is located in the java.util.logging package. This is the API that this course focuses on.
The logging process takes place as follows:
Code de l'application
|
v
[Logger] -- (filtrage optionnel)
|
v
[LogRecord] (créé si le message passe le filtre)
|
v
[Handler] -- (filtrage + formatage)
|
v
Destination (console, fichier, socket, etc.)
- Something in the code triggers a log event
- An instance of the
Loggerclass begins processing this event - Optionally, the logger filters the information
- If the information passes the filter, an instance of
LogRecordis created containing the data to be logged - The
LogRecordis sent to theHandler(s), who can filter and format the message before sending it to the destination
3.2 Log Levels
To distinguish the different types of logged events, log levels are defined. The level represents the severity of the situation. It is very important to use the correct level.
The 7 log levels in java.util.logging (from most to least serious)
| Level | Numerical value | Use |
|---|---|---|
SEVERE | 1000 | Serious error, the application may no longer be able to continue |
WARNING | 900 | Abnormal situation but the application can continue |
INFO | 800 | Informative message on the happy path |
CONFIG | 700 | Configuration Information |
FINE | 500 | High-level debugging information |
FINISH | 400 | More detailed debugging information |
FINEST | 300 | Very detailed debugging information |
There are also two special levels:
ALL: log everythingOFF: logs nothing
The logger knows the threshold level from which to start logging. For example, if the threshold is CONFIG, then CONFIG, INFO, WARNING and SEVERE will be logged, but FINE, FINER and FINEST will not.
Important note: By default, levels below INFO (FINE, FINER, FINEST, CONFIG) are not displayed in the log. This is due to the default level of the ConsoleHandler.
Example: change the level of certain logs
In the Carved Rock Fitness example, calls to endpoints (addUser, addOrder) are moved to the FINE level because they are low-level messages, while important situations in services remain in WARNING.
3.3 Log handlers
Handlers intercept LogRecord and process them before they are sent to the log. They can:
- Filter messages (each handler can have its own threshold level)
- Format messages (change message structure)
- Send message to appropriate destination
The ConsoleHandler is the default handler. It uses the INFO level as the default threshold. This is why the FINE level did not appear in the logs previously.
Available handler types
| Handler | Destination |
|---|---|
ConsoleHandler | Console (standard output) — default handler |
FileHandler | File — most common in production |
StreamHandler | Any OutputStream |
SocketHandler | Network socket |
MemoryHandler | Buffer memory |
Fix FINE level not displayed
To display FINE level messages, you must configure both the Logger and the Handler to the appropriate level. Indeed, the logger and each handler have their own level of filtering. If the logger level is more restrictive than the handler level, the message will never reach the handler.
static {
LOGGER.setLevel(Level.FINE); // Configurer le logger
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(Level.FINE); // Configurer le handler
LOGGER.addHandler(consoleHandler); // Ajouter le handler au logger
}
Important: Both levels must be configured. If the logger level is FINE but the handler level remains INFO, the FINE messages will still not pass.
3.4 The different log methods
There are three main logging methods, with many overloads:
1. log() – Basic method
LOGGER.log(Level.INFO, "Message informatif");
LOGGER.log(Level.WARNING, "Un avertissement avec détails: " + details);
Accepts a log level, a String type message, and optional parameters.
2. logp() – Log precise (log precise)
LOGGER.logp(Level.INFO, MyClass.class.getName(), "myMethod", "Message");
Similar to log() but also takes an explicit source class name and source method name. Without these parameters, the framework tries to guess them, which can be incorrect (especially with production JIT compilation). It is better to use logp() to avoid incorrect guesses.
3. logrb() – Log with Resource Bundle
LOGGER.logrb(Level.INFO, MyClass.class.getName(), "myMethod", bundle, "message.key");
Uses a ResourceBundle and localization parameters for log messages. This is a great option if localization needs to be applied to log messages.
Convenience Methods
There are also convenience methods allowing you to log directly at the right level:
LOGGER.info("Message informatif");
LOGGER.warning("Un avertissement");
LOGGER.severe("Une erreur grave");
LOGGER.fine("Message de débogage fin");
LOGGER.finer("Message de débogage plus fin");
LOGGER.finest("Message de débogage le plus fin");
4. Logging management and configuration
4.1 Logging to a file (FileHandler)
In production, we do not read the log from the console but from a log file. To log to a file, replace (or complete) the ConsoleHandler with a FileHandler.
try {
FileHandler fileHandler = new FileHandler("UserController.log");
fileHandler.setLevel(Level.FINE);
LOGGER.addHandler(fileHandler);
} catch (IOException e) {
e.printStackTrace();
}
Notes:
FileHandlercan throwIOExceptionexceptions (checked exception), so the creation must be surrounded by atry/catchblock- The log file is created in the specified directory
- By default, the
FileHandleruses theXMLFormatter(XML format) - The
ConsoleHandleruses theSimpleFormatterby default
4.2 Log formatters
Formatters change the structure of the log message. This is why the log format completely changed when we replaced the ConsoleHandler with a FileHandler.
Integrated formatters
| Format | Format | Default handler |
|---|---|---|
SimpleFormatter | Human readable text | ConsoleHandler |
XMLFormatter | Standard XML format | FileHandler |
Advantages and disadvantages
SimpleFormatter: Very readable for humans, but less easy to parse by automated toolsXMLFormatter: Slightly less readable for humans, but ideal for monitoring tools and dashboards that can read XML easily
Configure a formatter
fileHandler.setFormatter(new SimpleFormatter());
// ou
fileHandler.setFormatter(new XMLFormatter());
Customize the SimpleFormatter
You can customize the SimpleFormatter format via a system property:
# Exemple de format : <level>: <message> [<date/time>]
java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
4.3 Log Filters
A log filter allows you to finely control which messages are logged. To implement a filter, you must implement the Filter interface and its isLoggable() method.
Filter filterAll = new Filter() {
@Override
public boolean isLoggable(LogRecord record) {
return false; // Bloque tout (exemple pédagogique)
}
};
fileHandler.setFilter(filterAll);
isLoggable()returnstrueif the message should pass through the filterisLoggable()returnsfalseif the filter should block the message
Filters on the logger vs. filters on the handler
- A filter can be defined on the logger: applies to all handlers of this logger
- A filter can be defined on a handler: only applies to this particular handler
Since a logger can have several handlers, the filters on the handlers allow you to be more precise.
LogRecord properties that can be filtered
sourceClassName: name of the source classsourceMethodName: name of the source methodlevel: message levelmessage: content of the messagethreadID: thread identifier- And many other properties of
LogRecord
4.4 The LogManager and the configuration file
The LogManager
The LogManager is the class responsible for managing the logger hierarchy. It loads the configuration for all loggers. It is a singleton: it is only instantiated once and always returned as the same instance.
The LogManager can be configured via:
- A special configuration class
- A configuration properties file
Default configuration file
By default, the LogManager uses logging properties from $JRE_HOME/lib/logging.properties. You should never modify this file directly.
Good practice is to:
- Create a custom configuration file in the application (ex.
src/main/resources/logging.properties) - Read it via the
LogManagerwhen starting the application
LoggingUtil utility class
package carvedrockfitness.util;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.logging.LogManager;
public class LoggingUtil {
public static void initLogManager() {
try {
LogManager.getLogManager().readConfiguration(
new FileInputStream("src/main/resources/logging.properties")
);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Call in hand
public static void main(String[] args) throws IOException {
// Initialiser le LogManager en premier
LoggingUtil.initLogManager();
// ... reste du code
}
By initializing the LogManager in the main, the configuration applies to the entire application at once.
Full logging.properties file
handlers= java.util.logging.ConsoleHandler, java.util.logging.FileHandler
# Niveau global de tous les loggers et handlers
.level= INFO
# Propriétés spécifiques au FileHandler
java.util.logging.FileHandler.pattern = logs/carvedrockfitness%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
# Propriétés spécifiques au ConsoleHandler
java.util.logging.ConsoleHandler.level = OFF
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# Exemple de personnalisation du format SimpleFormatter
# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
Configuration file notes:
handlers=: lists active handlers separated by commas (several handlers possible).level=: default global level for all loggers and handlersjava.util.logging.FileHandler.pattern: path of the log file (%uis a unique number to avoid conflicts)java.util.logging.FileHandler.limit: maximum file size in bytes before rotationjava.util.logging.FileHandler.count: number of log files to keep- The
logs/folder must exist before running the application - Variables cannot be used in the configuration file. If variables are necessary, you must use a configuration class
Advantage of LogManager
Using the LogManager with a configuration file allows considerably cleaning up the code by removing all static {} configuration blocks in each class. All configuration is centralized in a single file.
5. Good logging practices
5.1 General good practices
1. Naming conventions
static finalfields must be entirely in UPPER CASE:LOGGER(and notlogorlogger)- Stick to naming conventions for class name and method name passed to
logp() - Use
MyClass.class.getName()for the class name (not a hardcoded string)
// ❌ Mauvaise pratique
private static final Logger log = Logger.getLogger("maclasse");
log.logp(Level.INFO, "namingconventionsloggers", "main method", "Message");
// ✅ Bonne pratique
private static final Logger LOGGER = Logger.getLogger(MyClass.class.getName());
LOGGER.logp(Level.INFO, MyClass.class.getName(), "main", "Message");
2. Log in a parseable format
The log must be readable both by humans and by automated tools (scripts, monitoring dashboards). Find a good balance between human readability and machine readability.
3. Keep in mind that logging can be used for other purposes
Logging is not only used for debugging. For example, during audits, certain decisions must be proven. When logging for other purposes, keep business language in mind to make the log more readable in that context.
4. Log OR rethrow the exception — not both
Never do both at the same time. If we log the exception and rerun it, the log will be polluted by the same cascading exceptions, or the stack trace will be unnecessarily propagated upwards.
// ❌ Mauvaise pratique : on logue ET on relance
try {
throw new FileSystemLoopException("Exception");
} catch (FileSystemLoopException e) {
LOGGER.logp(Level.SEVERE, MyClass.class.getName(), "main",
"FileSystemLoopException occurred: " + e.getStackTrace());
throw e; // ← on fait les deux !
}
// ✅ Bonne pratique : on fait l'un OU l'autre
// Option 1 : Seulement logger
try {
throw new FileSystemLoopException("Exception");
} catch (FileSystemLoopException e) {
LOGGER.logp(Level.SEVERE, MyClass.class.getName(), "main",
"FileSystemLoopException occurred: " + e.getStackTrace());
// Ne pas relancer
}
// Option 2 : Seulement relancer (pour que l'appelant gère)
try {
throw new FileSystemLoopException("Exception");
} catch (FileSystemLoopException e) {
throw e; // L'appelant loggera
}
5.2 Best practices for log messages
1. Be specific: answer the Who, When, Where, What and Result questions
// ❌ Message trop vague
"User logged in"
// ✅ Message précis
"John123 with ID 12 logged in successfully to MyApp on April 8th, 2022"
The log message must answer the questions:
- Who: the user or entity concerned
- When: timestamp (often managed automatically by the framework)
- Where: the class and the method (via
logp()) - What: what happened
- Result: success or failure
2. Be precise but not verbose
Don’t add too much unnecessary information, but make sure you have enough for errors and exceptions. For errors and exceptions, it is advisable to include the stack trace to locate precisely where the problem occurred.
3. Use the correct log level
If something is a warning, don’t log it with SEVERE. If something is an error, don’t log it with INFO. Using the right level will make it much easier to navigate the log to resolve an urgent customer issue.
| Location | Recommended level |
|---|---|
| Serious error preventing application from continuing | SEVERE |
| Abnormal but recoverable situation | WARNING |
| Happy path, normal events | INFO |
| Configuration Information | CONFIG |
| General debugging information | FINE |
| Detailed debugging information | FINISH |
| Very detailed debugging information | FINEST |
5.3 Logging and security
1. Do not store sensitive information in the log
The log is often on an external system that is less under control. It is accessible by many people for many reasons. Never log sensitive information.
Examples of sensitive information:
- Passwords (neither encrypted nor in clear text)
- Authentication tokens
- Social Security Numbers
- Personally Identifiable Information (PII)
- Encrypted data
// ❌ Mauvaise pratique : on logue le mot de passe
public boolean saveUser(String username, String password) {
LOGGER.log(Level.INFO, "Signing up user with " + username + " and password " + password);
// ...
}
// ✅ Bonne pratique : on ne logue que les informations non sensibles
public boolean saveUser(String username, String password) {
LOGGER.log(Level.INFO, "Signing up user with username: " + username);
// ...
}
Always check with your customer or company what you are authorized to log and what you are not.
2. Avoid code injection (Code Injection)
It is not uncommon for what we want to log to come from an external source (query parameters, user inputs). Always sanitize entries before using them in a log message, as malicious code could be accidentally executed.
This is one of the issues for which certain logging libraries have been known (notably Log4j with the Log4Shell vulnerability in 2021).
// ❌ Mauvaise pratique : entrée non sanitisée
public boolean saveUser(String username) {
LOGGER.log(Level.INFO, "Signing up user with " + username);
// username pourrait contenir du code malveillant
}
// ✅ Bonne pratique : sanitiser l'entrée
public boolean saveUser(String username) {
// Remplacer les caractères spéciaux, ne garder que les alphanumériques
String sanitizedUsername = username.replaceAll("[^a-zA-Z0-9]", "");
LOGGER.log(Level.INFO, "Signing up user with " + sanitizedUsername);
}
3. Use a safe location for the log file
The log file should ideally be on an external system separate from the application. If the log is on the same server as the application and the log is compromised, the application could be compromised too.
4. Set a maximum size for the log file
If someone finds an entry point to inflate the log file, the server may become slow or unusable. The java.util.logging library has a maximum size of 1 MB by default, but it is better to set it explicitly.
// Exemple programmatique
FileHandler fileHandler = new FileHandler("app.log", 50000, 1);
// 50 000 octets max, 1 fichier
// Exemple dans logging.properties
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
As soon as the limit is exceeded, a new log file is created (log rotation).
5. Backup the logs (Backup)
Logs can be lost for any reason. Without a backup, we no longer have any trace of what happened at a given moment. This makes it impossible to troubleshoot and provide logs for an audit.
6. External logging libraries
6.1 Common libraries
Although this course focuses on java.util.logging, there are more common alternatives:
| Library | Description |
|---|---|
| Log4j | Very popular despite old security vulnerabilities (now fixed). Very present in existing enterprise applications. |
| SLF4J | Simple Logging Facade for Java — an abstraction of logging. Allows you to choose the actual implementation at deployment time. |
| Logback | Successor to the Log4j project. High performance, more configuration options, excellent for old archived log files. |
| Apache Commons Logging (JCL) | Another Apache logging front. |
| tinylog | Lightweight logging library. |
6.2 The Log4j library
Advantages of Log4j
-
Asynchronous logging: In an enterprise environment, logging can become a bottleneck. Log4j allows logging asynchronously, significantly improving performance.
-
Lazy Evaluation: If the log level is too low, the log message is not constructed. This avoids unnecessary calculations for messages that will not be logged anyway.
-
Advanced filtering: Filtering based on context markers, regular expressions, and many other elements of the log event.
-
Garbage-Free Mode: By default in standalone applications, Log4j avoids allocating objects unnecessarily and reuses existing objects, reducing pressure on the garbage collector (which is costly in terms of performance). Note: this mode is different for web applications (risk of memory leaks), but it is configurable.
-
Configurable output formats and styles: As with
java.util.logging. -
Native Java Exception Handling: Log4j is designed to handle Java exceptions from the ground up.
Three main elements of Log4j
| Element | Role | Equivalent to java.util.logging |
|---|---|---|
| Logger | Create the log | Logger |
| Append | Log writing destination (file, console, etc.) | Handler |
| Layout | Output format | Formatter |
Log4j code example (demo)
<!-- pom.xml - Dépendance Log4j -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.1</version> <!-- Toujours utiliser la dernière version -->
</dependency>
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Log4jExample {
// Utilisation du LogManager d'Apache (et non de java.util.logging)
private static final Logger logger = LogManager.getLogger(Log4jExample.class.getName());
public static void main(String[] args) {
logger.info("Message d'information"); // Par défaut, non affiché (niveau trop bas)
logger.error("Message d'erreur"); // Par défaut, le niveau minimum affiché est ERROR
// On peut aussi inclure une exception dans le log
try {
throw new java.io.IOException("Exemple d'exception");
} catch (java.io.IOException e) {
logger.error("IOException survenue", e);
}
}
}
Note: By default in Log4j, the minimum level logged is ERROR. We can of course configure this. Log4j levels are: TRACE, DEBUG, INFO, WARN, ERROR, FATAL (slightly different from java.util.logging).
6.3 The SLF4J library
The problem that SLF4J solves
Imagine an application that depends on Log4j for its logging. At some point, we decide to move to java.util.logging. Without SLF4J, you would need:
- Modify all logging code (imports, method calls)
- Update dependencies
SLF4J (Simple Logging Façade for Java) is a facade (abstraction layer) that allows your code not to depend on a specific logging implementation.
Avec SLF4J :
Code de l'application
|
v (dépend uniquement de)
[SLF4J]
|
v (peut utiliser n'importe lequel)
Log4j / java.util.logging / Logback / ...
If we want to change the logging implementation, we update only the implementation dependency, without touching the application code.
Maven dependencies for SLF4J
<!-- SLF4J (façade - toujours nécessaire) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.0</version>
</dependency>
<!-- Implémentation : java.util.logging (JDK 1.4) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>2.0.0</version>
</dependency>
<!-- OU : Implémentation Logback (supporte SLF4J nativement) -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.0</version>
</dependency>
Important: SLF4J alone cannot log. If you don’t add an implementation you will get a “No SLF4J providers were found” error.
SLF4J code example
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SLF4JExample {
// LoggerFactory provient de SLF4J (et non de java.util.logging ou Log4j)
private static final Logger logger = LoggerFactory.getLogger(SLF4JExample.class.getName());
public static void main(String[] args) {
logger.info("Message d'information");
logger.error("Message d'erreur");
}
}
The code remains the same regardless of the logging implementation used (Log4j, java.util.logging, Logback). Only the dependency changes in the pom.xml.
6.4 Checking for vulnerabilities
Checking for vulnerabilities is always important when using dependencies, but even more so for logging libraries. Care should be taken not to use dependencies with known vulnerabilities.
The Log4Shell vulnerability (Log4j, December 2021)
In December 2021, several widespread critical vulnerabilities were discovered in Log4j. The solution was simple: update the library to version 2.17.1 or higher, which contains no known vulnerabilities.
Manual verification via Sonatype OSS Index
- Connect to the Sonatype OSS Index (oss.sonatype.org)
- Go to the Maven ecosystem
- Search for
log4j-core - Check versions and known vulnerabilities
Automated verification (highly recommended)
For production applications, it is strongly recommended to automate vulnerability checking. With each build, tools can analyze dependencies and issue warnings for critical vulnerabilities.
Example tools:
- Sonatype Lift: installs and analyzes pull requests automatically
- OWASP Dependency Check: Maven/Gradle plugin
- Snyk: dependency security analysis service
- GitHub Dependabot: integrated with GitHub, monitors vulnerabilities
Key principle: Always use the latest stable version of your logging libraries, and regularly check for new announced vulnerabilities.
7. Code Reference – Case Carved Rock Fitness
7.1 Project structure
The demo project follows a layered architecture typical of a Java SE application. It simulates an online store with user and order management.
carvedrockfitness/
├── CarvedRockFitness.java (classe principale - main)
├── account/
│ └── Account.java
├── order/
│ ├── Order.java
│ ├── OrderController.java
│ ├── OrderRepository.java
│ ├── OrderService.java
│ └── OrderStatus.java (enum)
├── product/
│ ├── Product.java
│ └── ProductRepository.java
├── user/
│ ├── User.java
│ ├── UserController.java
│ ├── UserRepository.java
│ ├── UserService.java
│ └── UserStatus.java (enum)
└── util/
└── LoggingUtil.java (ajouté au module 4)
Data model
UserStatus (enum):
public enum UserStatus {
ACTIVE, PENDING, BLOCKED, INACTIVE, DELETED
}
OrderStatus (enum):
public enum OrderStatus {
RECEIVED, ACCEPTED, IN_PROGRESS, CANCELLED, COMPLETED;
}
User:
public class User {
private long id;
private String userName;
private String email;
private LocalDateTime dateCreated;
private UserStatus userStatus;
// constructeurs, getters, setters...
@Override
public String toString() {
return User.class.getName() + " - name: " + userName
+ ", id: " + id + ", email: " + email;
}
}
Order:
public class Order {
private long id;
private User user;
private List<Product> products;
private LocalDateTime orderDateTime;
private OrderStatus orderStatus;
// constructeurs, getters, setters...
}
Maven configuration (pom.xml)
<project>
<groupId>com.pluralsight</groupId>
<artifactId>java-core-libraries-log-system</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<mainClass>carvedrockfitness.CarvedRockFitness</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
7.2 Module 2 – Application without logging
The original application, without any logging. Unable to diagnose the problem (an exception is thrown silently).
OrderService.java (without logging):
package carvedrockfitness.order;
import carvedrockfitness.user.User;
import carvedrockfitness.user.UserRepository;
import carvedrockfitness.user.UserStatus;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
public class OrderService {
private OrderRepository orderRepository = new OrderRepository();
public boolean addOrder(Order order) {
if (order.getOrderDateTime().isAfter(LocalDateTime.now())) {
try {
throw new Exception("Can't place an Order in the future!");
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
if (order.getProducts().size() < 1) {
try {
throw new Exception("Order must consist of at least one Product!");
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
if (order.getUser().getUserStatus() == UserStatus.BLOCKED) {
try {
throw new Exception("Order cannot be placed by blocked User!");
} catch (Exception e) {
e.printStackTrace();
return false;
}
} else if (order.getUser().getUserStatus() == UserStatus.PENDING) {
try {
throw new Exception("Order cannot be placed by pending User!");
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
return orderRepository.save(order);
}
}
7.3 Module 2 – Application with basic logging
After adding the base logging, we can see in the logs why the command fails.
CarvedRockFitness.java (main):
package carvedrockfitness;
import carvedrockfitness.order.Order;
import carvedrockfitness.order.OrderController;
import carvedrockfitness.product.ProductRepository;
import carvedrockfitness.user.User;
import carvedrockfitness.user.UserController;
import carvedrockfitness.user.UserStatus;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
public class CarvedRockFitness {
public static void main(String[] args) throws IOException {
// Simulation d'un utilisateur passant une commande
// Ajout d'un utilisateur (avec statut PENDING → cause du problème)
User user = new User(4, "Maaike", "maaike@maaike.nl",
LocalDateTime.now(), UserStatus.PENDING);
UserController userController = new UserController();
userController.addUser(user);
// Tentative de passer une commande
Order order = new Order(10, user,
List.of(ProductRepository.getDummyDataList().get(0)),
LocalDateTime.of(2021, 11, 3, 0, 0));
OrderController orderController = new OrderController();
orderController.addOrder(order);
}
}
UserController.java (with basic logging):
package carvedrockfitness.user;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
public class UserController {
private static final UserService userService = new UserService();
// Logger static final : appartient à la classe, créé une seule fois
private static final Logger LOGGER = Logger.getLogger(UserController.class.getName());
public boolean addUser(User user) {
LOGGER.log(Level.INFO, "In endpoint for adding user, with these user details: " + user);
return userService.addUser(user);
}
// ...
}
OrderController.java (with basic logging):
package carvedrockfitness.order;
import java.util.logging.Level;
import java.util.logging.Logger;
public class OrderController {
private static final OrderService orderService = new OrderService();
private static final Logger LOGGER = Logger.getLogger(OrderController.class.getName());
public boolean addOrder(Order order) {
LOGGER.log(Level.INFO, "At the endpoint order, with order details: " + order);
return orderService.addOrder(order);
}
// ...
}
UserService.java (with basic logging):
package carvedrockfitness.user;
import java.time.LocalDateTime;
import java.util.logging.Level;
import java.util.logging.Logger;
public class UserService {
private final UserRepository userRepository = new UserRepository();
private static final Logger LOGGER = Logger.getLogger(UserService.class.getName());
public boolean addUser(User user) {
if (user.getDateCreated().isAfter(LocalDateTime.now())) {
// WARNING : situation anormale
LOGGER.log(Level.WARNING,
"Trying to create a user with a creation date that's in the future. User details: " + user);
try {
throw new Exception("Can't have dateCreated in the future!");
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// INFO : chemin heureux
LOGGER.log(Level.INFO, "Adding user, with user details: " + user);
return userRepository.save(user);
}
// ...
}
OrderService.java (with basic logging):
package carvedrockfitness.order;
import java.time.LocalDateTime;
import java.util.logging.Level;
import java.util.logging.Logger;
public class OrderService {
private OrderRepository orderRepository = new OrderRepository();
private static final Logger LOGGER = Logger.getLogger(OrderService.class.getName());
public boolean addOrder(Order order) {
if (order.getOrderDateTime().isAfter(LocalDateTime.now())) {
LOGGER.log(Level.WARNING,
"Trying to place an order in the future, order details: " + order);
try {
throw new Exception("Can't place an Order in the future!");
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
if (order.getProducts().size() < 1) {
LOGGER.log(Level.WARNING,
"Trying to place an order with no products, order details: " + order);
try {
throw new Exception("Order must consist of at least one Product!");
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
if (order.getUser().getUserStatus() == UserStatus.BLOCKED) {
LOGGER.log(Level.WARNING,
"Trying to place an order by a blocked user, order details: " + order);
try {
throw new Exception("Order cannot be placed by blocked User!");
} catch (Exception e) {
e.printStackTrace();
return false;
}
} else if (order.getUser().getUserStatus() == UserStatus.PENDING) {
// Ce WARNING révèle la cause du problème Carved Rock Fitness
LOGGER.log(Level.WARNING,
"Trying to place an order by a pending user, order details: " + order);
try {
throw new Exception("Order cannot be placed by pending User!");
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// INFO : chemin heureux
LOGGER.log(Level.INFO, "Placing an order, order details: " + order);
return orderRepository.save(order);
}
// ...
}
Result in the console: We see the INFO messages for the happy path, then the WARNING “Trying to place an order by a pending user” which reveals the cause of the problem.
7.4 Module 3 – Configuring a ConsoleHandler with FINE level
To see FINE level messages (not displayed by default), you must explicitly configure the logger and the handler:
UserController.java (Module 3 – end):
package carvedrockfitness.user;
import java.util.List;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
public class UserController {
private final UserService userService = new UserService();
private static final Logger LOGGER = Logger.getLogger(UserController.class.getName());
// Bloc static pour configurer le logger et son handler
static {
LOGGER.setLevel(Level.FINE); // 1. Configurer le Logger
ConsoleHandler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(Level.FINE); // 2. Configurer le Handler
LOGGER.addHandler(consoleHandler); // 3. Ajouter le handler au logger
}
public boolean addUser(User user) {
// Niveau FINE pour le message d'endpoint (basse priorité)
LOGGER.log(Level.FINE, "In endpoint for adding user, with these user details: " + user);
return userService.addUser(user);
}
// ...
}
Why the two levels? The Logger and the Handler each have their own filtering level. If the Logger is at FINE but the Handler remains at INFO (default value), FINE messages will not pass to the handler.
7.5 Module 4 – Final application with LogManager and configuration file
logging.properties (in src/main/resources/)
handlers= java.util.logging.ConsoleHandler, java.util.logging.FileHandler
# Niveau global
.level= INFO
# FileHandler : écriture dans un fichier XML
java.util.logging.FileHandler.pattern = logs/carvedrockfitness%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter
# ConsoleHandler : désactivé (niveau OFF)
java.util.logging.ConsoleHandler.level = OFF
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
# Personnalisation optionnelle du SimpleFormatter :
# java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
LoggingUtil.java
package carvedrockfitness.util;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.logging.LogManager;
public class LoggingUtil {
public static void initLogManager() {
try {
LogManager.getLogManager().readConfiguration(
new FileInputStream("src/main/resources/logging.properties")
);
} catch (IOException e) {
e.printStackTrace();
}
}
}
CarvedRockFitness.java (final main – Module 4)
package carvedrockfitness;
import carvedrockfitness.order.Order;
import carvedrockfitness.order.OrderController;
import carvedrockfitness.product.ProductRepository;
import carvedrockfitness.user.User;
import carvedrockfitness.user.UserController;
import carvedrockfitness.user.UserStatus;
import carvedrockfitness.util.LoggingUtil;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.List;
public class CarvedRockFitness {
public static void main(String[] args) throws IOException {
// Initialiser le LogManager en premier (point d'entrée unique)
LoggingUtil.initLogManager();
// Simuler l'ajout d'un utilisateur avec statut PENDING
User user = new User(4, "Maaike", "maaike@maaike.nl",
LocalDateTime.now(), UserStatus.PENDING);
UserController userController = new UserController();
userController.addUser(user);
// Simuler une tentative de commande
Order order = new Order(10, user,
List.of(ProductRepository.getDummyDataList().get(0)),
LocalDateTime.of(2021, 11, 3, 0, 0));
OrderController orderController = new OrderController();
orderController.addOrder(order);
}
}
UserController.java (Module 4 – final, without static block)
package carvedrockfitness.user;
import java.util.List;
import java.util.logging.*;
public class UserController {
private final UserService userService = new UserService();
// Plus besoin de bloc static : le LogManager configure tout depuis logging.properties
private static final Logger LOGGER = Logger.getLogger(UserController.class.getName());
public boolean addUser(User user) {
LOGGER.log(Level.FINE, "In endpoint for adding user, with these user details: " + user);
return userService.addUser(user);
}
// ...
}
Advantage: no more static {} configuration blocks in each class. Everything is centralized in logging.properties via the LogManager.
7.6 Module 5 – Good Code Practices
NamingConventionsLoggers.java – Naming conventions
package general;
import java.util.logging.Level;
import java.util.logging.Logger;
public class NamingConventionsLoggers {
// ❌ Mauvaise pratique : "log" en minuscules, classe incorrecte passée à getLogger
// private static final Logger log = Logger.getLogger(DealingWithExceptionsAndLogging.class.getName());
// ✅ Bonne pratique : LOGGER en MAJUSCULES, nom de la bonne classe
private static final Logger LOGGER = Logger.getLogger(NamingConventionsLoggers.class.getName());
public static void main(String[] args) {
// ❌ Mauvaise pratique : nom de classe et méthode codés en dur incorrectement
// LOGGER.logp(Level.INFO, "namingconventionsloggers", "main method", "In the main method.");
// ✅ Bonne pratique : utiliser le nom réel de la classe et de la méthode
LOGGER.logp(Level.INFO, NamingConventionsLoggers.class.getName(), "main", "In the main method.");
}
}
DealingWithExceptionsAndLogging.java – Log OR restart, not both
package general;
import java.nio.file.FileSystemLoopException;
import java.util.logging.Level;
import java.util.logging.Logger;
public class DealingWithExceptionsAndLogging {
private static final Logger LOGGER = Logger.getLogger(
DealingWithExceptionsAndLogging.class.getName());
public static void main(String[] args) throws FileSystemLoopException {
try {
throw new FileSystemLoopException("One of my favorite exceptions");
} catch (FileSystemLoopException e) {
// Logger l'exception
LOGGER.logp(Level.SEVERE,
DealingWithExceptionsAndLogging.class.getName(),
"main",
"FileSystemLoopException occurred during request processing: "
+ e.getStackTrace());
// ❌ Mauvaise pratique : throw e; // Ne pas faire les deux !
// Choisir : soit logger, soit relancer — pas les deux
}
}
}
SensitiveLogging.java – Do not log sensitive information
package security;
import java.util.logging.Level;
import java.util.logging.Logger;
public class SensitiveLogging {
private static final Logger LOGGER = Logger.getLogger(SensitiveLogging.class.getName());
// ❌ Mauvaise pratique : on logue le mot de passe
public boolean saveUser(String username, String password) {
LOGGER.log(Level.INFO,
"Signing up user with " + username + " and password " + password);
return true;
}
// ✅ Bonne pratique : ne logguer que les informations non sensibles
public boolean saveUserSafe(String username, String password) {
LOGGER.log(Level.INFO, "Signing up user with username: " + username);
// Ne jamais logger le mot de passe
return true;
}
}
LoggingInjection.java – Sanitize entries before logging
package security;
import java.util.logging.Level;
import java.util.logging.Logger;
public class LoggingInjection {
private static final Logger LOGGER = Logger.getLogger(LoggingInjection.class.getName());
// ❌ Mauvaise pratique : entrée utilisateur non sanitisée
public boolean saveUser(String username) {
LOGGER.log(Level.INFO, "Signing up user with " + username);
// username pourrait contenir ${jndi:ldap://...} ou autre code malveillant
return true;
}
// ✅ Bonne pratique : sanitiser l'entrée avant de la logger
public boolean saveUserSafe(String username) {
// Ne garder que les caractères alphanumériques
String sanitizedUsername = username.replaceAll("[^a-zA-Z0-9]", "");
LOGGER.log(Level.INFO, "Signing up user with " + sanitizedUsername);
return true;
}
}
LoggingUtil.java (Module 5) – Maximum log file size
package util;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.logging.LogManager;
public class LoggingUtil {
// Méthode d'initialisation du LogManager avec gestion explicite des exceptions
public static void initLogManager() throws IOException {
LogManager.getLogManager().readConfiguration(
new FileInputStream("./src/main/resources/logging.properties")
);
}
}
The logging.properties file in module 5 sets a maximum size of 50,000 bytes (50 KB) for the log file via java.util.logging.FileHandler.limit = 50000, which prevents the log file from becoming too large and impacting server performance.
8. Summary
| Module | Main topic | Skill acquired |
|---|---|---|
| 1 | Overview | Understand why logging is essential |
| 2 | Logging Basics | Create a Logger, use Level.INFO and Level.WARNING |
| 3 | Levels and handlers | Configure levels, add a ConsoleHandler, use logp() |
| 4 | FileHandler, Formatter, Filter, LogManager | Log to a file, configure via logging.properties |
| 5 | Best practices | Naming, security, precise messages, exception management |
| 6 | External Libraries | Log4j, SLF4J, vulnerability checking |
Key Takeaways
- One
Loggerper class:private static final Logger LOGGER = Logger.getLogger(MyClass.class.getName()); - Two levels to configure: that of the
LoggerAND that of theHandler - Centralize configuration with
LogManagerandlogging.properties - Choose the right level: neither too serious nor too light
- Log OR rethrow exceptions — never both
- Never log passwords, tokens or sensitive data
- Sanitize the entries before logging them to avoid injection
- Set a maximum size for log files
- SLF4J decouples the code from the logging implementation
- Check for vulnerabilities in logging dependencies regularly
Search Terms
logging · management · java · se · backend · architecture · full-stack · web · log · configuration · log4j · level · logmanager · slf4j · application · levels · advantages · carved · case · conventions · filters · fine · fitness · formatters