Gradle: Directories & Logging

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Gradle 9.8.1 (released on 7 October 2026), with Gradle 8.14.6, the last 8.x release, mentioned where it differs. Every example marked as run was executed with the 9.8.1 wrapper on JDK 26 (Oracle java version "26") on 2026-10-16, on macOS (Apple silicon), in scratch copies of the tidepool build (a root project with tidepool-model, tidepool-engine and tidepool-cli), once with *.gradle.kts files and once with *.gradle files. The directory listings come from a Gradle User Home created for the page (through the GRADLE_USER_HOME environment variable), so the real ~/.gradle was not touched, and from a project that had been built with --configuration-cache. Scratch paths are shown as <work> (the directory of the project in question). Blank lines between the parts of Gradle’s output were dropped in places, and where a long output was cut to the interesting lines the command is shown with the filter that was used. Gradle 8.14.x was not downloaded, so what is said about it comes from the official documentation and release notes and is labelled "not run here"; so is everything that needs time to pass (the effect of a retention period of 30 days) or a download (the jdks directory).

A Gradle build touches two places on disk and one stream of text. The Gradle User Home belongs to the user and is shared by every project: the downloaded dependencies, the wrapper distributions, the daemon registry and logs, the global init scripts and properties. The project root holds the sources, the build state of that project and the generated output. The log is what Gradle prints while it runs, and the options that decide how much of it you see. The page follows the same order. The daemon that lives in the Gradle User Home is explained in The Gradle Daemon; the wrapper that fills the wrapper/dists directory is in Gradle Wrapper; the properties that can sit in the Gradle User Home are in Build Environment & Properties. For the other tools: Maven keeps its state in ~/.m2 and target/ (see Maven: Philosophy, Coordinates & Directory Layout), and Ant has no cache and no user home beyond ~/.ant (see Ant: Running Ant).

The Gradle User Home

By default the Gradle User Home is ~/.gradle (C:\Users\<USERNAME>\.gradle on Windows). It is changed with the environment variable GRADLE_USER_HOME, the system property gradle.user.home, or the command-line option -g / --gradle-user-home. It must not be confused with GRADLE_HOME, the optional directory where a Gradle distribution was unpacked (see Installation); the user home is the one that grows. The official layout is:

~/.gradle
├── caches              (1)
├── daemon              (2)
├── init.d              (3)
├── jdks                (4)
├── wrapper
│   └── dists           (5)
└── gradle.properties   (6)
Entry What it holds

caches

All the caches that are shared between projects: downloaded dependencies and their metadata (modules-2), instrumented JARs (jars-9), the compiled build scripts, the transforms, and so on (next section).

daemon

The registry of the running daemons and their logs, in a directory per Gradle version (see The Gradle Daemon).

init.d

Global initialization scripts, applied to every build of the user (see Init Scripts & the Tooling API). It is the place for the cache settings below.

jdks

The JDKs downloaded by toolchain auto-provisioning (see Toolchains). Not present in the examples because nothing was downloaded.

wrapper/dists

The Gradle distributions the wrapper downloaded, one directory per distribution (see Gradle Wrapper).

gradle.properties

User-level Gradle properties; they take precedence over the project-level file. A global place for org.gradle.jvmargs, proxy settings or credentials that must never be committed.

A real one, created by building tidepool once with the wrapper and a fresh GRADLE_USER_HOME, then adding an init.d directory for the cleanup examples below. The ls commands and what they printed:

$ ls -A $GRADLE_USER_HOME
.tmp
caches
daemon
init.d
native
notifications
wrapper

$ ls -A $GRADLE_USER_HOME/caches
9.8.1
CACHEDIR.TAG
jars-9
journal-1
modules-2

$ ls -A $GRADLE_USER_HOME/wrapper/dists
CACHEDIR.TAG
gradle-9.8.1-bin

$ ls -A $GRADLE_USER_HOME/wrapper/dists/gradle-9.8.1-bin
<hash>

Three entries are not in the official overview: native (native libraries that Gradle extracts for operating-system integration), notifications (the release features text shown after an upgrade) and .tmp. In the last listing the name of the sub-directory is a hash of the distribution URL, printed here as <hash> because the scratch wrapper points at a local file; with the official URL it is a fixed value for that URL, which is why two projects that use the same URL share one unpacked distribution. The size is worth knowing: the wrapper directory with a single -bin distribution of 9.8.1 took 165 MB, and the caches directory grows with every dependency, plugin and transform.

