rainbowgum API 0.8.0
User Guide
Rainbow Gum: JDK 21+ SLF4J logging implementationFast, modular, GraalVM native friendly and easy to use.
Contents
Getting Started
First step is to add Rainbow Gum as a dependency.
Second if you are not familiar with logging in the Java ecosystem please refer to
the excellent Logging facade SLF4J documentation
particularly the logger API
and what logging Levels are. Logging levels are and how dotted logger names inherit levels is covered in
the JDKs logging facade System.Logger.Level
as well as
Logback effective level
which Rainbow Gum follows.
Using Spring Boot
If using Spring Boot see Rainbow Spring Boot Integration section.Using System properties
Assuming we have installed RainbowGum as a dependency a simple configuration example using System Properties is below: java \
-Dlogging.appender.console.encoder=pattern \
-Dlogging.encoder.console.pattern="[%thread] %-5level %logger{15} - %msg%n" \
-Dlogging.level.com.myapp=DEBUG \
myapp.jar
Using RainbowGum Builders
For technical organizations that have many projects it is recommended to use the programmatic builder approach and make a standardized jar (module) shared between all your projects so that logging initialization and configuration is consistent. Assuming we have installed RainbowGum as a dependency a simple configuration example using Java directly and theServiceLoader
is below.
public class GettingStartedExample implements RainbowGumProvider {
@Override
public Optional<RainbowGum> provide(LogConfig config) {
return RainbowGum.builder(config) //
.route(r -> {
r.level(Level.DEBUG, "com.myapp");
r.appender("console", a -> {
a.encoder(new PatternEncoderBuilder("console")
// We use the pattern encoder which follows logback pattern
// syntax.
.pattern("[%thread] %-5level %logger{15} - %msg%n")
// We use properties to override the above pattern if set.
.fromProperties(config.properties())
.build());
});
}) //
.optional();
}
}
RainbowGumServiceProvider
has additional documentation on how to register
a service loader aware jar.
Description
Rainbow Gum is a JDK 21+ opinionated SLF4J implementation that aims to be easier to use while leveraging newer JDK technology. Rainbow Gum unlike Logback or Log4J (2) does not offer as much flexibility but is simpler and has less overhead. The readme in the Rainbow Gum project discusses more extensively on the opinionated design and philosophy.Project Information
- Source Control
- https://github.com/jstachio/rainbowgum
- Team
-
- Adam Gent (agentgt) - lead
- Issues
- https://github.com/jstachio/rainbowgum/issues
- Community
- https://github.com/jstachio/rainbowgum/discussions
- User Guide
- This document
- Javadoc
- This document (modules listing at bottom)
Requirements
- Java 21 or greater
- A build system that supports running the Java compiler annotation processor
java.base
Limitations
Currently Rainbow Gum does not provide support for:- Default external config file (e.g. log4j2.xml) - by design but do not worry because Rainbow Gum provides lots of extensions to use your applications configuration system. Out of the box system properties are supported.
- Rolling of log files without external tool - there is support for rolling without losing events using external tool such as logrotate.
- SLF4J Marker support - libraries and application rarely use it compared to MDC.
- It is actually fairly expensive to load resources from the module/classpath on initialization. If your configuration system is already loading a resource it should be used instead.
- In GraalVM native and in some cases modular environment loading module/classpath resources is more complicated. We do not want an OOB experience where it works in normal HotSpot but not in GraalVM native.
- It increases the security surface and Rainbow Gum aims to have security and integrity by default. Implicit loading of resources we feel is against this and if done should be a chosen opt-in which you can have with various extensions.
How it works
Most users will use Rainbow Gum through a logging facade such as SLF4J and the only interaction with Rainbow Gum is configuration of output and formatting. Rainbow Gum provides three ways to configure out of the box:- Simple String key/value properties often derived from System properties / env variables
- Programmatic Java configuration using builders.
- Dependency driven configuration where including a jar as dependency changes behavior automatically
Architecture
Rainbow Gum's design has many superficial similarities to Logback, Log4j2, and Reload4j in that there are Appenders and Encoders as well as how log levels are inherited but most of Rainbow Gum's design is very different!The key difference in Rainbow Gum is that once configured and initialized the logging system is locked in and cannot be changed. Certain parts of the system can be changed at runtime but require opt-in which includes changing levels of logger names.
Another major difference is that Rainbow Gum is highly modularized, immutable and componentized. In other frameworks
OOP inheritance is heavily abused and riddled with state management. Furthermore there are components that
have too much responsibility which is the case with Appenders in Logback or "Managers" in Log4j2.
Furthermore Rainbow Gum uses zero reflection other than the Service Loader and follows the builder pattern
extensively to separate configuration from immutable runtime components. Almost all components have
a builder to programmatically configure and the builders can take flat string key values as configuration.
The other logging frameworks use an enormous amount of reflection which slows initialization time.
(While the ServiceLoader
is technically reflection it is GraalVM native friendly and
is the preferred way for pluggable components in modern JDKs.)
The results of the above choices make Rainbow Gum far lighter than logback, log4j2, and reload4j particularly in initialization time as well as security surface area.
The key components expressed in the flow of log events is as follows:- Logging Facade (SLF4J)
RainbowGum
LogRouter
LogPublisher
LogAppender
LogEncoder
LogOutput
Roughly the hiearchy (through composition) of components is:
- Routes have a single publisher a level resolver and a list of appenders.
- Publishers are given the list of appenders from the route on start.
- Appenders have a single encoder and output.
Config
A defining characteristic of Rainbow Gum is that it does not have a special configuration format like Logback, Log4j2, and Reload4j (all three use XML as the default). The expectation is that for simple configuration simple properties interface analogous toFunction<String,String>
is good enough. For more complicated configuration
programmatic configuration using the builders and service loader should be used.
For implementers of plugins LogConfig
allows registering of services
and plugins (it also contains the global LogProperties
) however
most users will not need to know about it. For those coming from Logback
LogConfig
is analogous to what Logback calls "Context".
Properties based
Rainbow Gum out of the box uses System properties forLogProperties
but an optional module io.jstach.rainbowgum.avaje
will allow using Avaje Config
as the properties provider. Other configuration systems will be added in the future but an
important requirement is that these systems do not do any logging or if they do allow it to be
turned off, intercepted or blocked.
IMPORTANT: Rainbow Gum core does not do any interpolation of property values!
That is the responsibility of the LogProperties
backing implementation and the values retrieved should already be interpolated.
NOTE:
Throughout the documentation Properties
and URI
syntax is used to express
configurable properties but it is a not a requirement and
your configuration system may have a different format like YAML or JSON.
A listing of property patterns that are used out of the box is discussed in LogProperties
.
A general pattern is:
logging.{componentType}.{name}.{subComponent}=URI
logging.{subComponent}.{name}.{propertyName}=someStringThatIsConverted
logging.appender.example.encoder=gelf
logging.encoder.example.prettyPrint=true
gelf
is actually gelf:///
Because the first property is a URI one can do:
logging.appender.example.encoder=gelf:///?prettyPrint=true
URI scheme
is used for plugin lookup as well as single line configuration. This follows
12 factor recommendation of resources and configuration.
Plural keys for enabling/disabling
Because Rainbow Gum's property config model is a lookup of key to value and not a Map like collection special plural keys are used to indicate what is activated. These keys canonical end in"s"
and are comma separated.
logging.appenders=appender1,appender2 # appenders that are enabled.
logging.appender1.encoder=...
logging.appender2.output=...
logging.appender3.output=....
appender3
will not be used
and will not produce a configuration error if incorrect. This pattern
allows large groups of configuration to be turned on or off similar to
profiles in other systems.
Builder based
Rainbow Gum can be configured programmatically through builders. The builders normally will ignore whatever properties (key values) are set externally but most builders can be configured by properties throughLogBuilder.fromProperties(io.jstach.rainbowgum.LogProperties)
and thus a combination of properties and builders can be used.
Config Change during runtime
While most of the logging components in Rainbow Gum are immutable and cannot be changed during runtime there are some exceptions including custom components.A supporting configuration system (a LogProperties provider) can notify that there has been a change by using the Change Publisher. This is how level resolving changes can be published.
Config change is by default off even on supporting configuration systems and can only be enabled by setting "logging.global.change" to true.
Router
A Router has aLevelResolver
, appenders and a Publisher.
A Level Resolver takes a logger name and produces a level
.
The logging facade implementation then decides based on the level whether or not to construct an event.
It then passes the event to the router which in turn uses the publisher to schedule the event.
For those familiar with other logging frameworks
Rainbow Gum has no actual concept of named "Loggers" but a route from a router is the closest analog.
When configuring routers you are configuring
Level Resolvers
which are logger names to levels,
which publisher to use and which appenders should be associated with the publisher.
We call this configuration a "route". As discussed later on
a publisher has appenders and appenders have outputs thus
an important consequnce of this is if you want outputs to have different level thresholds
(e.g. a debug.log and an error.log) you will need multiple routes.
example
".
logging.routes=example
logging.route.example.appenders=appender1,appender2
logging.route.example.publisher=async
logging.route.example.level.com.mycompany=DEBUG
Level Resolvers
As mentioned previously routers have a level resolver. A level resolver simply resolves the level (an enum) from a logger name (String). Because ofLevelResolver
functional interface we can chain them
with fallbacks. An important fallback level resolver that almost all routers use
is the global level resolver. These levels can be configured with
properties
where logger names prefixed with
"logging.level" as the key and the level as the value.
Below is an example:
logging.level.com.mycompany.stuff=DEBUG # Descedant of "com.mycompany"
logging.level.com.mycompany=INFO # Ancestor to "com.mycompany.stuff"
logging.level=ERROR
Although not required most level resolvers including the global follow an inheritance or prefix model where
com.mycompany.stuff.foo
will resolve to DEBUG
and com.mycompany.bar
will resolve to INFO
and anything not prefixed with com.mycompany
will resolve to ERROR
.
An analog is to think of the ".
" as directory path separators like in filesystems
and resolution happens by going up the directories.
This behavior is stated more formally in
Logback's Effective Level
which is what Rainbow Gum follows as well.
An important consideration is that Rainbow Gum by default assumes Level Resolvers are cached or static and will never change. This is a critical feature of Rainbow Gum as it allows facade loggers like in SLF4J to be lower overhead than almost all other logging frameworks! That being said RainbowGum allows levels to be changed if the backing configuration framework supports reloading and the logger name is allowed to be changed.
Below is an example of configuring to allow "changing" loggerslogging.global.change=true # this is required to turn on level changing.
logging.change.com.mycompany=level # only logger names starting with com.mycompany can have their levels changed.
Level Groups
Rainbow Gum out of the box supports Spring Boot like Log Groups . However unlike Spring Boot groups are not enabled unless "logging.groups" contains the group. (this is largely because Spring Boots configuration model is like a Map and Rainbow Gums a Function.)Furthermore group levels but not group definitons can be assigned on the router itself. Using Spring Boots example:
logging.groups=tomcat # This is required otherwise the tomcat group will not be resolved
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
logging.level.tomcat=trace
logging.route.myroute.level.tomcat=info
org.apache.catalina
, org.apache.coyote
, and org.apache.tomcat
and their descedants
will resolve to INFO
for "myroute
" but TRACE
for other routes.
Note that just like logger names groups are case sensitive.
Additivity
Those familiar with Logback and Log4j2/1 "additivity" may wonder how that is achieved. Additivity is associated with a logger name in those frameworks and means that the logger level settings will apply to other appenders associated with logger (name) and its ancestors. If false the settings only apply to the associated appenders at that logger and descendants.In rainbowgum the configuration including logger name to level is done on the route and rainbowgum allows multiple appenders on a route and multiple routes.
For example let us assume we want logger namecom.mycompany
(and descendants)
at
DEBUG
or greater to go to a file called mycompany.log
and
WARN
for console com.mycompany
(and everything else).
In Logback and others this is achieved by setting the appenders
on com.mycompany
and additivity=false
but in rainbowgum this is done
by configuring separate routes:
logging.level=INFO
logging.routes=console,mycompany
logging.route.mycompany.appenders=mycompany
logging.route.mycompany.level.com.mycompany=DEBUG
logging.route.console.level=WARN
logging.route.console.appenders=console
logging.appender.mycompany.output=file:///./mycompany.log
logging.appender.console.output=stdout
WARN
or greater (ERROR
)
to stdout
(terminal/console output).
The route my "mycompany" will log DEBUG
and greater but less than or equal
of INFO
for
com.mycompany
(and descedants like com.mycompany.something
)
to the file mycompany.log
.
IF we only want mycompany.log
to have com.mycompany
and descendants logger name entries
with DEBUG
or greater than we turn off all other logger names.
To do this we add to the above configuration:
logging.route.mycompany.level=OFF
Caller Information
Somewhat related to level resolvers and config change is how to enable caller info.LogEvent.Caller
has stack trace like information on where the logging call was made.
It is incredibly slow so like changing loggers it must be enabled and you can enable it with logger name inheritance
so not all loggers will be slow.
Below is an example of configuring to allow "changing" loggers with caller info:
logging.global.change=true # this is required to turn on level changing.
logging.change.com.mycompany=caller # only logger names starting with com.mycompany will have caller info
LogConfig.ChangePublisher.ChangeType
however caller info will automatically allow
the logger to change levels as well.
Publishers
ALogPublisher
schedules delivery of a LogEvent
to a group of
Appenders. Publishers come in two types:
- synchronous - the default
- asynchronous
An example configuring a route named "example" to use the default async publisher:
logging.route.example.publisher=async
logging.publisher.example.bufferSize=1024
NOTE: If you want some appenders to be async and others sync you just create multiple routes.
Rainbow Gum has an experimental async publisher that uses the LMAX Disruptor ringbuffer:io.jstach.rainbowgum.disruptor
.
If the jar is found on start it will replace the default async publisher.
Appenders
An appender contains an Encoder and anOutput
.
Unlike other logging frameworks there are no custom Appenders as most of the work is done by the encoder and output.
That is an appender can be mostly thought of as a tuple of encoder and output as well as it supervises both.
Appenders are associated to the previously mentioned route. Like previously mentioned when a route is not given a name it is automatically named "default". Thus to register appenders to the default route one can use:
logging.appenders=myappender
logging.appender.myappender.flags=reuse_buffer # an example config of appender
logging.route.example.appenders=myappender
logging.appender.myappender.flags=reuse_buffer # an example config of appender
Appender Configuration
Appenders can be configured either programmatically through thebuilder
or through properties.
- Output
- "logging.appender.{name}.output" = URI
- Encoder
- "logging.appender.{name}.encoder" = URI
- Flags
- "logging.appender.{name}.flags" = List of
LogAppender.AppenderFlag
LogAppender.AppenderFlag.IMMEDIATE_FLUSH
which
will tell the output to flush after each event. This flag is disabled by default for performance reasons
however if one would like to follow 12 Factors requirements of events written unbuffered
synchronously this flag should be used.
Appender Reentry Protection
A problem that can occur in any logging framework is if an output, appender, or encoder does logging which if synchronous will usually cause a stackoverflow and if asynchronous an infinite stream of events. Let us create a hypothetical example where we have an Output that uses a message queue client. The message queue client uses SLF4J for logging. If an event is sent that causes the message queue client to log an event and that event is not discarded and the output client then logs the event it created you get reentry in a synchronous publisher. With asynchronous it is far worse as there will be less immediate evidence that something is broken and instead a never ending supply of log entries.
Some logging frameworks like logback provide reentry protection
usually through threadlocals but one must understand this only works if logging is synchronous (synchronous publisher), hurts performance,
and is just a misleading bandaid over a potential serious problem if the events are being dropped (which logback does by default).
That is why Rainbow Gum does not provide reentry protection unless requested!
However the flags LogAppender.AppenderFlag.REENTRY_DROP
and LogAppender.AppenderFlag.REENTRY_LOG
can be used to provide similar and arguably better protection than logback.
Overall the real solution is to fix the output so that it filters events that would cause such behavior or ideally not do any logging.
Encoders
Encoders encode an event into binary. Because most encoding is text based RainbowGum has the concept ofLogFormatter
which can be turned into an Encoder and is covered
in the next section. However there are scenarios where generating bytes directly is desirable.
Rainbow Gum JSON encoder module io.jstach.rainbowgum.json
has efficient JSON encoders.
They are encoders instead of formatters (which are covered next) because writting JSON as bytes is more efficient.
logging.appenders=myappender
logging.appender.myappender.encoder=gelf
logging.encoder.myappender.prettyPrint=true
Encoders should have a builder as well to configure programmatically and the builders javadoc has the string properties that can configure it.
Here are some of the supported encoders (not a complete listing):Formatters
Formatting of log events as textual data is so common that Rainbow Gum has special support for that withLogFormatter
. While formatters can at high-level be considered
an encoder for technical API reasons they are not. However any formatter can be converted to an
encoder with LogEncoder.of(io.jstach.rainbowgum.LogFormatter)
.
Pattern Formatter
For those coming from Logback or Log4j2 Rainbow Gum has an optional module for Logback style based pattern formatters. This allows describing an output format usingString.format
percent style syntax. Rainbow Gum implements most of the builtin Logback
pattern keywords with far less overhead.
Like Logback you can even add your own pattern keywords which is covered in io.jstach.rainbowgum.pattern
module.
NOTE: If you plan on using the color pattern keywords it is generally a good idea (for now) to use
the io.jstach.rainbowgum.jansi
module which is covered next. Regardless ANSI escape can be
globally disabled by setting "logging.global.ansi.disable" to
true
which will disable Jansi and the default pattern formatter from omitting ANSI escape sequences.
JAnsi support
Rainbow Gum also has support for proper cross platform ANSI output with JAnsi through an optional module. Unlike other frameworks where this is detected with reflection Rainbow Gum uses the Service Loader which is GraalVM native friendly.
Why is JAnsi desirable?- Support stripping ANSI characters on piping out which is desirable if you only use the console for output which is often the case for kubernetes and other microservices.
- Windows color output support.
NOTE: Future versions of the JDK may require command line arguments for using jars packaged with native libraries. Given that Rainbow Gum favors security we strongly agree with the draft JEP and are exploring other options for the future.
Jansi can be disabled with "logging.jansi.disable" or "logging.global.ansi.disable" boolean properties.A major reason to disable Jansi is because it may accidentally strip ANSI escape characters for terminals that do support ANSI but Jansi cannot determine they do. The most notable case of this problem are IDE terminals/consoles particularly IntelliJ's.
Outputs
LogOutput
do the actual work of outputting an event.
Configuring an appenders output with properties
looks something like:
logging.appenders=myappender
logging.appender.myappender.output=stdout
./
.
logging.appenders=myappender
# logging.appender.myappender.output=app.log this is wrong.
logging.appender.myappender.output=./app.log
app.log
is a URI scheme if it is not prefixed.
Of course fully qualified file URI are supported as well like: file:///./app.log
.
For Spring Boot compatibility "logging.file.name" is also supported
if custom routers and appenders are not configured.
Here are some of the supported outputs (not a complete listing):
- stdout and stderr
FileOutputBuilder
with URI scheme of "file"RabbitMQOutputBuilder
with URI scheme of "amqp"
Rolling Files
Rainbow Gum currently does not support rolling of files on its own but does provide a mechanism to safely allow external programs such as logrotate to do the rolling. How this typically works:- External program moves the current log file. Rainbow Gum will continue to log to the same file but the file name is effectively changed.
- Signals to the Java process with Rainbow Gum to reopen the files. This signal is usually done via HTTP or TCP socket since Java does not support Unix signals.
- The external program then typically compresses, delete or send the log files elsewhere.
(The other method of rotating files without doing a move and reopen is called "copy truncate" but has the potential of losing events. This method avoids that problem. )
Unfortunately Rainbow Gum does not offer a way to receive the external signal as this can vary greatly across applications. The following is an example of reopening outputs using HTTP for signaling./*
* This method could be bound to an internal HTTP route say /log/rotate such that curl
* http://localhost:8080/log/rotate will block and wait till all the applicable
* outputs have reopened.
*/
@RequestMapping("/log/rotate")
@ResponseBody
public String someInternalHttpRequestHandler() {
var gum = RainbowGum.getOrNull();
if (gum != null) {
List<LogResponse> response = gum.config() //
.outputRegistry() //
.reopen(); // Here is where we siginal to reopen outputs that support
// reopening.
return response.toString();
}
return "";
}
http://localhost:8080/log/rotate
and we have configured our logging like
logging.appenders=myappender
logging.appender.myappender.output=/var/log/app.log
/var/log/app.log
{
rotate 4
weekly
missingok
notifempty
compress
delaycompress
# we only need one call to reopen all files
sharedscripts
# in most cases it is best for rainbowgum recreate the file which wil happen after the postrotate
nocreate
postrotate
/usr/bin/curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/log/rotate | {
read status
if [ "$status" -ne 200 ]; then
logger "logrotate: Error hitting endpoint, received HTTP status $status"
fi
}
endscript
}
Filtering
Like logback Rainbow Gum provides two types of filtering through functional composition:- router based filtering - similar to logbacks normal filters.
- SLF4J based filtering - similar to logback Turbo Filters
Event based Filtering
Unfortunately at this time router based filtering is not configurable through properties and can only be added using Rainbow Gum builder and composition has to be done manually.An example using router based filtering:
RainbowGum.builder(config) //
.route(rb -> {
rb.factory(RouterFactory.of(e -> {
/*
* We only log DEBUG level events.
*/
return switch (e.level()) {
case DEBUG -> e;
default -> null;
};
}));
/*
* If we do not set the level correctly the router will never get the
* event regardless of the logic of the above filtering function.
*/
rb.level(Level.DEBUG);
});
LogRouter.Router
with a function.
Event based filtering is still based on the invariants
of level resolvers not changing (unless they are configured to be dynamic) so
not all events are delivered if the level resolved does not not allow it!
A work around is to adjust the levels of the router so that it does get
all events desired and is why filtering is associated with routing.
Unfortunately this can be inefficient as the event always has to be created
so if performance matters consider using SLF4J based filtering.
SLF4J based filtering
SLF4J based filtering is not simple filtering but rather decorating of the SLF4J loggers somewhat akin to Servlet filtering. Rainbow Gum hands off its generated SLF4J logger to you and you can decorate or even disregard it and provide your own implementation. This is extremely powerful and efficient but requires more work. There is not really an analog in Logback or Log4j but as usual with great power comes great responsibility!These filters are usually loaded with the ServiceLoader so manually using the Rainbow Gum builder is not required like router based filtering.
Installation
For most simply including the dependencyio.jstach.rainbowgum:rainbowgum
is enough. That dependency will transitively pull in the most commonly used modules.
For a more minimal setup (total application size in terms of classes)
io.jstach.rainbowgum:rainbowgum-core
and io.jstach.rainbowgum:rainbowgum-slf4j
(if using SLF4J)
should be used.
Maven
<properties>
<rainbowgum.version>0.8.0</rainbowgum.version>
</properties>
...
<dependencies>
<dependency>
<groupId>io.jstach.rainbowgum</groupId>
<artifactId>rainbowgum</artifactId>
<version>${rainbowgum.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<properties>
<rainbowgum.version>0.8.0</rainbowgum.version>
</properties>
...
<dependencies>
<dependency>
<groupId>io.jstach.rainbowgum</groupId>
<artifactId>rainbowgum-core</artifactId>
<version>${rainbowgum.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
Gradle
dependencies {
runtimeOnly 'io.jstach.rainbowgum:rainbowgum:0.8.0'
}
If you plan on configuring Rainbow Gum programmatically you will need to make a module and create a
service loader registration. In that case you will want the dependency like:
dependencies {
implementation 'io.jstach.rainbowgum:rainbowgum-core:0.8.0'
}
Extensions and Integrations
SLF4J 2.0
Rainbow Gum SLF4J moduleio.jstach.rainbowgum.slf4j
supports:
However there is currently no Marker
support.
Rainbow Gum supports key value pairs
in LoggingEventBuilder
by overlaying on top
of the current MDC at the time the event is constructed and put into the
events key values
.
The value parameter in
LoggingEventBuilder.addKeyValue(String,Object)
are
converted to String
immediately as only String values are
supported at this time.
Rainbow Gum SLF4J implementation is unique in that it has two special implementation of loggers:
- Level Logger - logger based on level threshold and can never change!
- Changing Logger - level and other configuration can change.
java.lang.System.Logger and java.util.logging
Rainbow Gumio.jstach.rainbowgum.jdk
module provides special integration and adapters for the builtin JDK logging facilities.
The impetus for this is these logging facilities can be used early in the JDK boot processes well before
logging has fully initialized. The default rainbow gum dependency has this integration as a transitive dependency.
The integration will make sure that neither the System.Logger or java.util.logging initialize
Rainbow Gum too early by queueing the events if some other facade is detected like SLF4J. When a Rainbow Gum initializes and
set as global the events will be replayed.
If the events level are equal to System.Logger.Level.ERROR
and a normal Rainbow Gum has not been bound
the messages will be printed to System.err
.
The idea is something catastrophic has happened that will probably cause Rainbow Gum to never load and thus never replay the events
and you will not be able to figure out what happened otherwise.
The exception of the above is if no other facade is detected
the module will initialize Rainbow Gum. The idea is that your application only uses the JDK logging facilities
(the Rainbow Gum SLF4J implementation is not found in the module/classpath). Thus only having the dependencies
rainbowgum-core
, and rainbowgum-jdk
will work as normal.
SLF4J does provide an adapter/bridge for the System.Logger (org.slf4j:slf4j-jdk-platform-logging) but its use may cause Rainbow Gum to initialize too early. However that maybe desirable if:
- You are sure that Rainbow Gum can initialize early
- Your application uses System.Logger
(the SLF4J adapter will initialize Rainbow Gum on System.Logger usage if using
io.jstach.rainbowgum.slf4j
)
The following System properties allow greater control of the queueing and initialization. Because of how early initialization can happen this configuration can only be done with System properties.
- "logging.global.queue.level" = level
- "logging.global.queue.error" = level
- "logging.systemlogger.initialize" = init option
NOTE: While the JDK System.Logger is good for low level libraries that rarely log it's API (and Rainbow Gum implementation) is not designed for performance. For applications and frameworks that do a lot of logging the SLF4J facade is the preferred choice.
Spring Boot
Rainbow Gum provides Spring Boot 3 or greater support that is maintained by the Rainbow Gum project.Spring Boot allows configuration through simple properties for logging that tries to be logging framework agnostic. How this works is Spring Boot configures the logging framework from its own set of properties. There is a shocking amount of code Spring Boot requires to do this for Logback and Log4J2 because both of those frameworks were not designed well for programmatic configuration.
Rainbow Gum key value configuration is purposely very similar and largely compatible with
Spring Boot's however Spring Boot still requires special integration if you would like
to use its configuration system (for example application.properties
).
This is because Spring Boot uses/needs logging for initialization of its configuration system.
<dependencies>
<dependency>
<groupId>io.jstach.rainbowgum</groupId>
<artifactId>rainbowgum-spring-boot-starter</artifactId>
<version>0.8.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!--
This is the important part as logback will be used otherwise
regardless if rainbow gum spring boot integration is used.
-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
rainbowgum-spring-boot-starter
or rainbowgum-spring-boot
Spring will re-initialize
java.util.logging
and take over it so again the integration is recommended.
See io.jstach.rainbowgum.spring.boot
.
Extension development guide
To a develop a custom plugin it is probably easiest to look at the other rainbow gum modules. An important concept with extensions are the registries which allow you to register a provider with a URI scheme.LogConfig
contains all the registries
and a special generic registry: ServiceRegistry
.
The service registry is allows other plugins to share components.
To access these registries at configuration time as a plugin is to implement
a RainbowGumServiceProvider.Configurator
.
Next one should construct builders that are properties friendly to create the components
so that programmatic configuration users can use your extension. Rainbow Gum provides
an annotation processor that helps create builders
io.jstach.rainbowgum.annotation
.
<properties>
<rainbowgum.version>0.8.0</rainbowgum.version>
</properties>
...
<dependencies>
<dependency>
<groupId>io.jstach.rainbowgum</groupId>
<artifactId>rainbowgum-core</artifactId>
<version>${rainbowgum.version}</version>
<scope>compile</scope>
</dependency>
<!-- This is the annotation processor -->
<dependency>
<groupId>io.jstach.rainbowgum</groupId>
<artifactId>rainbowgum-apt</artifactId>
<version>${rainbowgum.version}</version>
<!-- the following maven config is important as the annotation processor should never be a transitive dep -->
<scope>provided</scope>
<optional>true</optional>
</dependency>
</dependencies>
LogConfigurable
.
FAQ
There is a typo in this documentation how can I fix it?
If you would like to make corrections please file an issue or even better fork, edit, PR this file:doc/overview.html
Where is the Javadoc?
Shockingly this document is the Javadoc! To be precise it is the aggregate javadocoverview.html
.
The modules javadocs should be at the bottom of this document and the search bar at the top can be
used to find documented classes.