kotlinkotlin-logging

Idiomatic way of logging in Kotlin


Kotlin doesn't have the same notion of static fields as used in Java. In Java, the generally accepted way of doing logging is:

public class Foo {
    private static final Logger LOG = LoggerFactory.getLogger(Foo.class);
}

Question is what is the idiomatic way of performing logging in Kotlin?


Solution

  • In the majority of mature Kotlin code, you will find one of these patterns below. The approach using Property Delegates takes advantage of the power of Kotlin to produce the smallest code.

    Note: the code here is for java.util.Logging but the same theory applies to any logging library

    Static-like (common, equivalent of your Java code in the question)

    If you cannot trust in the performance of that hash lookup inside the logging system, you can get similar behavior to your Java code by using a companion object which can hold an instance and feel like a static to you.

    class MyClass {
        companion object {
            val LOG = Logger.getLogger(MyClass::class.java.name) 
        }
    
        fun foo() {
            LOG.warning("Hello from MyClass")
        }
    }  
    

    creating output:

    Dec 26, 2015 11:28:32 AM org.stackoverflow.kotlin.test.MyClass foo INFO: Hello from MyClass

    More on companion objects here: Companion Objects ... Also note that in the sample above MyClass::class.java gets the instance of type Class<MyClass> for the logger, whereas this.javaClass would get the instance of type Class<MyClass.Companion>.

    Per Instance of a Class (common)

    But, there is really no reason to avoid calling and getting a logger at the instance level. The idiomatic Java way you mentioned is outdated and based on fear of performance, whereas the logger per class is already cached by almost any reasonable logging system on the planet. Just create a member to hold the logger object.

    class MyClass {
      val LOG = Logger.getLogger(this.javaClass.name)
      
      fun foo() {
            LOG.warning("Hello from MyClass")
      }
    } 
    

    creating output:

    Dec 26, 2015 11:28:44 AM org.stackoverflow.kotlin.test.MyClass foo INFO: Hello from MyClass

    You can performance test both per instance and per class variations and see if there is a realistic difference for most apps.

    Property Delegates (common, most elegant)

    Another approach, which is suggested by @Jire in another answer, is to create a property delegate, which you can then use to do the logic uniformly in any other class that you want. There is a simpler way to do this since Kotlin provides a Lazy delegate already, we can just wrap it in a function. One trick here is that if we want to know the type of the class currently using the delegate, we make it an extension function on any class:

    fun <R : Any> R.logger(): Lazy<Logger> {
        return lazy { Logger.getLogger(unwrapCompanionClass(this.javaClass).name) }
    }
    // see code for unwrapCompanionClass() below in "Putting it all Together section"
    

    This code also makes sure that if you use it in a Companion Object that the logger name will be the same as if you used it on the class itself. Now you can simply:

    class Something {
        val LOG by logger()
    
        fun foo() {
            LOG.info("Hello from Something")
        }
    }
    

    for per class instance, or if you want it to be more static with one instance per class:

    class SomethingElse {
        companion object {
            val LOG by logger()
    
        }
    
        fun foo() {
            LOG.info("Hello from SomethingElse")
        }
    }
    

    And your output from calling foo() on both of these classes would be:

    Dec 26, 2015 11:30:55 AM org.stackoverflow.kotlin.test.Something foo INFO: Hello from Something

    Dec 26, 2015 11:30:55 AM org.stackoverflow.kotlin.test.SomethingElse foo INFO: Hello from SomethingElse

    Extension Functions (uncommon in this case because of "pollution" of Any namespace)

    Kotlin has a few hidden tricks that let you make some of this code even smaller. You can create extension functions on classes and therefore give them additional functionality. One suggestion in the comments above was to extend Any with a logger function. This can create noise anytime someone uses code-completion in their IDE in any class. But there is a secret benefit to extending Any or some other marker interface: you can imply that you are extending your own class and therefore detect the class you are within. Huh? To be less confusing, here is the code:

    // extend any class with the ability to get a logger
    fun <T: Any> T.logger(): Logger {
         return Logger.getLogger(unwrapCompanionClass(this.javaClass).name)
    }
    

    Now within a class (or companion object), I can simply call this extension on my own class:

    class SomethingDifferent {
        val LOG = logger()
    
        fun foo() {
            LOG.info("Hello from SomethingDifferent")
        }
    }
    

    Producing output:

    Dec 26, 2015 11:29:12 AM org.stackoverflow.kotlin.test.SomethingDifferent foo INFO: Hello from SomethingDifferent

    Basically, the code is seen as a call to extension Something.logger(). The problem is that the following could also be true creating "pollution" on other classes:

    val LOG1 = "".logger()
    val LOG2 = Date().logger()
    val LOG3 = 123.logger()
    

    Extension Functions on Marker Interface (not sure how common, but common model for "traits")

    To make the use of extensions cleaner and reduce "pollution", you could use a marker interface to extend:

    interface Loggable {} 
    
    fun Loggable.logger(): Logger {
         return Logger.getLogger(unwrapCompanionClass(this.javaClass).name)
    }    
    

    Or even make the method part of the interface with a default implementation:

    interface Loggable {
        public fun logger(): Logger {
            return Logger.getLogger(unwrapCompanionClass(this.javaClass).name)
        }
    }
    

    And use either of these variations in your class:

    class MarkedClass: Loggable {
        val LOG = logger()
    }
    

    Producing output:

    Dec 26, 2015 11:41:01 AM org.stackoverflow.kotlin.test.MarkedClass foo INFO: Hello from MarkedClass

    If you wanted to force the creation of a uniform field to hold the logger, then while using this interface you could easily require the implementer to have a field such as LOG:

    interface Loggable {
        val LOG: Logger  // abstract required field
        
        public fun logger(): Logger {
            return Logger.getLogger(unwrapCompanionClass(this.javaClass).name)
        }
    }
    

    Now the implementer of the interface must look like this:

    class MarkedClass: Loggable {
        override val LOG: Logger = logger()
    }
    

    Of course, an abstract base class can do the same, having the option of both the interface and an abstract class implementing that interface allows flexibility and uniformity:

    abstract class WithLogging: Loggable {
        override val LOG: Logger = logger()
    }
    
    // using the logging from the base class
    class MyClass1: WithLogging() {
        // ... already has logging!
    }
    
    // providing own logging compatible with marker interface
    class MyClass2: ImportantBaseClass(), Loggable {
        // ... has logging that we can understand, but doesn't change my hierarchy
        override val LOG: Logger = logger()
    }
    
    // providing logging from the base class via a companion object so our class hierarchy is not affected
    class MyClass3: ImportantBaseClass() {
        companion object : WithLogging() {
           // we have the LOG property now!
        }
    }
    

    Putting it All Together (A small helper library)

    Here is a small helper library to make any of the options above easy to use. It is common in Kotlin to extend API's to make them more to your liking. Either in extension or top-level functions. Here is a mix to give you options for how to create loggers, and a sample showing all variations:

    // Return logger for Java class, if companion object fix the name
    fun <T: Any> logger(forClass: Class<T>): Logger {
        return Logger.getLogger(unwrapCompanionClass(forClass).name)
    }
    
    // unwrap companion class to enclosing class given a Java Class
    fun <T : Any> unwrapCompanionClass(ofClass: Class<T>): Class<*> { 
       return ofClass.enclosingClass?.takeIf { 
          ofClass.enclosingClass.kotlin.companionObject?.java == ofClass 
       } ?: ofClass 
    }
    
    // unwrap companion class to enclosing class given a Kotlin Class
    fun <T: Any> unwrapCompanionClass(ofClass: KClass<T>): KClass<*> {
       return unwrapCompanionClass(ofClass.java).kotlin
    }
    
    // Return logger for Kotlin class
    fun <T: Any> logger(forClass: KClass<T>): Logger {
        return logger(forClass.java)
    }
    
    // return logger from extended class (or the enclosing class)
    fun <T: Any> T.logger(): Logger {
        return logger(this.javaClass)
    }
    
    // return a lazy logger property delegate for enclosing class
    fun <R : Any> R.lazyLogger(): Lazy<Logger> {
        return lazy { logger(this.javaClass) }
    }
    
    // return a logger property delegate for enclosing class
    fun <R : Any> R.injectLogger(): Lazy<Logger> {
        return lazyOf(logger(this.javaClass))
    }
    
    // marker interface and related extension (remove extension for Any.logger() in favour of this)
    interface Loggable {}
    fun Loggable.logger(): Logger = logger(this.javaClass)
    
    // abstract base class to provide logging, intended for companion objects more than classes but works for either
    abstract class WithLogging: Loggable {
        val LOG = logger()
    }
    

    Pick whichever of those you want to keep, and here are all of the options in use:

    class MixedBagOfTricks {
        companion object {
            val LOG1 by lazyLogger()          // lazy delegate, 1 instance per class
            val LOG2 by injectLogger()        // immediate, 1 instance per class
            val LOG3 = logger()               // immediate, 1 instance per class
            val LOG4 = logger(this.javaClass) // immediate, 1 instance per class
        }
    
        val LOG5 by lazyLogger()              // lazy delegate, 1 per instance of class
        val LOG6 by injectLogger()            // immediate, 1 per instance of class
        val LOG7 = logger()                   // immediate, 1 per instance of class
        val LOG8 = logger(this.javaClass)     // immediate, 1 instance per class
    }
    
    val LOG9 = logger(MixedBagOfTricks::class)  // top level variable in package
    
    // or alternative for marker interface in class
    class MixedBagOfTricks : Loggable {
        val LOG10 = logger()
    }
    
    // or alternative for marker interface in companion object of class
    class MixedBagOfTricks {
        companion object : Loggable {
            val LOG11 = logger()
        }
    }
    
    // or alternative for abstract base class for companion object of class
    class MixedBagOfTricks {
        companion object: WithLogging() {} // instance 12
    
        fun foo() {
           LOG.info("Hello from MixedBagOfTricks")
        }
    }
    
    // or alternative for abstract base class for our actual class
    class MixedBagOfTricks : WithLogging() { // instance 13
        fun foo() {
           LOG.info("Hello from MixedBagOfTricks")
        }
    }
    

    All 13 instances of the loggers created in this sample will produce the same logger name, and output:

    Dec 26, 2015 11:39:00 AM org.stackoverflow.kotlin.test.MixedBagOfTricks foo INFO: Hello from MixedBagOfTricks

    Note: The unwrapCompanionClass() method ensures that we do not generate a logger named after the companion object but rather the enclosing class. This is the current recommended way to find the class containing the companion object. Stripping "$Companion" from the name using removeSuffix() does not work since companion objects can be given custom names.