Three of these directories carry a CACHEDIR.TAG file (caches, wrapper/dists and daemon in the run above; the official page lists jdks too). It is a marker defined by the Cache Directory Tagging Specification: backup and archive tools that honour it skip the directory. Gradle writes the tags since 8.1; markingStrategy in an init script can turn them off (below). This is the reason why a backup of the home directory does not need an exclude rule for .gradle/caches.

Putting the Gradle User Home somewhere else

Move it for a CI agent whose home directory is read-only or ephemeral, for a build that must not share caches with others, or to keep many Gradle versions apart:

export GRADLE_USER_HOME=/ci/cache/gradle        # for the whole session
./gradlew -g /ci/cache/gradle build              # for one command (--gradle-user-home)
./gradlew -Dgradle.user.home=/ci/cache/gradle build

(Not run here with a second directory, because it would unpack the distribution again; the examples of this page all used the environment variable.) The wrapper honours the same variable when it unpacks the distribution, unless gradle-wrapper.properties says otherwise through distributionBase and zipStoreBase. A related option is --project-cache-dir (property org.gradle.projectcachedir), which moves the .gradle directory of the project. With --project-cache-dir=../pcd, a build wrote the project cache there instead of in the project:

$ ./gradlew logDemo -q --project-cache-dir=../pcd
$ ls ../pcd
9.8.1
buildOutputCleanup
vcs-1

(With --configuration-cache it would also hold the configuration-cache directory.)

The caches

Gradle keeps three kinds of caches, and the official page separates them by where they live. The listing is from the run above (a Kotlin DSL project and a Groovy DSL project had both been built).

Location Contents

caches/<version>/ (here caches/9.8.1)

Caches tied to one Gradle release, recreated at first use: generated-gradle-jars, kotlin-dsl (compiled Kotlin DSL scripts and accessors) and the compiled Groovy DSL scripts. The official page names the last one scripts; in 9.8.1 the directory that appeared after building a Groovy DSL project was groovy-dsl. The listing also had cc-keystore (the key used to encrypt configuration cache entries), file-changes, fileContent, fileHashes, javaCompile, jvms, md-rule, md-supplier and transforms.

caches/ (shared by all versions)

Directories that end in a format version: modules-2 (downloaded dependencies, see Dependency Locking, Verification & Caching), jars-9 (instrumented JARs), transforms-3 (artifact transform outputs), build-cache-1 (the local build cache, see Build Cache), and journal-1.

.gradle/ in the project root

.gradle/<version> (execution history and file hashes for incremental builds), .gradle/configuration-cache (the serialized task graph, see Configuration Cache & Isolated Projects), .gradle/buildOutputCleanup and .gradle/vcs-1.

The number at the end of a shared directory is the on-disk format version. When Gradle changes a format, the new version gets a new directory (transforms-3 next to transforms-4) and the old one is left until it is cleaned up, so after an upgrade the home grows before it shrinks. In the example ls caches/modules-2 shows files-2.1, metadata-2.107 and modules-2.lock: the files and the resolution metadata have their own format numbers.

The caches are not an API. You can delete the whole caches directory when no Gradle process is using it, and Gradle will download again what it needs; the reason not to do it routinely is that the dependencies come back from the network (see Repositories & Publishing Across Tools). For a clean rebuild use --refresh-dependencies, --rerun-tasks or clean instead (see Command-Line Interface).

The project root

The project root holds the sources and everything Gradle generates for the project. The official overview and what the scratch project (with an added buildSrc directory, and built with the configuration cache) really contained:

$ ls -A                      (project root)
.gradle
build.gradle.kts
buildSrc
data
gradle
gradlew
gradlew.bat
settings.gradle.kts
tidepool-cli
tidepool-engine
tidepool-model

$ ls -A .gradle
9.8.1
buildOutputCleanup
configuration-cache
noVersion
vcs-1

$ ls -A .gradle/9.8.1
checksums
executionHistory
expanded
fileChanges
fileHashes
gc.properties
kotlin-dsl-plugin-entries
vcsMetadata

$ ls -A buildSrc
.gradle
build
build.gradle.kts
src

