Skip to main content

Sending log events

Once you have a logger, you can call functions on it to send log events. The base function is log():

    logger.log(Level.INFO, "Application started")
info

All the information here applies to both the coroutine logger Klogger and the non-coroutine logger NoCoLogger.

Utility functions

These utility functions are a convenient way to send logs. They call log() with the appropriate level:

  • trace()
  • debug()
  • info()
  • warn()
  • error()
  • fatal()

Logging patterns

Klogging offers a range of patterns for different logging scenarios. The patterns shown here apply to log() and all utility functions.

String message

The simplest pattern is to send a string, for example:

    logger.info("Processing started")
// meaningful code
val response = callOtherService()
logger.info("User response was ${response.text}")

Message template

Message templates provide a convenient way to both create meaningful messages and to create structured log events:

    logger.info("User {userId} signed in", userId)

The resulting log event contains both:

  • the message populated with the value of userId; and
  • an item called userId with the same value.

For example, if userId has the value wonti321 then:

  • the message becomes User wonti321 signed in; and
  • the log event contains "userId": "wonti321".

The log event displayed in Splunk may look like this:

Example of structured event message in Splunk

[JVM] Klogging supports the @ destructuring operator that destructures objects into their components when serialised as JSON for sending to Seq, ElasticSearch or Splunk. A simple example is:

data class User(val id: Long, val name: String)

// Other code

val user = User(61733972217, "Neville")
logger.info("User {@user} signed in", user)

The event in Seq may look like this:

Example of destructured object message in Seq

The exported JSON from Seq may look like this:

{
"@t": "2025-02-16T11:23:34.6534440Z",
"@mt": "User {@user} signed in",
"@m": "User {\"id\":61733972217,\"name\":\"Neville\",\"$type\":\"User\"} signed in",
"@i": "bef02ae7",
"context": "main",
"host": "MikeBook.local",
"logger": "Destructuring",
"user": {
"$type": "User",
"id": 61733972217,
"name": "Neville"
}
}

Nested objects are destructured as deep as they nest.

Immediate context items

You can specify a map of items to add to the context of a single log event.

info

Immediate context items are useful with NoCoLogger instances, where there is no coroutine context available.

A simple example:

logger.info("Retrieved values from services", mapOf(
"serviceOneCount" to serviceOneResult.count,
"serviceTwoCount" to serviceTwoResult.count,
))

The resulting log event may look like something like this:

{
"@t": "2024-02-05T07:45:47.837117Z",
"@l": "INFO",
"@m": "Retrieved values from services",
"host": "589ef8fa",
"logger": "com.example.ServiceCombiner",
"context": "main",
"serviceOneCount": 9,
"serviceTwoCount": 17
}

Exception

To log exception information, include the exception object as the first argument in the function call:

    try {
// This might throw an exception
snurgle(id, agger)
} catch (ex: SnurgleException) {
logger.warn(ex, "Exception calling snurgle with id={id} and agger={agger}", id, agger)
}

The resulting log event will include any stack trace included in the exception.

Minimum level check

Every logger has a minimum level set by configuration, below which log events are not sent. See Log levels and checking for more details.

Klogging provides functions to check minimum levels to call before calling a logging function if the values to be logged are expensive to obtain.

Here is one example, where DEBUG logging might not be enabled for this logger in all environments:

    if (logger.isDebugEnabled()) {
val complex = calculateSpecialValueForDebugging()
val other = extractOtherValueFromSomewhereElse()
logger.debug("Calculated {complex} and {other}", complex, other)
}

Likewise, there are functions isTraceEnabled(), isInfoEnabled() etc.

Kotlin lambda

The idiomatic Kotlin way to send a log event that might be expensive is to use a lambda. The lambda is only called if the minimum level check evaluates true.

It is simple when logging a string message:

    logger.trace { "Starting run with ID=$runId" }

Here, a log event is only sent if logger.isTraceEnabled() evaluates true.

The lambda can only return a single value, so if you want to use a message template, you can call the e() function as the return value of the lambda:

    logger.debug {
val complex = calculateSpecialValueForDebugging()
val other = extractOtherValueFromSomewhereElse()
e("Calculated {complex} and {other}", complex, other)
}

Lambda and exception

Combine a lambda and an exception like this:

    logger.warn(ex) { "Exception processing payment" }