ZetCode

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.

JulLog.groovy
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.

JulLevel.groovy
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.

Slf4jLog.groovy
@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.

AnnotationLibraryLevelsPlaceholders
@LogJDK (java.util.logging)fine, info, warning, severe, ...No, use string interpolation
@Slf4jSLF4J API (needs a backend)debug, info, warn, errorYes, {}

Best Practices

Source

Groovy documentation

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than eight years of experience in teaching programming.

List all Groovy tutorials.