$ ls tidepool-cli/build
classes
distributions
generated
libs
scripts
tmp
Path Role and version control

gradle/wrapper/, gradlew, gradlew.bat

The wrapper: commit them (see Gradle Wrapper).

settings.gradle(.kts), build.gradle(.kts), gradle.properties, gradle/libs.versions.toml

The build definition: commit. gradle.properties in the project holds settings, not secrets.

.gradle/

The project cache: execution history, file hashes, the configuration cache. Do not commit; it is an optimisation that Gradle rebuilds. Gradle also cleans it (below). noVersion holds state that is not tied to a Gradle version.

<project>/build/

The output of that project (classes, JARs, reports). Do not commit. clean deletes it.

buildSrc/

A build that Gradle compiles first and puts on the classpath of the main build: the original place for shared build logic, with its own .gradle and build. The official guidance now prefers a named included build (see below). See Convention Plugins & Sharing Build Logic.

build-logic/

By convention the name of an included build (pluginManagement { includeBuild("build-logic") } in the settings) that holds convention plugins. Gradle gives the name no special meaning; unlike buildSrc it is not picked up automatically.

A .gitignore that matches this table needs only two lines for Gradle (the build directory of each subproject is matched by the pattern):

.gradle/
build/

The project cache is also cleaned: since Gradle 4.10 the version-specific directories in .gradle/<version> are checked at most every 24 hours after a build, and deleted when they have not been used for seven days.

Cleanup and retention

The caches would grow without limit, so Gradle cleans the Gradle User Home itself. By default the cleanup runs in the background when the daemon is stopped or shuts down, and in the foreground after the build session if the build uses --no-daemon. It runs periodically, at most once every 24 hours, and applies these rules (from the official page):

  • version-specific caches in caches/<version> that are no longer in use: released versions are deleted after 30 days of inactivity, snapshots after 7 days;

  • shared caches (jars-*, transforms-*) that no Gradle version uses any more are deleted;

  • files in the shared caches the current version uses are deleted when they were not accessed for 7 days if Gradle can re-create them locally, or for 30 days if they must be downloaded again;

  • distributions in wrapper/dists that no version-specific cache corresponds to are deleted;

  • daemon logs in daemon/<version> that were last modified more than 14 days ago are deleted.

The retention periods have been configurable since Gradle 8.0 (before that they were fixed: 30 or 7 days depending on the cache). Gradle 9.8.1 has six categories, each with its own default; the first four exist since 8.0, buildCache since 8.8 and daemonLogs since 9.4.0 (the Since entries of the CacheConfigurations Javadoc):

Category (property of caches) Default Applies to

releasedWrappers

30 days

Released distributions and their version-specific caches.

snapshotWrappers

7 days

Snapshot and nightly distributions.

downloadedResources

30 days

Caches filled from remote repositories, such as modules-2.

createdResources

7 days

Caches that Gradle creates during the build, such as transforms.

buildCache (since 8.8)

7 days

The local build cache build-cache-1.

daemonLogs (since 9.4.0)

14 days

The daemon-<pid>.out.log files.

The settings can only be made in an init script in init.d of the Gradle User Home, so they are tied to the home they apply to and a project cannot override a shared home. The script below uses longer retentions for the downloads and the wrapper distributions and a shorter one for the build cache. The Kotlin DSL file and the Groovy DSL file were each placed in init.d of the scratch Gradle User Home with a second file that prints one line, to prove that the directory is applied; the build succeeded with both and printed the line, so the accessors exist and accept the values:

Kotlin DSL
// $GRADLE_USER_HOME/init.d/cache-settings.init.gradle.kts
beforeSettings {
    caches {
        releasedWrappers.setRemoveUnusedEntriesAfterDays(45)
        snapshotWrappers.setRemoveUnusedEntriesAfterDays(10)
        downloadedResources.setRemoveUnusedEntriesAfterDays(45)
        createdResources.setRemoveUnusedEntriesAfterDays(10)
        buildCache.setRemoveUnusedEntriesAfterDays(5)
        daemonLogs.setRemoveUnusedEntriesAfterDays(14)
        cleanup = Cleanup.DEFAULT
    }
}
Groovy DSL
// $GRADLE_USER_HOME/init.d/cache-settings.init.gradle
beforeSettings { settings ->
    settings.caches {
        releasedWrappers.removeUnusedEntriesAfterDays = 45
        snapshotWrappers.removeUnusedEntriesAfterDays = 10
        downloadedResources.removeUnusedEntriesAfterDays = 45
        createdResources.removeUnusedEntriesAfterDays = 10
        buildCache.removeUnusedEntriesAfterDays = 5
        daemonLogs.removeUnusedEntriesAfterDays = 14
        cleanup = Cleanup.DEFAULT
    }
}
$ ls $GRADLE_USER_HOME/init.d
cache-settings.init.gradle.kts
hello.init.gradle.kts
$ ./gradlew help --task logDemo
init.d/hello.init.gradle.kts runs before every build that uses this Gradle User Home

