Kotlin

Idiomatic way of logging in Kotlin closed

25 September 2026 · 16 min read

Idiomatic way of logging in Kotlin closed

Logging is a crucial aspect of software development, enabling developers to track application behavior, debug issues, and monitor performance. In Kotlin, a modern and concise language, there are idiomatic ways to approach logging that leverage its features and best practices. Understanding the idiomatic way of logging in Kotlin involves not just choosing a logging framework but also adopting patterns that make your logging clear, efficient, and maintainable. This article explores various techniques and tools to help you effectively implement logging in your Kotlin projects, ensuring that you have the insights you need to keep your applications running smoothly. We will delve into the best practices, popular libraries, and practical examples to guide you.

Choosing a Logging Framework in Kotlin

Selecting the right logging framework is the first step toward implementing effective logging in your Kotlin applications. Several options are available, each with its own set of features and advantages. Some of the most popular choices include SLF4J (Simple Logging Facade for Java), Logback, and Kotlin Logging. SLF4J provides a simple abstraction layer that allows you to switch between different logging implementations without modifying your code. Logback, a powerful and flexible logging framework, is often used with SLF4J due to its performance and configurability. Kotlin Logging, a lightweight wrapper around SLF4J, offers a more Kotlin-friendly API and leverages Kotlin’s features like extension functions and string templates.

When choosing a framework, consider factors like performance, ease of configuration, and integration with other libraries and tools in your project. For instance, if you are working on a Spring Boot application, Logback is often the default choice and integrates seamlessly. On the other hand, if you need a lightweight solution with minimal dependencies, Kotlin Logging might be more suitable. According to a study by Sematext, “Proper logging practices can reduce debugging time by up to 50%,” highlighting the importance of a well-chosen logging framework. Source: Sematext Blog

Ultimately, the best logging framework is the one that best fits your project’s specific needs and requirements. Experiment with different options and evaluate their performance and ease of use before making a final decision. Remember to configure your chosen framework properly to ensure that log messages are formatted consistently and stored in a way that makes them easy to analyze. Consider using structured logging formats like JSON to enhance the ability to parse and analyze logs programmatically. For example, libraries such as Logstash can ingest and process structured logs very effectively.

Implementing Basic Logging in Kotlin

Once you’ve selected a logging framework, the next step is to implement basic logging in your Kotlin code. This involves creating a logger instance and using it to log messages at different severity levels, such as DEBUG, INFO, WARN, ERROR, and TRACE. The specific syntax for creating a logger and logging messages will vary depending on the framework you’re using, but the general principles remain the same. Here’s an example using Kotlin Logging:

import mu.KotlinLogging private val logger = KotlinLogging.logger {} fun main() { logger.debug { "This is a debug message" } logger.info { "This is an info message" } logger.warn { "This is a warning message" } logger.error { "This is an error message" } } 

This code snippet demonstrates how to create a logger instance using Kotlin Logging and log messages at different severity levels. The curly braces {} around the message allow for lazy evaluation, which means the message is only evaluated if the logging level is enabled. This can improve performance, especially for complex messages that involve string concatenation or other expensive operations. Proper configuration of log levels is crucial. Setting the log level too high (e.g., only logging errors) can obscure important information, while setting it too low (e.g., logging everything at the TRACE level) can generate excessive log data.

To ensure consistency, it’s a good practice to define a logger instance for each class or module in your application. This makes it easier to identify the source of log messages and track down issues. You can also use context-specific information, such as user IDs or request IDs, to enrich your log messages and make them more informative. Libraries like MDC (Mapped Diagnostic Context) in SLF4J allow you to add contextual information to your logs without modifying the log message itself. This is particularly useful in multi-threaded environments where you need to track the flow of execution across different threads. One way to improve logging is to implement automated log analysis tools to parse and highlight common issues.

Advanced Logging Techniques

Beyond basic logging, there are several advanced techniques you can use to enhance your logging in Kotlin. These include structured logging, asynchronous logging, and custom log appenders. Structured logging involves formatting your log messages as structured data, such as JSON, rather than plain text. This makes it easier to parse and analyze your logs programmatically using tools like Elasticsearch and Kibana. Asynchronous logging allows you to offload the actual writing of log messages to a separate thread, preventing it from blocking the main thread and improving application performance. Custom log appenders allow you to send your log messages to different destinations, such as databases, message queues, or cloud-based logging services.

