We (still) use slf4j/logback in our Kotlin code. Currently, we define a `val logger: Logger = Logger...
n
We (still) use slf4j/logback in our Kotlin code. Currently, we define a
val logger: Logger = LoggerFactory.getLogger(Foo::class.java)
in the companion object of a class
Foo
. I remember a discussion about defining this as a class property instead of as a companion object property (which would then allow to use
val logger: Logger = LoggerFactory.getLogger(this::class.java)
) but I can't find it anymore. What is current best practice here ?
n
I don't remember that discussion, but another aspect to consider is whether this class is a singleton or not, e.g. you're using Guice or Spring with
@Singleton
. if it's not, I'd probably avoid defining the logger as an instance field. myself I usually define it at either the companion object or file level
a
I use a functional interface:
Copy code
interface HasLogger {
    val logger: Logger
        get() = LoggerFactory.getLogger(javaClass)
}
And then every class that need a logger simply implements the interface:
Copy code
class MyClass : HasLogger {
   logger.debug("some stuff here")
}
n
that seems extremely heavy in terms of logger instantiation
b
Exactly, new logger is instantiated each time you log something
a
No, the logger factory is caching by classname
b
Ah, right
n
eh, it still uses reflection on every message and calls into a (concurrent) map. not doing those things would still be faster
b
I personally don't really find tagged loggers all that useful, so most of the time I just use one global logger for everything. Your mileage might vary
n
I've encountered ideas similar to HasLogger before, and really dislike it. You have to add a field to every class, and it doesn't offer anything with respect to free functions.
b
You'd be better off with @Logger from lombok. At least it instantiates them at compile time
n
I guess in Java where you don't have free functions things are different, but in languages with free functions, you really just want
logger
at file level
n
"global logger" does not allow to vary log threshold levels bewteen modules / subsystems. Not really acceptable in large code bases.
n
You can make do with a global logger if you have to, but it's a major compromise which little upside, for languages that can easily support hierarchical logging like python, kotlin
*with
In C++ hierarchical logging is a real pain in many ways so global loggers are more common
n
So real question is: how much is the overhead of creating a
logger
property in every class instance instead of in the single companion object class, and how much is the extra cost of initializing it whenever an object of that class is constructed.
n
But why create the logger property per class, instead of per file/package?
n
and is tere a runtime cost difference between accessing a class property vs. a companion object property
we create loggers per class for 2 reasons: (a) this is what we are used to do from the Java world and (b) allows to customize thresholds on class level. Obviously argument (a) is very weak...
b
On jvm side of things, file level is almost the same as companion level. In fact they're exactly the same if you only have one class in that file
n
someone in my team just used
val logger: Logger = LoggerFactory(javaClass)
and while that looks nice, it will use
Foo$Companion
instead of
Foo
as logger name. So I either have to revert this or move it out of the companion object
b
You can mark that with @JvmStatic to get rid of Companion
n
JVM: yes, forgot to mention that we currently only target JVM
@JvmStatic
does not help here
j
Copy code
object MyLoggerFactory {
    fun getLogger(name: String?): Logger = LoggerFactory.getLogger(name)
    fun getLogger(clazz: Class<*>?): Logger = LoggerFactory.getLogger(clazz)
    fun getILoggerFactory(): ILoggerFactory = LoggerFactory.getILoggerFactory()
}
You can also wrap the logger factory in your own to inject / clean / do anything before actually creating the logger
We do this to get metrics and things on our logs
n
can you elaborate how this helps?
j
sure, 2 secs
Copy code
fun getLogger(clazz: Class<*>?): Logger = getLogger(clazz?.simpleName?.removeSuffix("\$Companion"))
you can strip out the $Companion part of the logger name
n
yes, thought about that. looks a bit hacky though
but would do the trick
j
Yeah, but all encapsulated in a single place and not littered all over your code at least
n
yup. needs an extra import (for the global method) in every file. But we could save the
LoggerFactory
import
j
Yeah. We just did a big replace all of the LoggerFactory with our own Logger factory
Few hundred file changes but we only used those three functions from LoggerFactory, so everything still compiled and all the tests worked 😃
Allowed us a seam to more easily mock / test our logs (mocking objects is slightly less difficult than Java static classes, although still gnarly)
n
interesting. we moved from our own logger to slf4j to reduce "custom facades" some time back. But of course this only changes the factory and does not affect the logging API. I will think about is
j
Right. We're still using slf4j, just wrapping the logger factory.
n
anyone has an answer / opinion of the more general question of cost of class properties vs companion object properties. I understand the "running the constructor of the property every time an instance of the enclosing class is constructed" part. But if that is negligible (which I think is the case for logback-based loggers), is there really a difference?
n
@nkiesel I don't find the arguments for class level logger properties, even b), very compelling
per file/package customization is already a lot
And unlike per-class, it's actually a general approach that makes sense, in a language that has free functions. When you log from a free function in kotlin you'll still need to use a logger... are you just going to pick the logger of the "closest" class, or one that happens to be defined in the same file?
n
Practically it would not make a gig difference for us because we still mostly use "1 class in 1 file" approach though we start to have divergences from this. However, using a per-file logger means we have to manually specify the logger name, no? That will lead to copy-and-paste errors
n
No, I don't think so
n
yes, logging for free functions is a clear problem of per-class loggers
n
I've been curious about this myself and I've seen code for kotlin before that creates file lever loggers without manually writing any file name
n
enlighten me!
n
Copy code
import mu.KotlinLogging
private val logger = KotlinLogging.logger {} 
class FooWithLogging {
    val message = "world"
    fun bar() {
        logger.debug { "hello $message" }
    }
}
I think what happens here is that you pass an empty lambda to
KotlinLogging.logger
, and it figures out what package the lambda belongs to
This is very very similar to how python's hierarchical logging looks, which IMHO is a logical thing to consider if you're thikning about Kotlin. Because python's logging is Java influenced, but of course python puts much more emphasis on free functions, like Kotlin. So there are a lot of parallels.
In python you'd typically write
logger = logging.getLogger(___name___)
I've used python's
logging
for a long while and it's always served me very well, I can't ever remember feeling like I wanted per class customization.
(although, if you were writing some kind of special class that you knew did a lot of logging, you always have that option, of course, but not something to do routinely, IMHO)
n
just looked at https://github.com/MicroUtils/kotlin-logging. The trick there is to pass an empty lambda and then get the class name from that (by removing anything after
$
in the resulting
.javaClass.name
). One downside is that the logger name for a class
C
inside a file
C.kt
will be "CKt" and not "C". But interesting idea nevertheless
n
n
https://github.com/kxtra/kxtra-slf4j is a simpler (but JVM only) version. But all (not surprisingly) will use the file name instead of the class name for the logger. We do have production instances with specific log thresholds (or files) for specific classes. I will have to see how easy it will be to adjust that.
n
I'd expect that in either case, there are multiple ways to create the logger, you should still be able to pass the class name or a custom string, if you need to for specific cases
it would be odd/unnecessary to lock things down like that
1207 Views