> Task :help
Detailed task information for logDemo

Path
     :logDemo (registered in build file 'build.gradle.kts')
...

The effect of the periods themselves (an unused distribution disappearing after 45 days) was not observed: it needs time to pass and a cache that is genuinely unused, so it is not run here. Besides the periods, cleanup takes three values: Cleanup.DEFAULT (periodic, in the background), Cleanup.DISABLED (never; for an ephemeral home, or when the cleanup should happen at a point you choose) and Cleanup.ALWAYS (at the end of every build session, in the foreground, which can be expensive). With Cleanup.ALWAYS and --info or --debug no line announcing a cleanup of these caches was found in the output or in the daemon log in this run. The local build cache is different: with --build-cache and --info it logs its own "removing files not accessed" and "cleanup deleted" lines, as shown in Gradle Build Cache.

Two version rules matter on a machine that builds with several Gradle versions. The version-specific caches are cleaned by the version that owns them, but the shared ones are cleaned by everyone, and Gradle before 8.0 has fixed periods: a longer retention configured for 8.x can be undone by an older version in the same home. The DSL elements do not exist before 8.0, so a script in init.d that is shared with older versions must guard them (the official page applies a second script from a sub-directory only when GradleVersion.current() is at least 8.0, because every script directly in init.d is applied):

Kotlin DSL
// $GRADLE_USER_HOME/init.d/cache-settings.init.gradle.kts
if (GradleVersion.current() >= GradleVersion.version("8.0")) {
    apply(from = "gradle8/cache-settings.init.gradle.kts")
}
Groovy DSL
// $GRADLE_USER_HOME/init.d/cache-settings.init.gradle
if (GradleVersion.current() >= GradleVersion.version('8.0')) {
    apply from: "gradle8/cache-settings.init.gradle"
}

(Not run here: only 9.8.1 was available.) The gradle8 script must then name only what 8.0 has: releasedWrappers, snapshotWrappers, downloadedResources, createdResources and cleanup. The two scripts above also set buildCache and daemonLogs, so they work only from 9.4.0 on; those two settings go into scripts of their own behind >= 8.8 and >= 9.4 guards. The cache marking is configured the same way: markingStrategy = MarkingStrategy.NONE inside caches { } stops writing CACHEDIR.TAG files, and the default marks caches, wrapper/dists, daemon and jdks. On CI the common choices are to keep the home between jobs and let the default cleanup run, or to throw an ephemeral home away and disable the cleanup.

Log levels

Gradle has six log levels. A message is shown when its level is at or above the level you select; the default selection is LIFECYCLE.

Level Used for Option Property

ERROR

Error messages.

always shown

QUIET

Important information messages. Standard output of the build (println) is sent here.

-q / --quiet

org.gradle.logging.level=quiet

WARNING

Warning messages.

-w / --warn

org.gradle.logging.level=warn

LIFECYCLE

Progress information. The default level.

no option

no property needed

INFO

Information messages.

-i / --info

org.gradle.logging.level=info

DEBUG

Debug messages; all messages.

-d / --debug

org.gradle.logging.level=debug

To see what each level shows, a task writes one message at every level (and one with println). Both DSLs were run; the Groovy DSL run printed the same messages at the default level.

