Question
Idiomatic Logging in Kotlin: Companion Objects, Properties, and Best Practices
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
staticfield. - In Kotlin, the
companion objectis 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.
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 objectholds values shared by the classprivate valmeans the logger cannot be changed and is only visible inside the classFoo::class.javagets the JavaClassobject 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
- The
PaymentServiceclass is loaded. - Its
companion objectis created. - The
logproperty is initialized once usingLoggerFactory.getLogger(PaymentService::class.java). - When
process(100)is called, the method uses the sharedloginstance. - The logging framework records a message associated with the
PaymentServiceclass.
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
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
| Approach | Kotlin Example | Shared at Class Level? | Typical Use |
|---|---|---|---|
| Companion object logger | companion object { private val log = LoggerFactory.getLogger(Foo::class.java) } | Yes | Most common idiomatic class-level logging |
| Instance property logger | private val log = LoggerFactory.getLogger(javaClass) | No | Sometimes used when subclass runtime type matters |
| Top-level logger | private val log = LoggerFactory.getLogger("MyFile") | File-level | Useful for top-level functions or singleton utilities |
| Helper function | private val log = logger<Foo>() | Usually yes | Reduces 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
javaClassfor runtime type - less common for ordinary service classes
Kotlin vs Java style
| Java | Kotlin |
|---|---|
private static final Logger LOG = LoggerFactory.getLogger(Foo.class); | companion object { private val log = LoggerFactory.getLogger(Foo::class.java) } |
Uses static | Uses companion object |
Uses final | Uses val |
Cheat Sheet
import org.slf4j.LoggerFactory
class Foo {
companion object {
private val log = LoggerFactory.getLogger(Foo::class.java)
}
}
Quick rules
- Use
companion objectfor a class-level logger - Use
val, notvar - Usually make the logger
private - Use
Foo::class.javato 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.
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
UserRegistrationServiceclass. - 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.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.