Plugins
Log4j plugin system is the de facto extension mechanism embraced by various Log4j Core components. Plugins make it possible for extensible components to receive feature implementations without any explicit links in between. It is analogous to a dependency injection framework, but curated for Log4j-specific needs.
|
Log4j plugin system is implemented by Log4j Core, the logging implementation. It is deliberately not a part of the Log4j API to keep the logging API footprint small. |
|
Did you know about Plugin reference, the documentation extracted from the source code of all predefined Log4j plugins? Like Javadoc, but specialized for plugins! |
In this section we will give an overview of the Log4j plugin system by answering certain questions:
Declaring plugins
A class can be declared as a plugin by adding a @Plugin annotation and a @Configurable annotation.
@Plugin::value-
Name of the plugin. If left unspecified, this defaults to the simple class name of the annotated class. This name should be unique within all plugins sharing the same
@Namepsace(in this case, the@Configurableannotation uses theCorenamespace). The plugin name is matched using case-insensitive equality. @Namespace::value-
Plugins are grouped together in namespaces where they can be identified by name. Namespaces are case-sensitive. Typical plugins use the
Corenamespace which is handled by the@Configurableannotation. @Configurable::elementType(deprecated)-
We don’t recommend the usage of
elementTypeanymore. Existing usages are kept for backward compatibility reasons with the legacy configuration syntax:<appender type="ConsoleAppender".
See LowerLookup.java (a lookup for lower-casing its input) for a simple example.
Click to read more on name collision and overriding an existing plugin
The name of a plugin given in either @Plugin::value or derived from the simple name of the annotated class should be distinct within the same @Namespace::value.
In case of a name collision, a warning will be emitted, and the plugin discovery order will determine the effective plugin.
For example, to override the File plugin which is provided by the built-in File Appender, you would need to place your plugin in a JAR file in the classpath ahead of Log4j Core JAR.
In an OSGi environment, the order that bundles are scanned for plugins generally follows the same order that bundles were installed into the framework; see getBundles() and SynchronousBundleListener.
In short, name collisions are even more unpredictable in an OSGi environment.
Declaring plugins represented in a configuration file
If your plugin needs to be represented by an element in a configuration file (such as an appender, layout, logger, or filter), following requirements must be met:
-
The
@Namespaceannotation of the@Plugin-annotated class must be set toNode.CORE_NAMESPACE(Core) or it must use the@Configurableannotation -
It must have a plugin factory
See JsonTemplateLayout.java for an example and notice these details:
-
There are two plugin declarations:
JsonTemplateLayoutandJsonTemplateLayout.EventTemplateAdditionalField -
Both plugin declarations
-
Use a
@Configurableand@Pluginannotation -
Provide a
@Factory-annotated static method
-
Declaring plugin factories
A plugin factory is responsible for
-
Creating an instance of the plugin
-
Receiving values (
Configurationinstance, configuration attributes, etc.) available in the context
Plugin factories are comparable to more general dependency injection frameworks and consist of strategies for binding configuration data to a plugin class instance.
A plugin factory typically consists of a static method in the plugin class annotated with a @FactoryType-annotated annotation such as @Factory or @PluginFactory.
Alternatively, a plugin can include a @Inject-annotated constructor (along with optional @Inject-annotated methods or fields).
This plugin factory executable (constructor or method) may include parameters for binding configuration data or injecting other shared instances as described below.
If a static method returns an instance that implements java.util.function.Supplier<T> (which includes both Builder<T> and Builder<T>), then this instance is further processed for dependency injection.
This makes it possible to create a builder class with @Inject-annotated methods whose parameters can be bound to injected data in the same fashion as parameters of the static factory method.
If a plugin class implements Collection or Map, then no factory method is used.
Instead, the class is instantiated using the default constructor, and all child configuration nodes are added to the Collection or Map.
Plugin factory attribute types
To allow the current Configuration to populate the correct arguments for the @Factory-annotated method or @Inject-annotated constructor or method, every argument to the executable must be annotated using one of the following attribute types.
@PluginAliases-
Identifies a list of aliases for a
@Plugin,@PluginAttribute, or@PluginBuilderAttribute. Aliases allow a plugin to be referenced by multiple names in configuration files. For example, a plugin with@PluginAliases({"MyApp", "MyApplication"})can be referred to by any of those names. Aliases are also supported for plugin attributes, allowing configuration elements to use alternative attribute names. @PluginAttribute-
Denotes a configuration element attribute. The parameter must be convertible from a
Stringusing aTypeConverter. Most built-in types are already supported, but customTypeConverterplugins may also be provided for more type support. Note thatPluginBuilderAttributecan be used in builder class fields as an easier way to provide default values. @PluginConfiguration(optional)-
The current
Configurationobject will be passed to the plugin as a parameter. Note that this annotation is optional as long as the type of the parameter or field isConfiguration. -
@PluginElement -
The parameter may represent a complex object that itself has parameters that can be configured. This also supports injecting an array of elements.
@PluginNode-
The current
Nodebeing parsed will be passed to the plugin as a parameter. @PluginValue-
The value of the current
Nodeor its attribute namedvalue. If@PluginValue::valueis overridden, then the value from the annotation is used as the attribute name to look up a value for.
Each attribute or element annotation must include the name that must be present in the configuration in order to match the configuration item to its respective parameter if the parameter name (or field name) does not match the expected name.
Plugin factory attribute type converters
TypeConverters are a certain group of plugins for converting Strings read from configuration file elements into the types used in plugin factory attributes.
Other plugins can already be injected via the @PluginElement annotation; now, any type supported by TypeConverters can be used in a @PluginAttribute-annotated factory attribute.
Conversion of enum types are supported on demand and do not require custom TypeConverters.
A large number of built-in Java classes (int, long, BigDecimal, etc.) are already supported; see CoreTypeConverters for a more exhaustive listing.
Note that type converters for primitive types and their respective boxed types are handled by anonymous type converters in TypeConverterFactory.
You can create custom TypeConverters as follows:
-
Extend from the
TypeConverterinterface -
Add a
@Pluginand@TypeConvertersannotation to the class (which is an alias for theTypeConverternamespace). -
Have a default constructor
-
Optionally, extend from
Comparable<TypeConverter<?>>, which will be used for determining the order in case of multipleTypeConvertercandidates for a certain type
See CoreTypeConverters.java for example implementations.
Plugin factory attribute validators
Plugin factory fields and parameters can be automatically validated at runtime using constraint validators inspired by Bean Validation.
The following annotations are bundled in Log4j, but custom ConstraintValidator can be created as well.
@Required-
This annotation validates that a value is non-empty. This covers a check for null as well as several other scenarios: empty
CharSequenceobjects, empty arrays, emptyCollectioninstances, and emptyMapinstances. @RequiredClass-
This annotation validates that a class can be loaded by name. This can be used on a plugin class to avoid loading the plugin unless the required class is present.
@ValidHost-
This annotation validates that a value corresponds to a valid host name. This uses the same validation as
InetAddress.getByName(String). @ValidPort-
This annotation validates that a value corresponds to a valid port number between 0 and 65535.
Plugin factory conditional annotations
Conditional annotations allow plugins to be registered only when certain conditions are met. They are typically used on bundle classes or factory methods to control whether bindings should be created based on the runtime environment.
The following built-in conditional annotations are available:
@ConditionalOnMissingBinding-
Checks if a binding exists for the return type of the annotated factory method. When no binding exists for that key, the method will be considered for binding. This is commonly used to provide default implementations that can be overridden by user-defined plugins.
@ConditionalOnPresentBindings-
Only creates a binding if all the requested dependencies exist. The required dependencies are specified via the
bindingsattribute. This is useful for plugins that require optional dependencies to be present on the classpath.
Custom conditions can be created by implementing the Condition interface and annotating a custom annotation with @Conditional.
Registering plugins
Registering plugins are done by placing a Log4j plugin class (i.e., a generated Log4jPlugins.java extending PluginService) into the classpath.
This class is generated using the PluginProcessor annotation processor at compile-time.
You need to configure your build tool as follows to employ PluginProcessor by the Java compiler:
-
Maven
-
Gradle
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<executions>
<execution>
<id>generate-log4j-plugin-descriptor</id>
<goals>
<goal>compile</goal>
</goals>
<phase>process-classes</phase>
<configuration>
<proc>only</proc>
<annotationProcessorPaths>
<!-- Include `log4j-plugin-processor` providing `org.apache.logging.log4j.plugin.processor.PluginProcessor` that generates `Log4jPlugins.java` -->
<path>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-plugin-processor</artifactId>
<version>3.0.0-beta3</version>
</path>
</annotationProcessorPaths>
<annotationProcessors>
<!-- Process sources using `org.apache.logging.log4j.plugin.processor.PluginProcessor` to generate `Log4j2Plugins.dat` -->
<processor>org.apache.logging.log4j.plugin.processor.PluginProcessor</processor>
</annotationProcessors>
</configuration>
</execution>
</executions>
</plugin>
dependencies {
// Process sources using `log4j-plugin-processor` providing `org.apache.logging.log4j.plugin.processor.PluginProcessor` that generates `Log4jPlugins.java` -->
annotationProcessor('org.apache.logging.log4j:log4j-plugin-processor:3.0.0-beta3')
}
Discovering plugins
PluginRegistry is responsible for discovering plugins and loading their descriptions.
-
When using a module path, these plugin services must be included in the
module-info.javafile via aprovides org.apache.logging.log4j.plugins.model.PluginService with org.example.plugins.Log4jPluginsline. -
When using a classpath, these plugin services must be included in a
META-INF/services/org.apache.logging.log4j.plugins.model.PluginServicefile (which is automatically generated by the plugin annotation processor).
See Registering plugins for details.
Loading plugins
It is pretty common that a plugin uses other plugins; appenders accept layouts, some layouts accept key-value pairs, etc. You can do this as follows:
-
If your plugin has a plugin factory (i.e., it is represented by a configuration file element), you can use the
@PluginElementannotation to receive other plugins. See@PluginElement("EventTemplateAdditionalField")usage inJsonTemplateLayout.javafor an example.