Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/asciidoc/configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,20 @@ Jooby uses **SLF4J**, allowing you to plug in your preferred logging framework.
1. **Add Dependencies:** `log4j-slf4j-impl` and `log4j-core`.
2. **Configure:** Place `log4j2.xml` in your `conf` directory or classpath root.

===== Rainbow Gum

Add `jooby-rainbowgum` to configure logging directly in `application.conf`, including environment-specific
files. The module includes the SLF4J provider and pattern encoder and handles logging before the
application environment is available. See link:{uiVersion}/modules/rainbowgum[Rainbow Gum] for examples
and configuration updates.

===== Environment-Aware Logging

Logging is also environment-aware. Jooby will look for `logback.[env].xml` or `log4j2.[env].xml` and favor them over the default files.

[IMPORTANT]
====
To ensure environment-specific logging works correctly, avoid using **static** loggers in your main App class before `runApp` is called. Static loggers force the logging framework to initialize before Jooby can apply the environment-specific configuration. Use an instance logger or Jooby's `getLog()` method instead.
With Logback or Log4j2, to ensure environment-specific logging works correctly, avoid using **static** loggers in your main App class before `runApp` is called. Static loggers force the logging framework to initialize before Jooby can apply the environment-specific configuration. Use an instance logger or Jooby's `getLog()` method instead.
====

==== Application Properties
Expand Down
3 changes: 3 additions & 0 deletions docs/asciidoc/modules/modules.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ Modules are distributed as separate dependencies. Below is the catalog of offici
* link:{uiVersion}/modules/jackson3[Jackson3]: Jackson3 module for Jooby.
* link:{uiVersion}/modules/yasson[Yasson]: JSON-B module for Jooby.

==== Logging
* link:{uiVersion}/modules/rainbowgum[Rainbow Gum]: SLF4J logging configured through the application environment.

==== Metrics
* link:{uiVersion}/modules/metrics[Metrics]: Application metrics from the excellent metrics library.
* link:{uiVersion}/modules/opentelemetry[Open Telemetry]: Application metrics using Open Telemetry library.
Expand Down
93 changes: 93 additions & 0 deletions docs/asciidoc/modules/rainbowgum.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
== Rainbow Gum

https://jstach.io/rainbowgum/[Rainbow Gum] is an SLF4J implementation for Java 21 and newer.
The `jooby-rainbowgum` module configures it directly from Jooby's resolved Typesafe Config environment.

=== Usage

Add the dependency:

[dependency, artifactId="jooby-rainbowgum"]
.

Use this module in place of `jooby-logback`, `jooby-log4j`, or another SLF4J provider.
No `install(...)` call or separate logging configuration file is needed. The module includes the
SLF4J provider, pattern encoder and file output support.

.application.conf
[source, hocon]
----
logging {
level {
root = info
"com.myapp" = debug
}
appenders = [console]
appender.console {
output.uri = stdout
encoder {
uri = pattern
pattern.pattern = "[%thread] %-5level %logger - %msg%n"
}
}
}
----

All Rainbow Gum properties are available, including appenders, encoders, routes and asynchronous
publishers. Native HOCON lists and maps are supported. Quote dotted logger names so they remain
one object member. Use `logging.level.root` for the root level when defining other logger levels
in the same object; HOCON cannot keep `logging.level` as both a scalar and an object.

Jooby resolves `application.conf`, environment-specific files such as `application.prod.conf`,
substitutions and system properties before passing the environment to Rainbow Gum. Programmatic
environments supplied through `setEnvironment(...)` and `setEnvironmentOptions(...)` also configure
logging.

Logging is global to the JVM and configured once, from the first environment Jooby loads, as with
Jooby's other logging implementations. Later environments, such as those of additional applications
started by `runApp` or another `setEnvironment(...)` call, do not change it.

=== Early logging

A bootstrap Rainbow Gum queues messages emitted before Jooby loads its environment, including messages
from static loggers. When the environment is available, the module installs the configured Rainbow
Gum, replays queued messages through its routes, and moves loggers created during bootstrap to its
levels and routes. It then closes the bootstrap instance. An explicitly installed Rainbow Gum, or one supplied by the application's own
`RainbowGumProvider`, is left under the application's control.

Caller information and decorators are chosen when a logger is created. For loggers created during
bootstrap, configure these capabilities before startup (for example,
`-Dlogging.caller.logger.root=true`); the later environment configuration does not retrofit them.

=== Configuration changes

Jooby core does not watch configuration files or publish configuration-change events. Jooby Run's
development watcher restarts the application but keeps project dependencies loaded, so logging keeps
the configuration from the first start. Restart Jooby Run to apply logging changes.

Loggers have a fixed level once created. To let existing loggers follow level changes, allow them
with Rainbow Gum's `logging.change.logger` property:

.application.conf
[source, hocon]
----
logging.change.logger = level
----

The application can then replace the configuration of its environment and reread logger levels
without reopening appenders:

