# Logging

Logging captures information from a running program.

For example, an installation program might log:

  • The steps taken during setup.
  • The directories for file storage.
  • Startup values for the program.

A web server might log the origin address and status of each request.

Logging is also helpful during debugging. Without logging, you might decipher the behavior of a program using println() statements. This can be helpful in the absence of a debugger (such as the one built into IntelliJ IDEA). However, once you decide the program is working properly, you’ll probably take the println() statements out. Later, if you run into more bugs, you might put them back in. In contrast, logging can be dynamically enabled when you need it, and turned off otherwise.

For some failures you can only report the issue. A program that recovers from some types of errors (as shown in [Exception Handling](javascript:void(0))) can log details about those errors for later analysis. In a web application, for example, you don’t terminate the program if something goes wrong. Logging captures these events, giving programmers and administrators a way to discover the problems. Meanwhile, the application continues running.

We use an open-source logging package designed for Kotlin called Kotlin-logging (opens new window), which has the feel and simplicity of Kotlin. Note that there are other logging packages to choose from.

You must create a logger before using it. You’ll almost always want to create it at file scope so it’s available to all components in that file:

// Logging/BasicLogging.kt
package logging
import mu.KLogging

private val log = KLogging().logger

fun main() {
  val msg = "Hello, Kotlin Logging!"
  log.trace(msg)
  log.debug(msg)
  log.info(msg)
  log.warn(msg)
  log.error(msg)
}

main() shows the different logging levels: trace(), debug() and info() capture behavioral information, while warn() and error() indicate problems.

Start-up configuration determines the logging levels that are actually reported. This can be modified during execution. Operators of long-running applications can change the logging level without restarting the program (which is often unacceptable).

Logging libraries have a rather odd history. People were dissatisfied with the original logging library distributed with Java, so they created other libraries. In an attempt to unify logging, designers began developing common logging interfaces. Acknowledging that organizations may be invested in existing logging libraries, those interfaces were created as facades for multiple different logging libraries. Later, other programmers created (presumably improved) facades over those facades. Utilizing a logging system often means choosing a facade, then choosing one or more underlying implementations.

The Kotlin-logging library is a facade over the Simple Logging Facade for Java (SLF4J) (opens new window), which is an abstraction over multiple logging frameworks. You choose the framework that meets your needs—although it is more likely that the operations group in your company will make that decision, as they are the ones that usually manage logging and analyze the resulting log files.

For this example we use slf4j-simple as our implementation. This comes as part of SLF4J and thus we are not required to install or configure an additional library—some libraries have an annoying amount of setup complexity. slf4j-simple sends its output to the console error stream. When you run the program, you see:

[main] INFO mu.KLogging - Hello, Kotlin Logging!
[main] WARN mu.KLogging - Hello, Kotlin Logging!
[main] ERROR mu.KLogging - Hello, Kotlin Logging!

trace() and debug() produce no output because the default configuration doesn’t report those levels. To get different reporting levels, change your logging configuration. Logging configuration varies depending on the logging package you’re using, so we don’t talk about it here.

Logging implementations that log to files often manage those log files by automatically discarding the oldest parts when files get too large. There are additional tools designed to read and analyze log files. The practice of logging can require fairly involved research.

For basic problems, the work of installing, configuring, and using a logging system might tempt you back to println() statements. Fortunately, there are easier strategies.

The quick-and-dirty approach is to define a global function. This can easily be disabled when you don’t need it:

// Logging/SimpleLoggingStrategy.kt
package logging
import checkinstructions.DataFile

val logFile = // Reset ensures an empty file:
  DataFile("simpleLogFile.txt").reset()

fun debug(msg: String) =
  System.err.println("Debug: $msg")
// To disable:
// fun debug(msg: String) = Unit

fun trace(msg: String) =
  logFile.appendText("Trace: $msg\n")

fun main() {
  debug("Simple Logging Strategy")
  trace("Line 1")
  trace("Line 2")
  println(logFile.readText())
}
/* Sample Output:
Debug: Simple Logging Strategy
Trace: Line 1
Trace: Line 2
*/

debug() sends its output to the console error stream. trace() sends its output to a log file.

You can also create your own simple logging class:

// Logging/AtomicLog.kt
package atomiclog
import checkinstructions.DataFile

class Logger(fileName: String) {
  val logFile = DataFile(fileName).reset()
  private fun log(type: String, msg: String) =
    logFile.appendText("$type: $msg\n")
  fun trace(msg: String) = log("Trace", msg)
  fun debug(msg: String) = log("Debug", msg)
  fun info(msg: String) = log("Info", msg)
  fun warn(msg: String) = log("Warn", msg)
  fun error(msg: String) = log("Error", msg)
  // For basic testing:
  fun report(msg: String) {
    trace(msg)
    debug(msg)
    info(msg)
    warn(msg)
    error(msg)
  }
}

You can add support for other features like logging levels and time stamps.

Using the library is straightforward:

// Logging/UseAtomicLog.kt
package useatomiclog
import atomiclog.Logger
import atomictest.eq

private val logger = Logger("AtomicLog.txt")

fun main() {
  logger.report("Hello, Atomic Log!")
  logger.logFile.readText() eq """
  Trace: Hello, Atomic Log!
  Debug: Hello, Atomic Log!
  Info: Hello, Atomic Log!
  Warn: Hello, Atomic Log!
  Error: Hello, Atomic Log!
  """
}

It’s tempting to create yet another logging library. This is probably not a good use of time.

  • -

Logging is not as simple as calling library functions—there’s a significant run-time component. Logging is typically included in the deliverable product, and operations people must be able to turn logging on and off, dynamically adjust logging levels, and control the logfiles. For long-running programs such as servers, this last issue is particularly important because it includes strategies to prevent logfiles from filling up.

Exercises and solutions can be found at www.AtomicKotlin.com.