JakeWharton/cite

Supply fully qualified names for loggers, making those work on Js

OliverO2 opened this issue · 6 comments

Logging frameworks such as oshai/kotlin-logging use invocations like this to automatically assign a logger name:

private val logger = KotlinLogging.logger {}

Behind the scenes, they use KClass.qualifiedName which does not produce a fully qualified name on Js.

This plugin could solve that, e.g.:

  • A file-level logger in MyFile.kt

    package com.example
    
    val logger = Logger(__QUALIFIED_CLASS__) // "com.example.MyFileKt"
  • A class-level logger

    package com.example
    
    class MyClass {
        companion object {
            val logger = Logger(__QUALIFIED_CLASS__) // "com.example.MyClass" 
        }
    }

In an internal project, I had created an annotation-based compiler plugin to generate logger names (also via a IrElementTransformerVoidWithContext derivative). The relevant qualified class name (including Kotlin's invisible file-level classes) was retrieved like so:

val qualifiedSurroundingClassName = allScopes.reversed().firstNotNullOfOrNull {
    it.scope.scopeOwnerSymbol.owner.qualifiedClassName()
}
if (qualifiedSurroundingClassName == null) {
    reportError("Could not find a qualified class name", call)
    return super.visitCall(call)
}

What should qualified name return in the top-level case on JS and native where there is no concept of an implicit file class like there is on the JVM?

A different way of phrasing that question is: Should we include MyFileKt in the qualified name when we don't support __TYPE__ at the same location?

From a plugin user perspective, I would not differentiate between platforms. For example, I have a YAML server-side logging configuration, which is directly applied at the backend, and transmitted to frontends on JVM and Js platforms. Since there are classes in commonMain which are used across platforms, it is essential to have platform-independent logger names. The above retrieval code does that on the JVM and Js.

Example configuration:

logging:
  com.example.service.FrontendServiceKt: DEBUG # this is a Js/JVM frontend class
  com.example.service.ClientSessionKt: DEBUG # this is a JVM backend class

It seems that qualifiedSurroundingClassName is similar to __TYPE__, except that it does not rely on visitor tracking, but asks the compiler directly at retrieval time. So it always returns a fully qualified class, since a file-level class usually exists (at least the way I'm using the compiler via Gradle).

Oh, I forgot: For the above code to work, it needs to be supplemented with these extension functions:

    private fun IrSymbolOwner?.qualifiedClassName(): String? = when (this) {
        is IrFile -> "${fqName.asQualificationPrefix()}${name.substringBeforeLast('.')}Kt"
        is IrClass -> if (isLocal || isCompanion) null else fqnString(true)
        else -> null
    }

    private fun FqName.asQualificationPrefix(): String = if (isRoot) "" else "$this."

So it was actually me creating a name for that fictious file-level class, regardless of whether such a class is actually generated or not.