[source, java]
----
getEnvironment().setConfig(updatedConfig);
RainbowGum.of().config().changePublisher().publish();
----

=== Server logger muting

Jooby turns off some server loggers while the server starts and stops. The module allows level
changes for those loggers only: `org.eclipse.jetty.server` for Jetty, and `io.undertow`, `org.xnio`
and `org.jboss.threads` for Undertow. `logging.change.mode = off` turns off all level changes,
including this muting.

See the https://jstach.io/rainbowgum/[Rainbow Gum guide] for the complete configuration reference.
7 changes: 4 additions & 3 deletions jooby/src/main/java/io/jooby/Jooby.java
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public Jooby setRouterOptions(RouterOptions options) {
*/
public Environment getEnvironment() {
if (env == null) {
env = Environment.loadEnvironment(environmentOptions);
setEnvironment(Environment.loadEnvironment(environmentOptions));
}
return env;
}
Expand Down Expand Up @@ -213,6 +213,7 @@ public Config getConfig() {
*/
public Jooby setEnvironment(Environment environment) {
this.env = environment;
LoggingService.configure(environment);
return this;
}

Expand All @@ -224,9 +225,9 @@ public Jooby setEnvironment(Environment environment) {
*/
public Environment setEnvironmentOptions(EnvironmentOptions options) {
this.environmentOptions = options;
this.env =
setEnvironment(
Environment.loadEnvironment(
options.setClassLoader(options.getClassLoader(getClass().getClassLoader())));
options.setClassLoader(options.getClassLoader(getClass().getClassLoader()))));
return this.env;
}

Expand Down
24 changes: 22 additions & 2 deletions jooby/src/main/java/io/jooby/LoggingService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
import org.jspecify.annotations.Nullable;

/**
* Describe the underlying logging system. Jooby provides two implementation: jooby-logback and
* jooby-log4j. You might want to add one of these to your classpath, yet still these are optional.
* Describe the underlying logging system. Jooby provides jooby-logback, jooby-log4j and
* jooby-rainbowgum. These integrations are optional; add one to your classpath.
*
* @since 3.0.1
*/
Expand All @@ -32,6 +32,26 @@ public interface LoggingService {
*/
String getPropertyName();

/**
* Initializes logging using the application's resolved environment. Called when Jooby loads or
* replaces its environment, including environments supplied programmatically. The default does
* nothing, for logging systems configured by their own files.
*
* @param environment Application environment.
*/
default void initialize(Environment environment) {}

/**
* Initializes the installed logging service using the application's resolved environment.
*
* @param environment Application environment.
*/
static void configure(Environment environment) {
ServiceLoader.load(LoggingService.class, environment.getClassLoader())
.findFirst()
.ifPresent(service -> service.initialize(environment));
}

/**
* List of possible names for configuration file. Like: <code>logback.xml, log4j.properties, etc..
* </code>.
Expand Down
17 changes: 17 additions & 0 deletions jooby/src/test/java/io/jooby/LoggingServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ private void restore(String key, String value) {
}
}

@Test
public void configureResolvedEnvironment() {
var environment =
new Environment(
getClass().getClassLoader(), com.typesafe.config.ConfigFactory.empty(), "test");
try (MockedStatic<ServiceLoader> serviceLoaderMock = mockStatic(ServiceLoader.class)) {
ServiceLoader<LoggingService> loader = mock(ServiceLoader.class);
LoggingService service = mock(LoggingService.class);
when(loader.findFirst()).thenReturn(Optional.of(service));
serviceLoaderMock
.when(() -> ServiceLoader.load(LoggingService.class, environment.getClassLoader()))
.thenReturn(loader);
LoggingService.configure(environment);
verify(service).initialize(environment);
}
}

@Test
public void isBinaryPath() {
assertFalse(LoggingService.isBinary(Paths.get("src", "main", "java")));
Expand Down
5 changes: 5 additions & 0 deletions modules/jooby-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,11 @@
<artifactId>jooby-quartz</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-rainbowgum</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-reactor</artifactId>
Expand Down
83 changes: 83 additions & 0 deletions modules/jooby-rainbowgum/NOTE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Rainbow Gum integration review notes

Working notes for this branch. Remove before merging.

## Public API added outside this module

Only `io.jooby.LoggingService` changes:

- `default void initialize(Environment environment)`: a hook for logging systems configured from the
Jooby environment. The default does nothing, so `jooby-logback` and `jooby-log4j` are unchanged.
- `static void configure(Environment environment)`: finds the installed `LoggingService` with the
environment's class loader and calls `initialize`. It is public only because interface static
methods cannot be package private; `Jooby` is its only caller. It overloads the existing
`configure(ClassLoader, List<String>)`.

`Jooby` keeps its signatures, but `setEnvironment`, `setEnvironmentOptions` and the lazy load in
`getEnvironment` now all go through `setEnvironment`, which calls `LoggingService.configure`. Every
environment Jooby loads or is given now does a `ServiceLoader` lookup.

The BOM and the parent pom gain `jooby-rainbowgum`, and the parent pom gains `rainbowgum.version`.

## Changed in review

- **Bootstrap marker.** The marker that lets Jooby replace the bootstrap Rainbow Gum was added by a
`PropertiesProvider`, which runs for every service loaded `LogConfig`. A Rainbow Gum the
application built from such a config, or supplied by its own `RainbowGumProvider`, was closed and
replaced when the environment loaded. `RainbowGumBootstrap` now builds its own config with the
marker and has the lowest priority.
- **Configured once.** Only the queued bootstrap instance is replaced. Loggers with a fixed level
stay bound to the instance that created them, so rebuilding logging for a later environment left
them writing to a closed instance and their events were lost. Making every logger changeable hid
this. Later environments (`runApp` with several applications, another `setEnvironment(...)`, test
suites starting an application per class) now keep the first configuration, as Logback and Log4j
do.
- **Level changes only where muting needs them.** Defaults allow level changes for the loggers Jetty
and Undertow mute (`org.eclipse.jetty.server`, `io.undertow`, `org.xnio`, `org.jboss.threads`)
instead of every logger, so other loggers keep the fixed level fast path. Rereading levels with
`changePublisher().publish()` is opt in through `logging.change.logger`.

## Open on this branch

- **Unbounded queue.** If no Jooby environment is ever loaded (unit tests that never read the
config, command line tools), INFO and above queue in memory for the life of the JVM and are never
written. Only ERROR reaches stderr. Probably needs a Rainbow Gum side fix: a bounded queue or a
flush at shutdown.
- **Muting with an application Rainbow Gum.** Muting levels are only wired into the module's own
config, so `logOff` does nothing when the application installed its own Rainbow Gum.
- **Failed logging setup.** `Jooby.setEnvironment` stores the environment before configuring
logging. If the logging config is invalid the call throws, but `getEnvironment()` then returns that
environment and logging stays queued.
- **Jooby Run.** Dependencies stay loaded across restarts in the default mode, so logging keeps the
first start's configuration (documented). The configured instance also keeps the first
`Environment`, and with it the first project class loader, reachable. With
`useSingleClassLoader` everything reloads, so each restart configures a new instance, but nothing
closes the previous class loader's instance or removes its shutdown hook. This is from reading the
code, not tested.

## GraalVM native image

Checked with GraalVM CE 25.0.4 `native-image` on a small Jetty application with a static logger
and a `logging` block in `application.conf`. This module needs no native image metadata: its
service providers are registered from `META-INF/services`, and the native executable printed the
same output as the JVM run (early message replayed through the configured pattern, debug levels
from the config, Jetty startup muted).

Jooby itself ships no native image metadata. Without it, a classpath `application.conf` is not in
the image, and Jooby silently falls back to defaults (port 8080, default logging). The application
had to add `META-INF/native-image/<group>/<artifact>/reachability-metadata.json` with
`{"resources": [{"glob": "application*.conf"}]}`. Jooby could ship that resource entry in its own
jar.

## Jooby bugs outside this module

- **Log4j `logOff` does not restore exactly configured loggers.** `Log4jService.setLog4jLevel` reads
`logger.getLevel()` after `Configurator.setLevel`, which sets an existing `LoggerConfig` with that
exact name in place (checked against log4j-core 2.26.1). The saved level is OFF, so a muted logger
configured by name in `log4j2.xml` stays off. The first mute also adds a `LoggerConfig` with that
name, so after one start and stop every muted logger is affected on the next start in the same JVM.
Fix: read the level before setting it.
- **Jetty mutes `org.eclipse.jetty.server.Server` twice.** `JettyServer.getLoggerOff()` lists it
twice. Logback and Log4j `logOff` record the previous level per name, so the second entry records
OFF over the original and the logger stays off after startup. Fix: drop the duplicate, or apply
`distinct()` in `MutedServer`.
43 changes: 43 additions & 0 deletions modules/jooby-rainbowgum/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.jooby</groupId>
<artifactId>modules</artifactId>
<version>4.5.5-SNAPSHOT</version>
</parent>
<artifactId>jooby-rainbowgum</artifactId>
<name>jooby-rainbowgum</name>
<dependencies>
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby</artifactId>
</dependency>
<dependency>
<groupId>io.jstach.rainbowgum</groupId>
<artifactId>rainbowgum-slf4j</artifactId>
<version>${rainbowgum.version}</version>
</dependency>
<dependency>
<groupId>io.jstach.rainbowgum</groupId>
<artifactId>rainbowgum-pattern</artifactId>
<version>${rainbowgum.version}</version>
</dependency>
<dependency>
<groupId>io.jstach.rainbowgum</groupId>
<artifactId>rainbowgum-file</artifactId>
<version>${rainbowgum.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>org.jacoco.agent</artifactId>
<classifier>runtime</classifier>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Loading
Loading