Baeldung Pro – Kotlin – NPI EA (cat = Baeldung on Kotlin)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Overview

In this article, we’ll explore how to integrate and use the Apache Log4j Kotlin library. Log4j is a robust logging framework widely used in the Java ecosystem composed of an API, its implementation, and components that assist the deployment of various use cases. The library provides a Kotlin-friendly interface to the Log4j API.

2. Why Log4j?

Although the discovery of the Log4Shell vulnerability (CVE-2021-44228) has shaken its reputation, it remains widely adopted, providing powerful features such as asynchronous logging, JSON support, and a highly configurable architecture.

Using the Log4j framework allows us to filter logs based on configurable levels such as DEBUG, INFO, or ERROR without needing to change our code.

It also provides the flexibility to direct logs to various outputs like files, databases, or remote systems rather than being limited to the console.

3. Dependencies

To set up Log4j in a Kotlin project, we need to include the dependencies for both the API and the Log4j backend:

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api-kotlin</artifactId>
     <version>1.5.0</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.20.0</version>
</dependency>

4. Creating Loggers

With the library imported, let’s look at some common ways to create loggers.

4.1. Logger in Companion Objects

The traditional approach is to use a companion object that all instances of the class can share:

class Game {
    companion object {
        private val logger = logger()
    }

    fun startGame() {
        logger.info { "Game is starting..." }
    }
}

An alternative would be to extend the companion object from the Logging class:

class Game {
    companion object: Logging

    fun startGame() {
        logger.info { "Game is starting..." }
    }
}

While both methods are straightforward, the traditional approach is more efficient as the logger lookup is performed once. The alternative, which extends the class, adds extra overhead due to the logger lookup involved at runtime.

When we run the code, we’ll notice that there’s no output, and that’s because we need to specify the log level:

Configurator.setRootLevel(Level.DEBUG)

In the next chapter, we’ll expand on this topic and the common way to set up an external configuration file, allowing us to keep our code free from hardcoded log-level management.

4.2. Logger in Class

Alternatively, we can define the logger within the class:

class Player(val name: String) {
    private val logger = logger()

    fun joinGame() {
        logger.info { "Player '$name' has joined the game." }
    }

    fun takeDamage(amount: Int) {
        logger.warn { "Player '$name' took $amount damage!" }
    }
}

Although not recommended, this approach creates a logger instance for each class instance, which may be suitable in certain scenarios.

When creating the logger in the class, we can pass it a name parameter, ensuring that when the code is run, we’ll know exactly which logger was used:

val logger = logger("Player Action")

In this case, the output will contain our defined name instead of the class name.

4.3. Using the Logging Interface

We could also use the Logging interface, offering us a quick and convenient way to access the logger.

Let’s create a utility class that implements it:

class GameUtil : Logging {
    fun logEvent(event: String) {
        logger.info { "Game Event: $event" }
    }
}

When using this method, we effectively create a single Logger instance that is exclusively associated with the class instance and not shared among other instances.

Let’s look at a usage example:

val util = GameUtil()
util.logEvent("Level 1 Loaded")

4.4. Using the Logger Extension Property

As a substitute, we can use the logger extension property to dynamically inject a logger on the spot:

class Enemy {
    fun shoot() {
        logger.warn { "Enemy shoots the player" }
    }
}

In this case, the logger will look up the associated Logger instance for the encapsulating class, adding extra overhead at runtime.

5. Logger Configuration

Now that we’ve looked at the most common ways to use Log4j for Kotlin let’s check out how to configure it for our needs.

As mentioned in the previous chapter, the log level has been set to debug by hardcoding it. Now, we can create a configuration file named log4j2.xml in our resources folder, which the logger can use:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="ConsoleAppender" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="ConsoleAppender"/>
        </Root>
    </Loggers>
</Configuration>

This configuration defines a console appender with a specific pattern layout, ensuring that all log messages are output with a timestamp, thread information, log level, and message.

Additionally, we’re setting the log level to info. Therefore, the output will display only the messages of log level info, warn, error, and fatal, omitting the debug and trace levels.

The seven log levels provided help us separate the type of events that occur when our program is running, thus resulting in a hierarchy.

When set to all, for example, the log level will output all logger messages, while setting it to error will only display logged errors and fatals. Log level all is a special marker that’s rarely used in application code.

6. Best Practices

To maximize the effectiveness of logging in our Kotlin applications, we should follow these best practices consistently.

In most applications, we should create a single logger per class definition rather than per instance. This approach prevents redundant fields and clearly conveys the static binding to the class.

We should use the appropriate log level that corresponds to the severity of the events, such as:

  • trace to capture detailed information about the application’s behavior
  • debug for detailed information during debugging;
  • info for general operational messages;
  • warn for indication of potential issues;
  • error for error events that might still allow the application to continue running;
  • fatal for severe errors causing premature termination.

Additionally, we must ensure that sensitive data, such as passwords, personal information, or tokens, is never logged to prevent security vulnerabilities.

Our codebase should maintain a consistent log format across applications, which is essential for easier parsing and analysis. To achieve this, we define a standard pattern in the configuration and consistently follow it throughout.

7. Conclusion

In this article, we’ve explored multiple ways of creating loggers using the Kotlin Log4j library.

To begin, we tackled the differences between creating class-based loggers versus instance-based ones, as well as using the Logging interface and extension property.

Moving forward, we went through the configuration file, avoiding hardcoded log-level settings, and checked out some of the best practices when working with loggers.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.