Asynchronous logging can significantly improve performance, especially in high-throughput applications. By offloading the logging operation to a separate thread, you can minimize the impact on the main thread and prevent it from being blocked by slow I/O operations. Logback, for example, provides built-in support for asynchronous logging through its element. Custom log appenders offer even greater flexibility, allowing you to send your logs to any destination you can imagine. You can write custom appenders to integrate with third-party services, implement custom filtering logic, or perform other advanced logging tasks. According to research by Gartner, organizations leveraging advanced logging techniques experience a 20% reduction in incident resolution time. Source: Gartner

Consider the following points for effective advanced logging:

  • Use structured logging formats like JSON for easier parsing and analysis.
  • Implement asynchronous logging to improve application performance.
  • Leverage custom log appenders to send logs to various destinations.

For example, consider logging exceptions with detailed stack traces. This is crucial for debugging. Ensure that sensitive information is masked or removed from logs to comply with privacy regulations. Properly configured, advanced logging techniques provide a significant advantage in monitoring and maintaining Kotlin applications.

Best Practices for Logging in Kotlin

To ensure that your logging is effective and maintainable, it’s important to follow some best practices. These include using meaningful log messages, logging at the appropriate severity level, avoiding excessive logging, and properly configuring your logging framework. Meaningful log messages should provide enough context to understand what’s happening in your application without being overly verbose. Log at the appropriate severity level to avoid flooding your logs with irrelevant information or missing important events. Avoid excessive logging, as this can degrade performance and make it difficult to find the information you need. Properly configure your logging framework to ensure that log messages are formatted consistently and stored in a way that makes them easy to analyze. This paragraph is optimized as a featured snippet: The best practices for logging in Kotlin include using meaningful log messages to provide context, logging at the appropriate severity level to avoid flooding logs with irrelevant information, avoiding excessive logging to maintain performance, and properly configuring the logging framework for consistent formatting and easy analysis.

Here are some additional best practices to consider:

  • Use consistent formatting for log messages.
  • Include context-specific information in your logs.
  • Regularly review and prune your log configurations.

Consider implementing a centralized logging solution. Centralized logging solutions aggregate logs from multiple sources into a single, searchable repository. This makes it easier to correlate events across different parts of your application and identify the root cause of problems. Tools like Elasticsearch, Logstash, and Kibana (the ELK stack) are commonly used for centralized logging. Another crucial point is to document your logging strategy. A well-documented logging strategy helps ensure that everyone on your team understands how logging is implemented and how to use it effectively. Use the following steps to establish a logging strategy:

  1. Define your logging requirements.
  2. Choose a logging framework.
  3. Configure your logging framework.
  4. Implement basic logging.
  5. Implement advanced logging techniques.
  6. Establish best practices.

Adhering to these guidelines will significantly enhance the effectiveness of your Kotlin logging implementation. Proper logging will save time and resources in debugging and maintenance.

Infographic here
FAQ: Logging in Kotlin ----------------------
What is the best logging framework for Kotlin?
There is no single "best" framework, but popular choices include SLF4J with Logback, and Kotlin Logging, each offering different features and advantages. Consider your project's specific needs.
How can I improve logging performance in Kotlin?
Use asynchronous logging to offload logging operations to a separate thread, and avoid excessive logging or complex string manipulations within log messages.
What is structured logging, and why is it useful?
Structured logging involves formatting log messages as structured data (e.g., JSON), making it easier to parse and analyze logs programmatically.
How do I add context-specific information to my logs?
Use MDC (Mapped Diagnostic Context) in SLF4J to add contextual information to your logs without modifying the log message itself.
Effective logging is more than just writing messages to a console; it's about creating a system that helps you understand and maintain your application. By choosing the right logging framework, implementing basic and advanced logging techniques, and following best practices, you can create a robust and informative logging system that will save you time and effort in the long run. Remember to regularly review your logging configurations and adapt them to the evolving needs of your application. Consider exploring [Kotlin coroutines](https://courthousezoological.com/n7sqp6kh?key=e6dd02bc5dbf461b97a9da08df84d31c) for potentially enhanced asynchronous logging capabilities. Take these insights and start refining your logging strategy today, ensuring your Kotlin applications are easier to debug, monitor, and ultimately, more reliable. [Learn more about Kotlin](https://kotlinlang.org/) and its features for modern development. [Explore SLF4J for flexible logging](https://www.slf4j.org/).

Question & Answer :

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?

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.