Kotlin DSL
// build.gradle.kts (root project)
tasks.register("logDemo") {
    doLast {
        println("println: standard output")
        logger.error("error: something failed")
        logger.warn("warn: something is suspicious")
        logger.quiet("quiet: always shown unless errors only")
        logger.lifecycle("lifecycle: progress message")
        logger.info("info: extra detail")
        logger.debug("debug: troubleshooting detail")
        logger.info("{} items of {}", 3, "tidepool")
    }
}
Groovy DSL
// build.gradle (root project)
tasks.register('logDemo') {
    doLast {
        println 'println: standard output'
        logger.error 'error: something failed'
        logger.warn 'warn: something is suspicious'
        logger.quiet 'quiet: always shown unless errors only'
        logger.lifecycle 'lifecycle: progress message'
        logger.info 'info: extra detail'
        logger.debug 'debug: troubleshooting detail'
        logger.info '{} items of {}', 3, 'tidepool'
    }
}

The task is compatible with the configuration cache (it only calls the task’s own logger), and every run below used --configuration-cache, which is why the output contains the "Reusing configuration cache" lines. The default level:

$ ./gradlew logDemo --configuration-cache
Reusing configuration cache.
> Task :logDemo
println: standard output
error: something failed
warn: something is suspicious
quiet: always shown unless errors only
lifecycle: progress message
BUILD SUCCESSFUL in 395ms
1 actionable task: 1 executed
Configuration cache entry reused.

-q hides everything below QUIET, but not QUIET itself, so the println and the quiet message remain (the help text of the option says "errors only", which is not what it does), and -w adds the warning:

$ ./gradlew logDemo -q --configuration-cache
println: standard output
error: something failed
quiet: always shown unless errors only

$ ./gradlew logDemo -w --configuration-cache
println: standard output
error: something failed
warn: something is suspicious
quiet: always shown unless errors only
Configuration cache entry reused.

-i adds the INFO message and the information messages of Gradle itself. The full output is long and includes the machine’s paths; the lines from the task onwards, which show the reasons for running the task and the new message (the command and filter used: ./gradlew logDemo -i --configuration-cache piped to sed -n '/^> Task :logDemo/,/^BUILD/p'):

> Task :logDemo
Caching disabled for task ':logDemo' because:
  Build cache is disabled
Task ':logDemo' is not up-to-date because:
  Task has not declared any outputs despite executing actions.
println: standard output
error: something failed
warn: something is suspicious
quiet: always shown unless errors only
lifecycle: progress message
info: extra detail
3 items of tidepool
BUILD SUCCESSFUL in 423ms

That is the most valuable use of -i: it explains why a task ran or was not cached. The {} placeholder of logger.info("{} items of {}", 3, "tidepool") is the SLF4J form and avoids building the string when the level is off. The -d output has hundreds of lines (379 in this run). To see how Gradle classifies each message, the lines of the demo (command: ./gradlew logDemo -d --configuration-cache, filtered with grep for the logger names org.gradle.api.Task and system.out; the timestamp at the start of each line was removed):

[QUIET] [system.out] println: standard output
[ERROR] [org.gradle.api.Task] error: something failed
[WARN] [org.gradle.api.Task] warn: something is suspicious
[QUIET] [org.gradle.api.Task] quiet: always shown unless errors only
[LIFECYCLE] [org.gradle.api.Task] lifecycle: progress message
[INFO] [org.gradle.api.Task] info: extra detail
[DEBUG] [org.gradle.api.Task] debug: troubleshooting detail
[INFO] [org.gradle.api.Task] 3 items of tidepool

So println is a QUIET message of the logger system.out, and every logger in a task has the name org.gradle.api.Task. The default level can also be set in a property (gradle.properties in the project, or the Gradle User Home) instead of an option; with org.gradle.logging.level=warn in the project, a plain ./gradlew logDemo printed:

$ ./gradlew logDemo --configuration-cache
println: standard output
error: something failed
warn: something is suspicious
quiet: always shown unless errors only
Configuration cache entry stored.

The command line wins over the property. The related options --console (plain, rich, verbose, auto), --warning-mode (all, fail, summary, none) and --non-interactive (9.6) control the form of the output rather than the level and are in Command-Line Interface. For the equivalent in the other tools: Maven has -q, the default INFO and -X for debug, and Ant has -q, the default, -v and -d (see Maven: Running Maven from the Command Line and Ant: Running Ant).

Stack traces

When a build fails Gradle prints the problem, the location and the tips, and no stack trace. A task that fails on purpose shows it:

