Advanced

Logging and Management in Java SE

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 informati...

Level: Intermediate | Java SE 17


Table of Contents

  1. Course Overview
  2. What is Logging and how to use it
  1. Adding a Logger and creating log messages
  1. Logging management and configuration
  1. Best logging practices
  1. External logging libraries
  1. Code reference – Cas Carved Rock Fitness

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 LogManager and 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:

  1. 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.

  1. 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.

  2. 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

  1. 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.

  2. 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.

  3. Debug info: Very similar to events and states, but with the specific intent of helping debug a problem in the program.

  4. 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)
  1. 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).

  2. 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, UserStatus
  • carvedrockfitness.order: Order, OrderController, OrderRepository, OrderService, OrderStatus
  • carvedrockfitness.product: Product, ProductRepository
  • carvedrockfitness.account: Account
  • carvedrockfitness.CarvedRockFitness (main class with main)

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:

  1. Inside each relevant class, create a private static final Logger
  2. Obtain this logger with the static method Logger.getLogger() by giving the name of the class
  3. 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 unnecessarily
  • final: 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 normally
  • Level.WARNING: something went wrong but is not yet critical

After adding the basic logging, rereading the log we can see:

  • INFO messages 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 status PENDING and 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.)
  1. Something in the code triggers a log event
  2. An instance of the Logger class begins processing this event
  3. Optionally, the logger filters the information
  4. If the information passes the filter, an instance of LogRecord is created containing the data to be logged
  5. The LogRecord is sent to the Handler(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)

LevelNumerical valueUse
SEVERE1000Serious error, the application may no longer be able to continue
WARNING900Abnormal situation but the application can continue
INFO800Informative message on the happy path
CONFIG700Configuration Information
FINE500High-level debugging information
FINISH400More detailed debugging information
FINEST300Very detailed debugging information

There are also two special levels:

  • ALL: log everything
  • OFF: 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

HandlerDestination
ConsoleHandlerConsole (standard output) — default handler
FileHandlerFile — most common in production
StreamHandlerAny OutputStream
SocketHandlerNetwork socket
MemoryHandlerBuffer 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:

  • FileHandler can throw IOException exceptions (checked exception), so the creation must be surrounded by a try/catch block
  • The log file is created in the specified directory
  • By default, the FileHandler uses the XMLFormatter (XML format)
  • The ConsoleHandler uses the SimpleFormatter by 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

FormatFormatDefault handler
SimpleFormatterHuman readable textConsoleHandler
XMLFormatterStandard XML formatFileHandler

Advantages and disadvantages

  • SimpleFormatter: Very readable for humans, but less easy to parse by automated tools
  • XMLFormatter: 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() returns true if the message should pass through the filter
  • isLoggable() returns false if 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 class
  • sourceMethodName: name of the source method
  • level: message level
  • message: content of the message
  • threadID: 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:

  1. Create a custom configuration file in the application (ex. src/main/resources/logging.properties)
  2. Read it via the LogManager when 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 handlers
  • java.util.logging.FileHandler.pattern: path of the log file (%u is a unique number to avoid conflicts)
  • java.util.logging.FileHandler.limit: maximum file size in bytes before rotation
  • java.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 final fields must be entirely in UPPER CASE: LOGGER (and not log or logger)
  • 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.

LocationRecommended level
Serious error preventing application from continuingSEVERE
Abnormal but recoverable situationWARNING
Happy path, normal eventsINFO
Configuration InformationCONFIG
General debugging informationFINE
Detailed debugging informationFINISH
Very detailed debugging informationFINEST

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:

LibraryDescription
Log4jVery popular despite old security vulnerabilities (now fixed). Very present in existing enterprise applications.
SLF4JSimple Logging Facade for Java — an abstraction of logging. Allows you to choose the actual implementation at deployment time.
LogbackSuccessor to the Log4j project. High performance, more configuration options, excellent for old archived log files.
Apache Commons Logging (JCL)Another Apache logging front.
tinylogLightweight logging library.

6.2 The Log4j library

Advantages of Log4j

  1. Asynchronous logging: In an enterprise environment, logging can become a bottleneck. Log4j allows logging asynchronously, significantly improving performance.

  2. 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.

  3. Advanced filtering: Filtering based on context markers, regular expressions, and many other elements of the log event.

  4. 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.

  5. Configurable output formats and styles: As with java.util.logging.

  6. Native Java Exception Handling: Log4j is designed to handle Java exceptions from the ground up.

Three main elements of Log4j

ElementRoleEquivalent to java.util.logging
LoggerCreate the logLogger
AppendLog writing destination (file, console, etc.)Handler
LayoutOutput formatFormatter

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:

  1. Modify all logging code (imports, method calls)
  2. 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

  1. Connect to the Sonatype OSS Index (oss.sonatype.org)
  2. Go to the Maven ecosystem
  3. Search for log4j-core
  4. Check versions and known vulnerabilities

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

ModuleMain topicSkill acquired
1OverviewUnderstand why logging is essential
2Logging BasicsCreate a Logger, use Level.INFO and Level.WARNING
3Levels and handlersConfigure levels, add a ConsoleHandler, use logp()
4FileHandler, Formatter, Filter, LogManagerLog to a file, configure via logging.properties
5Best practicesNaming, security, precise messages, exception management
6External LibrariesLog4j, SLF4J, vulnerability checking

Key Takeaways

  1. One Logger per class: private static final Logger LOGGER = Logger.getLogger(MyClass.class.getName());
  2. Two levels to configure: that of the Logger AND that of the Handler
  3. Centralize configuration with LogManager and logging.properties
  4. Choose the right level: neither too serious nor too light
  5. Log OR rethrow exceptions — never both
  6. Never log passwords, tokens or sensitive data
  7. Sanitize the entries before logging them to avoid injection
  8. Set a maximum size for log files
  9. SLF4J decouples the code from the logging implementation
  10. 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.