Idiomatic way of logging in Kotlin

2019-01-16 00:45发布

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?

14条回答
淡お忘
2楼-- · 2019-01-16 00:59

There are many great answers here already, but all of them concern adding a logger to a class, but how would you do that to do logging in Top Level Functions?

This approach is generic and simple enough to work well in both classes, companion objects and Top Level Functions:

package nieldw.test

import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import org.junit.jupiter.api.Test

fun logger(lambda: () -> Unit): Lazy<Logger> = lazy { LogManager.getLogger(getClassName(lambda.javaClass)) }
private fun <T : Any> getClassName(clazz: Class<T>): String = clazz.name.replace(Regex("""\$.*$"""), "")

val topLog by logger { }

class TopLevelLoggingTest {
    val classLog by logger { }

    @Test
    fun `What is the javaClass?`() {
        topLog.info("THIS IS IT")
        classLog.info("THIS IS IT")
    }
}
查看更多
成全新的幸福
3楼-- · 2019-01-16 01:05

KISS: For Java Teams Migrating to Kotlin

If you don't mind providing the class name on each instantiation of the logger (just like java), you can keep it simple by defining this as a top-level function somewhere in your project:

import org.slf4j.LoggerFactory

inline fun <reified T:Any> logger() = LoggerFactory.getLogger(T::class.java)

This uses a Kotlin reified type parameter.

Now, you can use this as follows:

class SomeClass {
  // or within a companion object for one-instance-per-class
  val log = logger<SomeClass>()
  ...
}

This approach is super-simple and close to the java equivalent, but just adds some syntactical sugar.

Next Step: Extensions or Delegates

I personally prefer going one step further and using the extensions or delegates approach. This is nicely summarized in @JaysonMinard's answer, but here is the TL;DR for the "Delegate" approach with the log4j2 API (UPDATE: no need to write this code manually any more, as it has been released as an official module of the log4j2 project, see below). Since log4j2, unlike slf4j, supports logging with Supplier's, I've also added a delegate to make using these methods simpler.

import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import org.apache.logging.log4j.util.Supplier
import kotlin.reflect.companionObject

/**
 * An adapter to allow cleaner syntax when calling a logger with a Kotlin lambda. Otherwise calling the
 * method with a lambda logs the lambda itself, and not its evaluation. We specify the Lambda SAM type as a log4j2 `Supplier`
 * to avoid this. Since we are using the log4j2 api here, this does not evaluate the lambda if the level
 * is not enabled.
 */
class FunctionalLogger(val log: Logger): Logger by log {
  inline fun debug(crossinline supplier: () -> String) {
    log.debug(Supplier { supplier.invoke() })
  }

  inline fun debug(t: Throwable, crossinline supplier: () -> String) {
    log.debug(Supplier { supplier.invoke() }, t)
  }

  inline fun info(crossinline supplier: () -> String) {
    log.info(Supplier { supplier.invoke() })
  }

  inline fun info(t: Throwable, crossinline supplier: () -> String) {
    log.info(Supplier { supplier.invoke() }, t)
  }

  inline fun warn(crossinline supplier: () -> String) {
    log.warn(Supplier { supplier.invoke() })
  }

  inline fun warn(t: Throwable, crossinline supplier: () -> String) {
    log.warn(Supplier { supplier.invoke() }, t)
  }

  inline fun error(crossinline supplier: () -> String) {
    log.error(Supplier { supplier.invoke() })
  }

  inline fun error(t: Throwable, crossinline supplier: () -> String) {
    log.error(Supplier { supplier.invoke() }, t)
  }
}

/**
 * A delegate-based lazy logger instantiation. Use: `val log by logger()`.
 */
@Suppress("unused")
inline fun <reified T : Any> T.logger(): Lazy<FunctionalLogger> =
  lazy { FunctionalLogger(LogManager.getLogger(unwrapCompanionClass(T::class.java))) }

// unwrap companion class to enclosing class given a Java Class
fun <T : Any> unwrapCompanionClass(ofClass: Class<T>): Class<*> {
  return if (ofClass.enclosingClass != null && ofClass.enclosingClass.kotlin.companionObject?.java == ofClass) {
    ofClass.enclosingClass
  } else {
    ofClass
  }
}

Log4j2 Kotlin Logging API

Most of the previous section has been directly adapted to produce the Kotlin Logging API module, which is now an official part of Log4j2 (disclaimer: I am the primary author). You can download this directly from Apache, or via Maven Central.

Usage is basically as describe above, but the module supports both interface-based logger access, a logger extension function on Any for use where this is defined, and a named logger function for use where no this is defined (such as top-level functions).

查看更多
叛逆
4楼-- · 2019-01-16 01:06

create companion object and mark the appropriate fields with @JvmStatic annotation

查看更多
Bombasti
5楼-- · 2019-01-16 01:10

Slf4j example, same for others. This even works for creating package level logger

/**  
  * Get logger by current class name.  
  */ 

fun getLogger(c: () -> Unit): Logger = 
        LoggerFactory.getLogger(c.javaClass.enclosingClass)

Usage:

val logger = getLogger { }
查看更多
我想做一个坏孩纸
6楼-- · 2019-01-16 01:11

What about an extension function on Class instead? That way you end up with:

public fun KClass.logger(): Logger = LoggerFactory.getLogger(this.java)

class SomeClass {
    val LOG = SomeClass::class.logger()
}

Note - I've not tested this at all, so it might not be quite right.

查看更多
爱情/是我丢掉的垃圾
7楼-- · 2019-01-16 01:13
fun <R : Any> R.logger(): Lazy<Logger> = lazy { 
    LoggerFactory.getLogger((if (javaClass.kotlin.isCompanion) javaClass.enclosingClass else javaClass).name) 
}

class Foo {
    val logger by logger()
}

class Foo {
    companion object {
        val logger by logger()
    }
}
查看更多
登录 后发表回答