Kotlin DSL
// build.gradle.kts (root project)
tasks.register("fail") {
    doLast {
        throw GradleException("The tide table for station 8443970 is missing")
    }
}
Groovy DSL
// build.gradle (root project)
tasks.register('fail') {
    doLast {
        throw new GradleException('The tide table for station 8443970 is missing')
    }
}
$ ./gradlew fail --configuration-cache
Calculating task graph as no cached configuration is available for tasks: fail
> Task :fail FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':fail' (registered in build file 'build.gradle.kts').
> The tide table for station 8443970 is missing

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights from a Build Scan (powered by Develocity).
> Get more help at https://help.gradle.org.

BUILD FAILED in 365ms
1 actionable task: 1 executed
Configuration cache entry stored.

The Groovy DSL project reported the same failure, with a "* Where:" block that names build.gradle and line 16, because Groovy scripts are located by line. The stack trace options:

Option Property What is printed

none

none

No stack trace for a build error; only an internal exception prints one. With DEBUG, truncated stack traces are always printed.

-s / --stacktrace

org.gradle.logging.stacktrace=all

The truncated stack trace, which the official page recommends over the full one. It also renders stack traces for deprecation warnings.

-S / --full-stacktrace

org.gradle.logging.stacktrace=full

The full stack trace. It is very verbose, and in Groovy scripts it is full of the internals of dynamic dispatch.

With -s the failure above grows by an * Exception is: section: 151 lines for this build (the build run is a plain ./gradlew fail -s --configuration-cache). The beginning, the part that points to your code, and the end:

$ ./gradlew fail -s --configuration-cache
...
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':fail' (registered in build file 'build.gradle.kts').
	at org.gradle.api.internal.tasks.execution.DefaultTaskNodeExecutor.lambda$doExecute$1(DefaultTaskNodeExecutor.java:367)
	at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:288)
...
Caused by: org.gradle.api.GradleException: The tide table for station 8443970 is missing
	at Build_gradle$$$result$1$1.execute(build.gradle.kts:16)
	at Build_gradle$$$result$1$1.execute(build.gradle.kts:15)
	at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:858)
...
	... 25 more

BUILD FAILED in 223ms

Read it from the Caused by: line: the first line of your own code (build.gradle.kts:16) is the line of the throw. -S printed 156 lines for the same failure (... 28 more instead of ... 25 more), and org.gradle.logging.stacktrace=full in gradle.properties produced the same 156 lines without the option. In a build with a deeper failure (a compile error, a plugin bug) the difference is larger, because the truncated form drops internal frames. The usual sequence when something fails is the one the message itself suggests: --stacktrace, then --info or --debug, then --scan (see Performance Profiling & Build Scans).

Logging sensitive information

The DEBUG level can write sensitive data into the build log, and the official page lists what to expect: environment variables, credentials of private repositories, build cache and Develocity credentials, and Plugin Portal publishing credentials. Do not use --debug on public CI services, whose logs anybody can read, and be careful on private ones; masking by the CI provider only covers exact matches of pre-configured secrets. In the scratch run, DEMO_TOKEN=not-a-real-secret ./gradlew logDemo -d listed the names of 62 environment variables of the shell in one line ("Configuring env variables: [PATH, …​") and the value of the placeholder did not appear in the output; the log still showed that the variable existed. The conclusion is the official one: treat a DEBUG log as sensitive, and read credentials with providers.environmentVariable / providers.gradleProperty instead of putting them where they are printed (see Build Environment & Properties).

Logging from your own build logic

Gradle redirects the standard output of a build to the QUIET level and standard error to ERROR, so a println works but is shown even with -q. The logger property of a script or task is a Logger, which extends the SLF4J logger and adds quiet and lifecycle. The levels and the placeholder form were in the logDemo task above; logger.trace is accepted and never logged. Choose the level by who should see the message: LIFECYCLE for progress the user should see, INFO for information needed to diagnose why a task ran, DEBUG for noise.

Classes of build logic outside the scripts need a logger of their own. Since Gradle 9.8 the best practices catalogue says to obtain it with Logging.getLogger(MyClass.class) in every non-task class (plugins, build services, extensions), rather than going through project.logger, which is not compatible with the configuration cache when read at execution time and is not reachable from a build service at all; inside a task, the inherited getLogger() is right. The example is a class in buildSrc, with a script that calls it:

