Groovy Logging
last modified October 15, 2026
println is fine for a quick script, but programs that run for a
long time, or that run without a person watching, need logs. A log record has a
level, such as info or error, a timestamp, and the name of the class that wrote
it. Those fields let you filter output and find problems later. Groovy provides
two annotations that add a logger to a class: @Log uses the logging
built into the JDK, and @Slf4j uses the SLF4J API, which most
production applications use with a backend such as Logback. This tutorial shows
both.
The @Log Annotation
@Log, from groovy.util.logging, adds a
log field to the class. The field is a
java.util.logging.Logger, so no extra library is needed.
import groovy.util.logging.Log
@Log
class Downloader {
void fetch(String url) {
log.info("Fetching $url")
log.fine("Detailed step for $url")
if (url.contains('bad')) {
log.warning("Suspicious URL: $url")
}
}
}
new Downloader().fetch('https://example.com/a')
new Downloader().fetch('https://bad.example.com/b')
The log field is generated by the annotation, so the code only
calls its methods. Each call names a level: fine is for debugging
detail, info for normal events, and warning for
unexpected but recoverable situations. fine messages are not shown
by default, because the default level is INFO.
log.fine("Detailed step for $url")
The JDK levels are FINEST, FINER, FINE,
CONFIG, INFO, WARNING, and
SEVERE. Their methods are named in lower case, as shown above.
The default JDK formatter prints the timestamp and the Groovy internal method that made the call, which is rarely useful. You can replace it with a format string that prints only the level and the message, by setting a system property when the script starts:
The format uses %4$s for the level and %5$s for the
message. The backslash before $ stops the shell from expanding the
placeholder. The standard formats are described in the
SimpleFormatter documentation.
$ groovy -Djava.util.logging.SimpleFormatter.format="[%4\$s] %5\$s%n" JulLog.groovy [INFO] Fetching https://example.com/a [INFO] Fetching https://bad.example.com/b [WARNING] Suspicious URL: https://bad.example.com/b
Changing the Log Level
A logger writes only the messages at or above its level. The threshold is inherited from the root logger unless it is set for the logger itself. The example below prints the root level and then writes three messages at different levels.
import groovy.util.logging.Log
@Log
class Service {
void work() {
log.fine('fine detail')
log.info('service started')
log.severe('disk almost full')
}
}
println "level of root: ${java.util.logging.Logger.getLogger('').level}"
new Service().work()
The root logger's level is printed first. It is INFO by default,
so fine is suppressed, while info and
severe are written.
$ groovy -Djava.util.logging.SimpleFormatter.format="[%4\$s] %5\$s%n" JulLevel.groovy level of root: INFO [INFO] service started [SEVERE] disk almost full
The @Slf4j Annotation
@Slf4j generates a log field of type
org.slf4j.Logger. SLF4J is only an API: it needs a backend to
write messages. For a standalone script, the simplest backend is
slf4j-simple, which can be added with @Grab. The
@GrabConfig(systemClassLoader = true) line makes SLF4J find that
backend when the script runs.
@GrabConfig(systemClassLoader = true)
@Grab('org.slf4j:slf4j-simple:2.0.13')
import groovy.util.logging.Slf4j
@Slf4j
class Worker {
void run(int n) {
log.info('Starting with n={}', n)
log.debug('Debug detail')
try {
int result = 10 / (n - 2)
log.info('Result is {}', result)
} catch (ArithmeticException e) {
log.error('Division failed for n={}', n, e)
}
}
}
new Worker().run(5)
new Worker().run(2)
SLF4J messages use {} placeholders, which are filled from the
remaining arguments. The message is only built when the level is enabled, so
the placeholder form is cheaper than string concatenation in a loop.
log.error('Division failed for n={}', n, e)
When the last argument is an exception, the logger prints its stack trace after the message. The argument without a placeholder is treated as the throwable.
The debug message is missing because slf4j-simple
shows INFO and above by default. The stack trace is shortened
here; the full trace follows the error line. The int result
declaration truncates the quotient, so 10 / 3 is printed as
3.
$ groovy Slf4jLog.groovy [main] INFO Worker - Starting with n=5 [main] INFO Worker - Result is 3 [main] INFO Worker - Starting with n=2 [main] ERROR Worker - Division failed for n=2 java.lang.ArithmeticException: Division by zero at java.base/java.math.BigDecimal.divide(BigDecimal.java:1862) ...
Choosing a Logger
Use @Slf4j when the code will be part of a larger application,
since the logging backend can be changed later without touching the source.
Use @Log for small tools where the JDK logger is enough and no
extra dependency is wanted. Both annotations generate the same kind of
log field, so switching between them is a small change.
| Annotation | Library | Levels | Placeholders |
|---|---|---|---|
@Log | JDK (java.util.logging) | fine, info, warning, severe, ... | No, use string interpolation |
@Slf4j | SLF4J API (needs a backend) | debug, info, warn, error | Yes, {} |
Best Practices
- Log through the logger, not
println. A logger gives you levels, timestamps, and a way to turn output off in production. - Use the right level. Routine events are
info, diagnostic detail isdebugorfine, and failures areerrororsevere. - Use placeholders with SLF4J. Write
log.info('n={}', n)rather than building the string yourself. - Pass the exception as the last argument. The stack trace is then kept in the log instead of being lost.
- Keep configuration out of the code. Set levels and formats in the backend's configuration file or with system properties.
Source
This tutorial showed how to add a logger with @Log and
@Slf4j, write messages at different levels, use SLF4J placeholders,
log exceptions, and configure the output format for the JDK logger.
Author
List all Groovy tutorials.