Minimal overhead logging for libGDX applications.
Many Java logging frameworks provide string templates to avoid creation of unnecessary String
objects and (potentially
costly) toString()
calls. Most Java programmers should recognize the syntax immediately:
log.info("Message: {}, from: {}, at: {}", message, user, date)
. Still, vararg methods used in the process create
new object arrays for each logging call - even if the logging level is turned off, and the message is never formatted
and logged. Formatting itself also features some small overhead - every message has to be processed and built.
While libGDX does address the problem of cross-platform logging, but does not provide any mechanism for handling logs
with multiple custom parameters. (Possibly because of the vararg method call overhead.) Even its Logger
utility
class provides no methods consuming optional lazy-evaluated arguments - nor does it log class name and current time.
The most efficient way of logging with the default libGDX mechanisms would look somewhat like this:
if (Gdx.app.logLevel >= Application.LOG_DEBUG) Gdx.app.debug("someTag", "My message: $someObject")
In the example above, no new strings (or argument arrays) are created in the process if debug logs are turned off.
A smart JVM would likely notice that log level never changes and completely remove the calls at runtime. However,
this is a pretty verbose way of logging. Fortunately, Kotlin allows us to avoid verbosity with inline
functions.
ktx-log
introduces methods with inlined lambdas to address the problems of unnecessary string building and vararg calls.
They expect a simple function returning a String
instance - the function will be inlined during the compilation (a new
function object is never created) and proceeded with a check of current logging level. If the logging level is turned
off, the string will never be built.
debug
, info
and error
methods can be used to log messages in libGDX applications. If no tag is given, they will
log the exception under "DEBUG"
, "INFO"
or "ERROR"
tags (matching their logging level). Every method supports
an exception parameter, which is used to print the stack traces.
Logger
class is a thin wrapper over the usual libGDX logging API. It remembers its tag and is usually more convenient
to use than global logging methods, as you know the exact source of the logs. It has 2 operator invoke
methods - one
with and one without an exception parameter - which allows for logger { "Message." }
syntax as a synonym for info logging.
With the logger
factory method, you can create logger instances as easily as logger<MyClass>()
.
Note that Logger
instances are not cached. While its relatively cheap to create new loggers, you should keep in mind
that it is best to store them in companion objects ("static" variable equivalent).
Global message logging:
import ktx.log.*
debug { "Example." }
info { "Example." }
error { "Example." }
Global message logging with custom logging tags:
import ktx.log.*
debug("TRACE") { "Example." }
info("WARN") { "Example." }
error("FATAL") { "Example." }
Global message logging with parameters (String
built only if the respective logging level is turned on):
import ktx.log.*
debug { "Found ${entities.size} entities in ${id} category." }
info { "Found ${entities.size} entities in ${id} category." }
error { "Found ${entities.size} entities in ${id} category." }
Global exception logging:
import ktx.log.*
try {
problematicMethod()
} catch (exception: Exception) {
debug(exception) { "Error!" }
info(exception) { "Error!" }
error(exception) { "Error!" }
}
Getting logger for a specific class:
import ktx.log.*
val log = logger<MyClass>()
Using static logger instance for class (recommended):
import ktx.log.*
class MyClass {
companion object {
val log = logger<MyClass>()
}
fun sayHello() {
log.info { "Hello!" }
}
}
Logging messages with a logger:
import ktx.log.*
val log = logger<MyClass>()
try {
log.debug { "Example." }
log.info { "Found ${entities.size} entities in ${id} category." }
log { "Equivalent to info logging." }
} catch (exception: Exception) {
log.error(exception) { "Error! Unexpected category: ${id}." }
}
Extending Logger
implementation with a custom message format - prepending date to logged messages:
import java.util.Date
import ktx.log.Logger
/** Logs current date additionally to the log message.
* Uses "FATAL" tag for error messages. */
class TimeLogger(name: String) : Logger(name, errorTag = "FATAL") {
// Logger message format: Logger name [current date] Original message.
override fun buildMessage(message: String): String = "$name [${Date()}] $message"
}
/** Creates loggers that include current time in logs. */
inline fun <reified T : Any> myLogger(): Logger = TimeLogger(T::class.java.name)
// Usage:
class MyClass {
val logger = myLogger<MyClass>()
fun example() {
logger.info { "Works like the usual logger." }
}
}
- SLF4J is a common logging facade for Java applications. Note that each libGDX platform would most likely require a separate logging library, as SLF4J (clearly) does not rely on libGDX logging mechanism and there seem to be no cross-platform SLF4J implementation for libGDX applications (yet).
- Kotlin logging is an extension of SLF4J for Kotlin applications. It relies on a similar inlining mechanism to avoid runtime overhead of logging.
- Kiwi contains an alternative logging mechanism that wraps around libGDX cross-platform logging. It can include class names and current time in logs. Similarly to SLF4J, it uses string templates to avoid creation of new strings (unless necessary). However, it still suffers from vararg method calls.