// buildSrc/src/main/java/io/tidepool/build/TideLog.java
package io.tidepool.build;

import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;

public final class TideLog {
    private static final Logger LOG = Logging.getLogger(TideLog.class);

    private TideLog() {
    }

    public static void report(String station) {
        LOG.info("Resolving station {}", station);
        LOG.warn("Station {} has no harmonic constituents", station);
        LOG.lifecycle("Station {} done", station);
    }
}
// buildSrc/build.gradle.kts
plugins {
    `java-library`
}
Kotlin DSL
// build.gradle.kts (root project)
tasks.register("slf4jDemo") {
    doLast {
        io.tidepool.build.TideLog.report("8443970")
    }
}
Groovy DSL
// buildSrc/build.gradle
plugins {
    id 'java-library'
}

// build.gradle (root project)
tasks.register('slf4jDemo') {
    doLast {
        io.tidepool.build.TideLog.report('8443970')
    }
}
$ ./gradlew slf4jDemo --configuration-cache
Calculating task graph as no cached configuration is available for tasks: slf4jDemo
> Task :slf4jDemo
Station 8443970 has no harmonic constituents
Station 8443970 done
BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 executed
Configuration cache entry stored.

(The buildSrc tasks that compile the class were left out of the listing; buildSrc is built first.) With --info the first message, "Resolving station 8443970", appears too. Both DSLs behaved the same. The class is a plain SLF4J user as well: the first version of this example used LoggerFactory.getLogger("tidepool-build") from org.slf4j and printed the same messages, since the API of Gradle exposes SLF4J.

Standard output of a task and the configuration cache

A task can change the level its standard output is logged at, with logging.captureStandardOutput(LogLevel.INFO), so that its println calls only appear with -i. The documented form sets it when the task is configured. In this run it worked without the configuration cache, and stopped working with it, in both DSLs, because the setting made at configuration time is not part of what the cache stores:

Kotlin DSL
// build.gradle.kts (root project)
tasks.register("captureDemo") {
    logging.captureStandardOutput(LogLevel.INFO)    // at configuration time
    doFirst {
        println("A task message which is logged at INFO level")
    }
}

tasks.register("captureAtExecution") {
    doFirst {
        logging.captureStandardOutput(LogLevel.INFO)    // at execution time
        println("A task message which is logged at INFO level")
    }
}
Groovy DSL
// build.gradle (root project)
tasks.register('captureDemo') {
    logging.captureStandardOutput LogLevel.INFO
    doFirst {
        println 'A task message which is logged at INFO level'
    }
}

tasks.register('captureAtExecution') {
    doFirst {
        logging.captureStandardOutput LogLevel.INFO
        println 'A task message which is logged at INFO level'
    }
}
$ ./gradlew captureDemo -q
$ ./gradlew captureDemo -q --configuration-cache      (entry stored)
A task message which is logged at INFO level
$ ./gradlew captureDemo -q --configuration-cache      (entry reused)
A task message which is logged at INFO level
$ ./gradlew captureAtExecution -q --configuration-cache
$ ./gradlew captureAtExecution -i --configuration-cache | grep 'INFO level'
A task message which is logged at INFO level

Without the cache the message is hidden at the default and quiet levels. With the cache the setting made while the task was configured was lost on both the storing and the reusing run, and the message was printed. The execution-time form works with and without the cache, and this is the form to use in a build that has the configuration cache on. The same applies to any setting made on a task’s logging.

Logging of Ant, Ivy and other libraries

Gradle uses Ant and Ivy internally and maps their log levels one to one onto its own, except the Ant and Ivy TRACE level, which becomes DEBUG. At the default level you will therefore see Ant or Ivy output only for errors and warnings (the ant objects in Ant Integration behave the same). The logging of Java Util Logging, Jakarta Commons Logging and Log4j used by your build classes is redirected into Gradle’s logging as well.

Replacing Gradle’s console output: useLogger (deprecated)

Gradle.useLogger(Object) replaces the logging of the events whose listener interfaces your logger implements. The official page marks the feature as deprecated, to be removed in the next major version without a replacement, because the configuration cache limits what a custom logger can do: the listener interfaces BuildListener, TaskExecutionListener and TaskActionListener are not compatible with the cache, and the others do not receive events when an entry is reused. Run with the init script below on the tidepool build (the command is ./gradlew -I customLogger.init.gradle.kts :tidepool-model:compileJava --rerun-tasks, with <work> as before), 9.8.1 compiled it with two deprecation warnings and the logger replaced the usual output (there is no "BUILD SUCCESSFUL" line):

