1. Introduction

In this tutorial, we’ll take a look at a few logging idioms that fit typical Kotlin programming styles.

2. Logging Idioms

Logging is a ubiquitous need in programming. While apparently a simple idea (just print stuff!), there are many ways to do it.

In fact, every language, operating system, and the environment has its own idiomatic and sometimes idiosyncratic logging solution; often, actually, more than one.

Here, we’ll focus on Kotlin’s logging story.

We’ll also use logging as a pretext for diving into some advanced Kotlin features and exploring their nuances.

3. Setup

For the code examples, we’ll use the SLF4J library, but the same patterns and solutions apply to Log4J, JUL, and other logging libraries.

So, let’s begin by including the SLF4J API and Logback dependencies in our pom:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
</dependency>

Now, let’s take a look at what logging looks like for four different approaches:

  • A property
  • A companion object
  • An extension method, and
  • A delegated property

4. Logger as a Property

The first thing we might try is to declare a logger property wherever we need it:

class Property {
    private val logger = LoggerFactory.getLogger(javaClass)

    //...
}

Here, we’ve used javaClass to dynamically compute the logger’s name from the defining class name. We can thus readily copy and paste this snippet wherever we want.

Then, we can use the logger in any method of the declaring class:

fun log(s: String) {
    logger.info(s)
}

We’ve chosen to declare the logger as private because we don’t want other classes, including subclasses, to have access to it and log on behalf of our class.

Of course, this is merely a hint for programmers rather than a strongly enforced rule, since it’s easy to obtain a logger with the same name.

4.1. Saving Some Typing

We could shorten our code a bit by factoring the getLogger call to a function:

fun getLogger(forClass: Class<*>): Logger =
  LoggerFactory.getLogger(forClass)

And by placing this into a utility class, we can now simply call getLogger(javaClass) instead of LoggerFactory.getLogger(javaClass) throughout the samples below.

5. Logger in a Companion Object

While the last example is powerful in its simplicity, it is not the most efficient.

First, to hold a reference to a logger in each class instance costs memory. Second, even though loggers are cached, we’ll still incur a cache lookup for every object instance that has a logger.

Let’s see if companion objects fare any better.

5.1. A First Attempt

In Java, declaring the logger as static is a pattern that addresses the above concerns.

In Kotlin, though, we don’t have static properties.

But we can emulate them with companion objects:

class LoggerInCompanionObject {
    companion object {
        private val loggerWithExplicitClass
          = getLogger(LoggerInCompanionObject::class.java)
    }

    //...
}

Notice how we’ve reused the getLogger convenience function from section 4.1. We’ll keep referring to it throughout the article.

So, with the above code, we can use again the logger exactly as before, in any method of the class:

fun log(s: String) {
    loggerWithExplicitClass.info(s)
}

5.2. What Happened to javaClass?

Sadly, the above approach comes with a drawback. Because we are directly referring to the enclosing class:

LoggerInCompanionObject::class.java

we’ve lost the ease of copy-pasting.

But why not just use javaClass like before? Actually, we can’t. If we had, we would have incorrectly obtained a logger named after the companion object’s class:

//Incorrect!
class LoggerInCompanionObject {
    companion object {
        private val loggerWithWrongClass = getLogger(javaClass)
    }
}
//...
loggerWithWrongClass.info("test")

The above would output a slightly wrong logger name. Take a look at the $Companion bit:

21:46:36.377 [main] INFO
com.baeldung.kotlin.logging.LoggerInCompanionObject$Companion - test

In fact, IntelliJ IDEA marks the declaration of the logger with a warning, because it recognizes that the reference to javaClass in a companion object probably isn’t what we want.

5.3. Deriving the Class Name With Reflection

Still, not all is lost.

We do have a way to derive the class name automatically and restore our ability to copy and paste the code, but we need an extra piece of reflection to do so.

First, let’s ensure we have the kotlin-reflect dependency in our pom:

<dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-reflect</artifactId>
    <version>1.2.51</version>
</dependency>

Then, we can dynamically obtain the correct class name for logging:

companion object {
    @Suppress("JAVA_CLASS_ON_COMPANION")
    private val logger = getLogger(javaClass.enclosingClass)
}
//...
logger.info("I feel good!")

We’ll now get the correct output:

10:00:32.840 [main] INFO
com.baeldung.kotlin.logging.LoggerInCompanionObject - I feel good!

The reason we use enclosingClass comes from the fact that companion objects, in the end, are instances of inner classes, so enclosingClass refers to the outer class, or in this case, LoggerInCompanionObject.

Also, it’s okay now for us to suppress the warning that IntelliJ IDEA gives on javaClass since now we’re doing the right thing with it.

5.4. @JvmStatic

While the properties of companion objects look like static fields, companion objects are more like singletons.

Kotlin companion objects have a special feature though, at least when running on a JVM, that converts companion objects to static fields:

@JvmStatic
private val logger = getLogger(javaClass.enclosingClass)

5.5. Putting It All Together

Let’s put all three improvements together. When joined together, these improvements make our logging construct copy-pastable and static:

class LoggerInCompanionObject {
    companion object {
        @Suppress("JAVA_CLASS_ON_COMPANION")
        @JvmStatic
        private val logger = getLogger(javaClass.enclosingClass)
    }

    fun log(s: String) {
        logger.info(s)
    }
}

6. Logger From an Extension Method

While interesting and efficient, using a companion object is verbose. What started as a one-liner is now multiple lines to copy-paste all over the codebase.

