Question

Idiomatic Logging in Kotlin: Companion Objects, Properties, and Best Practices

kotlinkotlin-logging

Question

In Java, a common logging pattern is to define a class-level static logger like this:

public class Foo {
    private static final Logger LOG = LoggerFactory.getLogger(Foo.class);
}

Kotlin does not use static fields in the same way as Java. What is the idiomatic way to perform logging in Kotlin, and how can you define a logger that is reused at the class level?

Short Answer

By the end of this page, you will understand how Kotlin handles class-level logging without Java-style static fields. You will learn the most common Kotlin logging patterns, when to use a companion object, how top-level or delegated approaches can help, and what developers typically do in real Kotlin codebases.

Concept

In Java, logging is often stored in a static final field so that one logger instance is shared by the class. Kotlin does not have static members in the same form, but it provides several alternatives that serve the same purpose.

The most common Kotlin replacement is a property inside a companion object. A companion object belongs to the class rather than to individual instances, so it is the closest equivalent to a Java static member.

For example:

class Foo {
    companion object {
        private val log = LoggerFactory.getLogger(Foo::class.java)
    }
}

This matters because logging is usually:

  • shared by all instances of a class
  • created once and reused
  • tied to the class name for filtering and debugging

In real programs, loggers help you:

  • track application flow
  • record errors and warnings
  • debug production issues
  • monitor important events

In Kotlin, the goal is not to imitate Java syntax exactly, but to use Kotlin features to express the same idea clearly and safely.

Mental Model

Think of a logger as a notebook assigned to a class.

  • If every object instance had its own notebook, you would waste space and repeat setup.
  • Instead, the whole class shares one notebook.
  • In Java, that shared notebook is often stored in a static field.
  • In Kotlin, the companion object is the shared shelf where that notebook lives.

So a companion object is like a shared storage area for things that belong to the class itself, not to each object created from the class.

Take Quiz

Syntax and Examples

The most common Kotlin logging syntax uses a companion object.

import org.slf4j.Logger
import org.slf4j.LoggerFactory

class Foo {
    companion object {
        private val log: Logger = LoggerFactory.getLogger(Foo::class.java)
    }

    fun doWork() {
        log.info("Work started")
    }
}

Why this works

  • companion object holds values shared by the class
  • private val means the logger cannot be changed and is only visible inside the class
  • Foo::class.java gets the Java Class object required by many logging libraries

Another common style

Some developers put the logger directly in the companion object without explicitly writing the type:

class Foo {
    companion object {
        private val log = LoggerFactory.getLogger(Foo::class.java)
    }
}

Kotlin can infer the type automatically.

Using an extension or helper

In larger projects, developers often create a helper to avoid repeating the same logger setup:

import org.slf4j.Logger
import org.slf4j.LoggerFactory

inline fun <reified T> logger(): Logger {
    return LoggerFactory.getLogger(T::class.java)
}

class Foo {
    companion object {
        private val log = logger<Foo>()
    }
}

This keeps the class code cleaner while still producing a class-level logger.

Step by Step Execution

Consider this example:

import org.slf4j.LoggerFactory

class PaymentService {
    companion object {
        private val log = LoggerFactory.getLogger(PaymentService::class.java)
    }

    fun process(amount: Int) {
        log.info("Processing payment: {}", amount)
    }
}

What happens step by step

  1. The PaymentService class is loaded.
  2. Its companion object is created.
  3. The log property is initialized once using LoggerFactory.getLogger(PaymentService::class.java).
  4. When process(100) is called, the method uses the shared log instance.
  5. The logging framework records a message associated with the PaymentService class.

Important detail

Even if you create many PaymentService objects, they all use the same logger stored in the companion object.

val a = PaymentService()
val b = PaymentService()

a.process(100)
b.process(200)

Both calls use the same class-level logger.

Real World Use Cases

Logging in Kotlin is used in many real applications.

Web applications

class UserController {
    companion object {
        private val log = LoggerFactory.getLogger(UserController::class.java)
    }

    fun createUser(username: String) {
        log.info("Creating user: {}", username)
    }
}

Useful for tracking incoming requests and important actions.

Background jobs

class EmailJob {
    companion object {
        private val log = LoggerFactory.getLogger(EmailJob::class.java)
    }

    fun run() {
        log.info("Email job started")
    }
}

Useful for cron jobs, schedulers, and batch tasks.

Error reporting

class FileImporter {
    companion object {
        private val log = LoggerFactory.getLogger(FileImporter::class.java)
    }

    fun importFile(path: String) {
        try {
            // import logic
        } catch (e: Exception) {
            log.error("Failed to import file: {}", path, e)
        }
    }
}

Useful for diagnosing failures in production.

Data processing

Logging helps record:

  • how many records were processed
  • where a failure occurred
  • which configuration was used
  • whether a retry happened
Take Quiz

Real Codebase Usage

In real Kotlin projects, developers usually choose one of a few patterns.

1. Companion object logger

This is the most direct replacement for Java static loggers.

class OrderService {
    companion object {
        private val log = LoggerFactory.getLogger(OrderService::class.java)
    }
}

Use this when you want a clear class-level logger with minimal magic.

2. Base helper or generic logger function

Teams often reduce repetition with a helper:

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

class OrderService {
    companion object {
        private val log = logger<OrderService>()
    }
}

This improves consistency across many classes.

3. Instance property logger

Sometimes developers use an instance-level property:

