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 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.25</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 code base.
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 it 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 it 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 a 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.