Also, using companion objects produces extra inner classes. Compared with the simple static logger declaration in Java, using companion objects is heavier.

So, let’s try an approach using extension methods.

6.1. A First Attempt

The basic idea is to define an extension method that returns a Logger, so every class that needs it can just call the method and obtain the correct instance.

We can define this anywhere on the classpath:

fun <T : Any> T.logger(): Logger = getLogger(javaClass)

Extension methods are basically copied to any class on which they’re applicable; so, we can simply refer directly to javaClass again.

And now, all classes will have the method logger as if it had been defined in the type:

class LoggerAsExtensionOnAny { // implied ": Any"
    fun log(s: String) {
        logger().info(s)
    }
}

While this approach is more concise than companion objects, we might want to smooth out some problems with it first.

6.2. Pollution of the Any Type

A significant drawback of our first extension method is that it pollutes the Any type.

Because we defined it as applying to any type at all, it ends up a bit invasive:

"foo".logger().info("uh-oh!")
// Sample output:
// 13:19:07.826 [main] INFO java.lang.String - uh-oh!

By defining logger() on Any, we’ve polluted all types in the language with the method.

This isn’t necessarily a problem. It doesn’t prevent other classes from having their own logger methods.

However, aside from the extra noise, it also breaks encapsulation. Types could now log for each other, which we don’t want.

And logger will now pop up on almost every IDE code suggestion.

6.3. Extension Method on a Marker Interface

We can narrow our extension method’s scope with a marker interface:

interface Logging

Having defined this interface, we can indicate that our extension method only applies to types that implement this interface:

fun <T : Logging> T.logger(): Logger = getLogger(javaClass)

And now, if we change our type to implement Logging, we can use logger as before:

class LoggerAsExtensionOnMarkerInterface : Logging {
    fun log(s: String) {
        logger().info(s)
    }
}

6.4. Reified Type Parameter

In the last two examples, we’ve used reflection to obtain the javaClass and give a distinguished name to our logger.

However, we can also extract such information from the T type parameter, avoiding a reflection call at runtime. To achieve this, we’ll declare the function as inline and reify the type parameter:

inline fun <reified T : Logging> T.logger(): Logger =
  getLogger(T::class.java)

Note that this changes the semantics of the code with respect to inheritance.  We’ll discuss this in detail in section 8.

6.5. Combining With Logger Properties

A nice thing about extension methods is that we can combine them with our first approach:

val logger = logger()

6.6. Combining With Companion Objects

But the story is more complex if we want to use our extension method in a companion object:

companion object : Logging {
    val logger = logger()
}

Because we’d have the same problem with javaClass as before:

com.baeldung.kotlin.logging.LoggerAsExtensionOnMarkerInterface$Companion

To account for this, let’s first define a method that obtains the class more robustly:

inline fun <T : Any> getClassForLogging(javaClass: Class<T>): Class<*> {
    return javaClass.enclosingClass?.takeIf {
        it.kotlin.companionObject?.java == javaClass
    } ?: javaClass
}

Here, getClassForLogging returns the enclosingClass if javaClass refers to a companion object.

And now we can again update our extension method:

inline fun <reified T : Logging> T.logger(): Logger
  = getLogger(getClassForLogging(T::class.java))

This way, we can actually use the same extension method whether the logger is included as a property or a companion object.

7. Logger as a Delegated Property

Lastly, let’s look at delegated properties.

What’s nice about this approach is that we avoid namespace pollution without requiring a marker interface:

class LoggerDelegate<in R : Any> : ReadOnlyProperty<R, Logger> {
    override fun getValue(thisRef: R, property: KProperty<*>)
     = getLogger(getClassForLogging(thisRef.javaClass))
}

We can then use it with a property:

private val logger by LoggerDelegate()

Because of getClassForLogging, this works for companion objects, too:

companion object {
    val logger by LoggerDelegate()
}

And while delegated properties are powerful, note that getValue is re-computed each time the property is read.

Also, we should remember that delegate properties must use reflection for them to work.

8. A Few Notes About Inheritance

It’s very typical to have one logger per class. And that’s why we also typically declare loggers as private.

However, there are times when we’ll want our subclasses to refer to their superclass’s logger.

And depending on our use case, the above four approaches will behave differently.

In general, when we use reflection or other dynamic features, we pick up the actual class of the object at runtime.

But, when we statically refer to a class or a reified type parameter by name, the value will be fixed at compile time.

For example, with delegated properties, since the logger instance is obtained dynamically every time the property is read, it will take the name of the class where it’s used:

open class LoggerAsPropertyDelegate {
    protected val logger by LoggerDelegate()
    //...
}

class DelegateSubclass : LoggerAsPropertyDelegate() {
    fun show() {
        logger.info("look!")
    }
}

Let’s look at the output:

09:23:33.093 [main] INFO
com.baeldung.kotlin.logging.DelegateSubclass - look!

Even though logger is declared in the superclass, it prints the name of the subclass.

The same happens when a logger is declared as property and instantiated using javaClass.

And extension methods exhibit this behavior, too, unless we reify the type parameter.

Conversely, with reified generics, explicit class names, and companion objects, a logger’s name stays the same across the type hierarchy.

9. Conclusions

In this article, we’ve looked at several Kotlin techniques that we can apply to the task of declaring and instantiating loggers.

Starting simply, we progressively increased complexity in a series of attempts to improve efficiency and reduce boilerplate, taking a look at Kotlin companion objects, extension methods, and delegated properties.

As always, these examples are available in full over on GitHub.

2 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.