class OrderService {
    private val log = LoggerFactory.getLogger(javaClass)
}

This can work, but it creates the logger through each instance and is less aligned with the usual class-level pattern.

Common patterns around logging

  • Guard clauses: log and return early when input is invalid
  • Validation: log warnings for bad input or missing config
  • Error handling: log exceptions with context
  • Configuration loading: log environment values or startup details
  • Service methods: log key events, not every line of code

Good practice in real projects

  • prefer class-level loggers for services, controllers, and utilities
  • use parameterized logging like log.info("User id: {}", id)
  • avoid expensive string building unless needed
  • include meaningful context in messages

Common Mistakes

1. Creating a logger per instance when a class-level logger is enough

Broken style:

class Foo {
    private val log = LoggerFactory.getLogger(Foo::class.java)
}

This is not always wrong, but it is usually unnecessary if all instances can share one logger.

Better:

class Foo {
    companion object {
        private val log = LoggerFactory.getLogger(Foo::class.java)
    }
}

2. Using the wrong class reference

Broken code:

class Foo {
    companion object {
        private val log = LoggerFactory.getLogger(String::class.java)
    }
}

This logger will be named for String, not Foo.

Better:

class Foo {
    companion object {
        private val log = LoggerFactory.getLogger(Foo::class.java)
    }
}

3. Building log messages manually

Less ideal:

log.info("User id: " + userId)

Preferred with SLF4J:

log.info("User id: {}", userId)

Parameterized logging is cleaner and often more efficient.

4. Logging too much

Beginners often log every small step. That creates noisy logs.

Avoid:

  • logging inside tight loops unless necessary
  • logging sensitive data like passwords or tokens
  • writing vague messages like Something happened

5. Forgetting visibility and immutability

Prefer:

private val log = LoggerFactory.getLogger(Foo::class.java)

This prevents accidental reassignment or external access.

Comparisons

ApproachKotlin ExampleShared at Class Level?Typical Use
Companion object loggercompanion object { private val log = LoggerFactory.getLogger(Foo::class.java) }YesMost common idiomatic class-level logging
Instance property loggerprivate val log = LoggerFactory.getLogger(javaClass)NoSometimes used when subclass runtime type matters
Top-level loggerprivate val log = LoggerFactory.getLogger("MyFile")File-levelUseful for top-level functions or singleton utilities
Helper functionprivate val log = logger<Foo>()Usually yesReduces repetition in larger codebases

Companion object vs instance logger

  • Companion object

    • one logger per class
    • closest to Java static pattern
    • simple and explicit
  • Instance logger

    • tied to object instances
    • can use javaClass for runtime type
    • less common for ordinary service classes

Kotlin vs Java style

JavaKotlin
private static final Logger LOG = LoggerFactory.getLogger(Foo.class);companion object { private val log = LoggerFactory.getLogger(Foo::class.java) }
Uses staticUses companion object
Uses finalUses val
Take Quiz

Cheat Sheet

import org.slf4j.LoggerFactory

class Foo {
    companion object {
        private val log = LoggerFactory.getLogger(Foo::class.java)
    }
}

Quick rules

  • Use companion object for a class-level logger
  • Use val, not var
  • Usually make the logger private
  • Use Foo::class.java to pass the class to Java logging libraries
  • Prefer parameterized messages:
    • log.info("User id: {}", id)
    • not log.info("User id: " + id)

Useful helper

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

Usage:

class Foo {
    companion object {
        private val log = logger<Foo>()
    }
}

When not to use companion object

If you specifically need the runtime subclass type, an instance-based logger using javaClass may be useful:

open class BaseService {
    protected val log = LoggerFactory.getLogger(javaClass)
}

Best practice summary

  • default choice: companion object
  • keep logger private
  • log meaningful events
  • do not log secrets
  • include useful context in error logs

FAQ

What is the Kotlin equivalent of a static logger?

The usual equivalent is a val inside a companion object. It gives you one shared logger for the class.

Is companion object the idiomatic way to define a logger in Kotlin?

Yes, it is one of the most common and widely accepted patterns, especially when using Java logging libraries like SLF4J.

Can I create the logger as a normal property instead?

Yes, but that creates an instance property rather than a class-level property. It is usually less desirable unless you specifically need instance-based behavior.

Why do Kotlin examples use Foo::class.java?

Many logging libraries are Java libraries and expect a Java Class object. Foo::class.java converts the Kotlin class reference into the Java form.

Should the logger be private?

Usually yes. Other classes normally do not need direct access to another class's logger.

Should I name the logger log or logger?

Either is fine. log is very common because it is short and readable.

Is there a built-in logger in Kotlin?

No. Kotlin itself does not include a built-in general-purpose logging API. Projects usually use libraries such as SLF4J with Logback or Log4j.

Can I avoid repeating logger setup in every class?

Yes. Many teams create a helper function like logger<T>() or use a logging wrapper library to reduce boilerplate.

Take Quiz

Mini Project

Description

Create a small Kotlin service that logs different events during a user registration flow. This project demonstrates how to define a class-level logger idiomatically and use it for informational messages, warnings, and errors.

Goal

Build a Kotlin class that uses a companion object logger to record what happens during registration.

Requirements

  • Create a UserRegistrationService class.
  • Define a class-level logger using a companion object.
  • Add a register(username: String, email: String) function.
  • Log an info message when registration starts.
  • Log a warning if the username is blank.
  • Log an error if an exception occurs.
Take Quiz

Keep learning

Related questions

View more