Backend Naming
Table of contents
- Introduction
- Why Logger Naming Matters
- How Existing Logging Systems Handle Naming
- What Flogger Does Differently
- Flogger Next’s Naming Scheme
- Logger Backend Naming Strategies
- Summary
- Installation
Introduction
This page is best read in conjunction with the section Debugging With Flogger.
Almost every modern Java logging system has the concept of “named loggers”. In most cases, the name of a logger is just the name of the class in which that logger is used, and class will initialize their logger instance something like:
import java.util.logging.Logger;
class MyClass {
private static final Logger logger = Logger.getLogger(MyClass.class.getName());
...
}
However, contrary to what some people might think, the logger name is not used to determine the “log site” information often displayed in log messages. Log site information must be accurate, and this means resolving nested or inner class names as necessary.
2024-04-20T13:50:12.3456Z INFO [com.thing.project.foo.Frobulator$Listener#handleFrob] <message>
\----- This is NOT the logger name -----/
So, if a logger’s name is not used to determine log site information, and it doesn’t affect the content of log messages, why not just have a single logger instance for all classes?
Why Logger Naming Matters
Logger naming is primarily important for logging configuration, and allows different logger instances to hold distinct logging configurations. This is most commonly used to set different log levels for different classes.
While you could implement a mapping from “logger name” to logging configuration in many ways, the fact that logger instances typically hold their configuration directly is a very important design choice. By holding configuration, especially the logger’s enabled log level, directly in the instance, a logger can rapidly determine which log statements are disabled and should be ignored.
This matters because it is the more fine-grained log statements which are disabled and, since these most often appear in loops and innermost code, they are processed hundreds or thousands of times more often than enabled ones.
This means that minimizing the cost of deciding to ignore disabled log statements is one of the most important factors in efficient logger design. For example, the JDK logger goes to great lengths to make retrieving a logger’s log level as simple as a single read of a volatile
integer field.
How Existing Logging Systems Handle Naming
Since a logger typically holds its own configuration, for maximum flexibility of log level control, it becomes desirable to have many logger instances, since sharing a logger between many classes would limit the ability to control log levels independently. This is especially important for library code which is expected to be used in many environments, and cannot predict how it might need to be configured during debugging.
This results in the most common logger configuration being “one logger per class”, with the logger instance named after the class. Given this scheme, and the ability to create “intermediate” loggers named after parent packages, it’s possible to provide any combination of logging configuration, since every location in the namespace can have logging configuration associated with it.
While simple, this means that every single class must allocate a new logger instance during class initialization, which both adds to the time taken to load each class, and incurs hundreds or thousands of potentially non-trivially sized allocations.
And in reality, almost all of these logger instances will end up with effectively identical logging configurations almost all the time, since it’s only during debugging that the ability to individually control loggers on a per-class basis becomes important.
What Flogger Does Differently
Flogger sits above other logging systems, and FluentLogger
instances are associated with a LoggerBackend
, which provides an abstraction layer. Because Flogger was designed to handle different logger naming schemes it does not directly expose the backend name to the user.
In fact, very deliberately, there is no way to request a FluentLogger
instance with a specific name, and it is only possible to ask for “a logger suitable for the current class”.
import net.goui.flogger.FluentLogger;
class MyClass {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
}
By using a static method, rather than a constructor, it is possible for the same logger instance to be shared between many classes. Furthermore, there is no requirement that each FluentLogger
instance be associated with its own LoggerBackend
instance, as these too can be shared. And finally, there is no public API for the use to obtain either the name of a FluentLogger
instance, or its LoggerBackend
.
However, there is still the need to provide sufficient logger configurability during debugging, and especially to ensure that things like
ScopedLoggingContext
work as expected.
Why is ScopedLoggingContext important?
The ScopedLoggingContext
mechanism (see also Advanced Usage) is an important Flogger feature to enhance debugging. It permits logging to be “forced” within user defined contexts for arbitrary packages or classes. This largely replaces the need for users to edit and reload logging configuration while debugging, while providing proper scoping for log level control (e.g. for a single request).
Since ScopedLoggingContext
specifies the log level to be modified by using class or package names, it exposes an assumed mapping from class name to logger behaviour. So while there is no explicit way for users to determine a logger’s name, there is a need to have some kind of internal mapping which can associate loggers and class names.
Flogger Next’s Naming Scheme
Flogger Next offers the user a configurable naming scheme designed to be flexible enough to handle the need for fine-grained logger control, while dramatically reducing the number of logger backends which need to be allocated.
To achieve this, a user specifiable LoggerBackend
naming strategy is installed at application startup, mapping logging class names to backend names.
In this example, any classes under the com.foo.*
namespace are configured to use a shared LoggerBackend
named com.foo
. This is achieved with the addition of simple Flogger Next properties:
flogger.backend_naming.roots.size=1
flogger.backend_naming.roots.0=com.foo
Reducing Class Initialization Cost
While there is still the need for the Flogger Next FluentLogger
instances to be allocated on a per-class basis, these instances are now small (two fields) and reference a cached, shared LoggerBackend
instance. And since Flogger Next also defers the creation of the underlying logger instances until first use, the overall static initialization cost is typically one or two small object allocations with no additional overhead.
Logger Backend Naming Strategies
Of course, not every application will want the same naming strategy for the logger backends, so Flogger Next provides a simple way to configure backend naming via Flogger properties. Different strategies can be applied for different package hierarchies, and existing system logging configuration can be imported for use by Flogger.
By default, Flogger Next names each LoggerBackend
after a class with a FluentLogger
in. This is the most flexible approach when debugging because every logger can be controlled individually, but may cost you hundreds or even thousands of additional logger allocations, and affect class loading and static initialization performance.
At the other extreme, you could configure a single root logger which would result in every FluentLogger
sharing a single LoggerBackend
instance. This would mean you could no longer control log levels separately during debugging, means if you need to enable FINE
logging somewhere, you need to enable it everywhere, which could produce an excessive amount of unhelpful log output and even affect performance.
Naming Strategy: No Name Mapping
This is the default strategy if no naming properties are set and matches Google’s Fluent Logger behaviour. Each FluentLogger
will be associated with a LoggerBackend
with the same name as the logging class.
For this strategy, logger backend caching is not enabled, since backends are not shared.
Naming Strategy: Per Package Loggers
This strategy is probably the simplest non-default strategy and should result in a significant reduction in allocation of underlying logger instances. Each FluentLogger
will be associated with a LoggerBackend
with the name of the package of the logging class.
To enable this strategy, simply set the trim_at_least
naming option:
flogger.backend_naming.trim_at_least=1
You can also set higher values for name trimming to switch to using parent package names etc.
If
trim_at_least
has a positive value,LoggerBackend
caching is enabled.
Naming Strategy: Maximum Package Depth
For applications with deep class hierarchies, it may also be useful to limit the backend package depth. This strategy can be used alongside trim_at_least
, but trim_at_least
will be applied first, so you can always ensure backends use package names rather than individual class names.
To enable this strategy, simply set the retain_at_most
naming option:
flogger.backend_naming.retain_at_most=N
If
retain_at_most
has a positive value,LoggerBackend
caching is enabled.
Naming Strategy: Explicit Package Roots
As well as defining how arbitrary logging class names are mapped to backend names, you can also explicitly set package roots to be used for any classes in that hierarchy.
To enable this strategy, add package specifiers to the flogger.backend_naming.roots
array.
flogger.backend_naming.roots.size=3
flogger.backend_naming.roots.0=com.myproject.foo
flogger.backend_naming.roots.1=com.myproject.bar
flogger.backend_naming.roots.2=org.other.project.package
To avoid having to maintain a large number of package roots, it is also possible to append one or more “wildcard” suffixes (.*
) to package root specifications. The configuration below has the same effect as the one above, except it also defines roots for any other packages within com.myproject
.
flogger.backend_naming.roots.size=2
flogger.backend_naming.roots.0=com.myproject.*
flogger.backend_naming.roots.1=org.other.project.package
If a logging class name matches a root specifier, then options
trim_at_least
andretain_at_most
are not applied.
Naming Strategy: System Package Roots
If the underlying logging system defines a static configuration for loggers, this can be imported to provide additional package roots which are then merged with any explicitly provided roots.
For example, given the following configuration in a log4j2.xml
file:
<Loggers>
<!-- Root logger referring to console appender -->
<Root level="warn" additivity="false">
<AppenderRef ref="console"/>
</Root>
<Logger name="com.myproject.foo" level="info"/>
<Logger name="com.myproject.bar" level="warn"/>
<Logger name="org.other.project.package" level="info"/>
</Loggers>
The system roots com.myproject.foo
, com.myproject.bar
and org.other.project.package
will be used automatically. This is very useful because it automatically aligns the loggers for which an initial configuration was defined with Flogger’s naming strategy. This ensures that any statically configured logger can be used to control FluentLogger instances.
To enable this strategy, set use_system_roots
:
flogger.backend_naming.use_system_roots=true
The unnamed “root” logger will not be imported as an explicit system root, since it always exists and no name mapping should be applied to it.
When using the JDK logger backend, it is necessary to install the
net.goui.flogger.backend.system.FloggerConfig
as the logging config class in order to extract system roots.
-Djava.util.logging.config.class=net.goui.flogger.backend.system.FloggerConfig
Default Extension For Root Entries
You can also set a default depth to extend matched root entries by, which applies to both explicit root entries and system roots:
flogger.backend_naming.default_root_extend=N
This has the same effect as adding N
copies of the wildcard .*
suffix to all roots (including inherited system roots) for which no wildcard was specified. This is a useful way to extend system roots by some number of levels, to gain a little extra backend configurability at runtime.
Summary
The following diagram sums up the difference between the traditional approach (left), where every logger can be individually configured, with Flogger Next’s approach (right), where logging configuration is typically controlled at the package level, with FluentLogger
instances inheriting configuration from the nearest configurable system logger.
Even in this simple example, the number of required system loggers is less than half, and for typical code structure where many classes exist per package, it would probably be at least an order of magnitude less.
However, for modern server based applications, this reduction in fine-grained configurability is not expected to be an issue, because in many cases it would be inefficient or impractical to try to debug an issue by altering the log levels of individual classes. See Debugging With Flogger for more.
Installation
JDK logging backend (replaces the com.google.flogger:flogger-system-backend
dependency):
<dependency>
<groupId>net.goui.flogger.next</groupId>
<artifactId>backend-system</artifactId>
<version>${flogger-next.version}</version>
</dependency>
Log4J 2 backend (replaces the com.google.flogger:flogger-log4j2-backend
dependency):
<dependency>
<groupId>net.goui.flogger.next</groupId>
<artifactId>backend-log4j</artifactId>
<version>${flogger-next.version}</version>
</dependency>