Kotlin DSL
// customLogger.init.gradle.kts
useLogger(CustomEventLogger())

@Suppress("deprecation")
class CustomEventLogger() : BuildAdapter(), TaskExecutionListener {

    override fun beforeExecute(task: Task) {
        println("[${task.name}]")
    }

    override fun afterExecute(task: Task, state: TaskState) {
        println()
    }

    override fun buildFinished(result: BuildResult) {
        println("build completed")
        if (result.failure != null) {
            (result.failure as Throwable).printStackTrace()
        }
    }
}
Groovy DSL
// customLogger.init.gradle
useLogger(new CustomEventLogger())

@SuppressWarnings("deprecation")
class CustomEventLogger extends BuildAdapter implements TaskExecutionListener {

    void beforeExecute(Task task) {
        println "[$task.name]"
    }

    void afterExecute(Task task, TaskState state) {
        println()
    }

    void buildFinished(BuildResult result) {
        println 'build completed'
        if (result.failure != null) {
            result.failure.printStackTrace()
        }
    }
}
$ ./gradlew -I customLogger.init.gradle.kts :tidepool-model:compileJava --rerun-tasks
w: file://<work>/customLogger.init.gradle.kts:1:1: 'fun useLogger(logger: Any): Unit' is deprecated. Deprecated in Java.
w: file://<work>/customLogger.init.gradle.kts:14:18: This declaration overrides a deprecated member but is not marked as deprecated itself. Add the '@Deprecated' annotation or suppress the diagnostic.

> Task :tidepool-model:compileJava
[compileJava]

build completed

[Incubating] Problems report is available at: file://<work>/build/reports/problems/problems-report.html
1 actionable task: 1 executed

The Groovy DSL init script (-I customLogger.init.gradle, run in the Groovy copy) printed the same, without the Kotlin compiler warnings. With --configuration-cache the same run ended with "Configuration cache entry discarded with 1 problem.", which is the incompatibility the official page warns about. Do not build new tooling on useLogger; for custom reporting use a build service that listens to build events (see Worker API & Build Services) or a build scan.

Where the book is outdated

Gradle in Action (Muschko, Manning 2014, Gradle 1.7) shows the ~/.gradle directory and the .gradle and build directories of a project, and the logging chapter’s levels and -q, -i, -d options still apply unchanged. What changed: the cache layout (modules-2, the build output cleanup, the configuration cache and, since 8.0, configurable retentions and cleanup by Gradle itself), the way to set the JVM options (org.gradle.jvmargs, see The Gradle Daemon), the stack-trace options (-s for the truncated one is the recommended form), and the preference for Logging.getLogger in build logic over the script’s logger. The buildSrc directory is still supported, but the book predates the guidance towards included builds for build logic.

Summary

  • The Gradle User Home (~/.gradle, or GRADLE_USER_HOME, -g) holds caches, daemon, init.d, jdks, wrapper/dists and user-level gradle.properties; the project root holds .gradle/ (project cache) and each project’s build/. Commit neither.

  • Version-specific caches live in caches/<version>, shared ones in caches/ with a format number in the name, and project state in .gradle/. Do not rely on their contents.

  • Gradle cleans the home periodically (once a day at most): unused distributions and caches after 7 or 30 days by default, daemon logs after 14. Since 8.0 the periods and the strategy (DEFAULT, DISABLED, ALWAYS) are set in an init script in init.d (the buildCache period since 8.8, the daemonLogs period since 9.4.0); since 8.1 CACHEDIR.TAG marks the big directories for backup tools.

  • Log levels: -q (QUIET and ERROR), -w, the default LIFECYCLE, -i (also explains why tasks ran), -d; or org.gradle.logging.level. println logs at QUIET.

  • Stack traces: none by default, -s truncated, -S full, or org.gradle.logging.stacktrace. Read from Caused by:.

  • DEBUG logs can expose credentials; do not use them on public CI.

  • In build logic use Logging.getLogger(MyClass.class); set logging.captureStandardOutput at execution time when the configuration cache is on; useLogger is deprecated.