Is it possible for an inline function to grab a re...
# getting-started
m
Is it possible for an inline function to grab a reference to the calling class without it being passed explicitly?
j
There is no special syntax for this that I know of. What would that mean if it's called at the top level? Would you want the generated wrapper class for the file in this case? There might be solutions with reflection
m
I'm wondering how logback achieves it. I can just make inline logi(String) calls from anywhere and it will include the class name of the caller. Of course that doesn't work when there is no such class, but that's not important here.
t
Timber also does it no? Maybe you can check the source code 🤔
e
@Mark Be aware that getting stacktrace could be time consuming operation. I would suggest to make your own measurements. In one of my projects I collected a lot of logs and this action (getting class name from the stack trace) consumed significant amount of time and produced delays.
1
m
I was thinking the same, but if it’s so bad, why would it be default behavior in such a popular library?
e
In Timber it only affects debug builds normally, which is OK. But if you (as I did) are collecting logs on production it could be important. And also it depends on amount of logs, because it is not seconds, but milliseconds.
👍 1
j
If you're ok with defining a receiver, you could use something like:
Copy code
inline fun <reified T> T.log(...) {
    // use T::class here
}
And for top-level you could define one that requires passing an explicit type parameter:
Copy code
inline fun <reified T> log(...) {
    // use T::class here
}
This second one needs to be called like
log<MyClass>(...)
💡 1
m
@Joffrey Thanks very much for that. It looks like exactly what I need. I just remembered that the way I configured logback, the tag name was derived from the kotlin source filename, which I guess can only be obtained from the stacktrace. However, I really like the way that those two functions you gave, together cover all scenarios.
😊 1
Now that I’m applying this to my app, I’m noticing it’s very easy to call the log function with a useless Receiver, such as
CoroutineScope
.
j
True. The actual signature that I was personally using was instead returning a logger (and taking no argument). So I wasn't using this function for every log call, but rather to initialize a
logger
property for the class, and then I used the
logger
property to actually log messages
e
I personally also use this approach, described by @Joffrey. I have one function (2 actually - log and logError) in the class and call it from the code where necessary.
m
How about all the top level functions?
e
@Mark There is a tradeoff between simplicity of the end call and the complexity of this logging system in general. Having only top level functions you reduce complexity in general, but your end calls have to be more complex. So for me it was more important to have the most simple end calls possible. Ideally
log(text)
and nothing more. Easy to write and easy to read (in some classes I used to have a lot of them). But if you call
log
function only 1-2 times in a class or you don’t mind having something like
log<MyClass>(text)
the top functions may be better. In my particular case I also have another parameters besides the class/tag (I am logging also a feature) and I didn’t like to add a lot of duplicated boilerplate to each actual call.
Also there is another advantage of using local log functions. By uniforming the log calls you make easier moving/copying the code between classes. If you use only top level you will need to correct each log call when you move the code.
m
Thanks very much @electrolobzik. There may be some confusion though, I think you are referring to top-level logging functions, but I was asking about when call site is a top-level function? I suppose top-level functions would be a suitable place to use the
log<MyClass>(text)
type.
j
Yeah the point of the second variant I suggested was for top-level calls to still be able to use logging as long as they provide a class at the call site
For the first variant I usually prefer a different version that returns a
Logger
, and that you use once in the class, and then every logging call is made through the logger (which also solves the copy-paste problem mentioned by Roman)
m
Yes, that makes sense. So only use the logger within a class AND when there is perhaps more than one logging call.
j
Yeah or just use the logger all the time in classes, even if they just have one logging call (to be consistent). Otherwise, you might have to change unrelated pieces of code when you add one more log
m
I guess so. Although then we’re getting back to something not that dissimilar from legacy Android code where almost every class had a
public static final String TAG = Foo.class.getSimpleName();
It’s better than that, yes, but I’m going to have to add a few hundred logger properties to my codebase.
j
One thing we did in our big project was declare an abstract class that can be used as a parent class for a companion object, and which provides the
logger
property behind the scenes with the proper class. So every class would just do:
Copy code
class MyThing {

    companion object : KLogging()

    fun doStuff() {
        logger.info("I can use the logger property from companion here")
    }
}
Where
KLogging
is defined something like this:
Copy code
abstract class KLogging(target: KClass<*>? = null) {
    val logger: KLogger by lazy { YourLoggerFactory.logger(target ?: this::class) }
}
K 1
💡 1
m
Nice idea!
Would you combine it with an existing
companion object
or always keep it separate? (I think it’s possible to have multiple companion objects?)
j
You can only have one companion object, but it's unlikely your companion object already extends a class I think, so you can just mix both:
Copy code
companion object : KLogging() {
    val otherCustomProp = 42

    fun otherCustomFun() { ... }
}
👍 1
m
Perhaps I was thinking of named companion objects
j
I think even if you name them you cannot have multiple companions, but you probably don't need to have multiple anyway, unless you really want them to extend multiple different classes. In this case, you could make one of them a regular object instead of a companion, but then you will need qualified access to its functions and props AFAIR.
m
Thanks again @Joffrey, I believe I have enough now to be getting started 😀
🙂 1
e
Can KLogging be an interface? It could solve the issue with inheritance.
Also I would like to mention that Tag=Class concept is a bit outdated and it is more correct to use some other forms. The simplest is just String. But it could be an Enum or even some Interface, depending on the project.
Especially when we talk about top level functions.
m
Yes, I take your point about Tag/Class. I’ll just provide two constructors for
KLogging
(
String
&
KClass
) KISS
Copy code
abstract class KLogging(private val name: String) {

    constructor(target: KClass<*>) : this(target::class.java.simpleName)

    val logger: KLogger by lazy { createLogger(name) }
}
For top-level functions in the same source file as a class with the
Foo
companion object, would you prefer to do
Foo.log("bar")
or
log<Foo>("bar")
? The advantage of the former is reuse of the lazy logger. Also it means the
Foo
companion object is the single place that determines the tag name.
Hmm, and how about within
Composable
functions? Surely a different strategy is needed to prevent recompositions? I guess you shouldn’t be logging outside of a side effect anyway.
I came up with a new strategy that has minimal impact on existing code. In general, I declare one
internal
logger for each module (though you can of course change this strategy on a per module basis). Note - this obviously only applies to apps with lots of modules (mine has over 30).
Copy code
internal object ModuleLogger: MyLogger by MyLogger("featureFoo")
where:
Copy code
interface MyLogger {
    val logger: KLogger

    fun logd(message: String) {
        logger.debug(message)
    }

    fun logd(lazyMessage: () -> String) {
        logger.debug(lazyMessage)
    }

    fun logi(message: String) {
        logger.info(message)
    }

    ...

    companion object {
        operator fun invoke(name: String): MyLogger = object : MyLogger {
            override val logger: KLogger by lazy { createLogger(name) }
        }

        // for class-level loggers
        // use like:
        // Foo {
        //     companion object : MyLogger by MyLogger<Foo>()
        // }
        inline operator fun <reified T> invoke(): MyLogger = invoke(T::class.java.simpleName)
    }
}
This means, I can just leave the existing
logd
, `logi`calls as they are by simply adding imports like:
Copy code
import com.bar.myapp.feature.foo.ModuleLogger.logd
If, for certain classes, you want to do a class-level tag strategy, then you can just do the companion object thing there.
👍 1
The only problem with the module loggers is that
internal
visibility loggers cannot be used in
public inline
functions. In this case, you can use, for example, class-level loggers from which public companion objects extend.