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 @Configurable annotation uses the Core namespace). 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 Core namespace which is handled by the @Configurable annotation.

@Configurable::elementType (deprecated)

We don’t recommend the usage of elementType anymore. 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:

See JsonTemplateLayout.java for an example and notice these details:

  • There are two plugin declarations: JsonTemplateLayout and JsonTemplateLayout.EventTemplateAdditionalField

  • Both plugin declarations

    • Use a @Configurable and @Plugin annotation

    • Provide a @Factory-annotated static method

Declaring plugin factories

A plugin factory is responsible for

  • Creating an instance of the plugin

  • Receiving values (Configuration instance, 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 String using a TypeConverter. Most built-in types are already supported, but custom TypeConverter plugins may also be provided for more type support. Note that PluginBuilderAttribute can be used in builder class fields as an easier way to provide default values.

@PluginConfiguration (optional)

The current Configuration object 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 is Configuration.

@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 Node being parsed will be passed to the plugin as a parameter.

@PluginValue

The value of the current Node or its attribute named value. If @PluginValue::value is 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 TypeConverter interface

  • Add a @Plugin and @TypeConverters annotation to the class (which is an alias for the TypeConverter namespace).

  • Have a default constructor

  • Optionally, extend from Comparable<TypeConverter<?>>, which will be used for determining the order in case of multiple TypeConverter candidates 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 CharSequence objects, empty arrays, empty Collection instances, and empty Map instances.

@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 bindings attribute. 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.java file via a provides org.apache.logging.log4j.plugins.model.PluginService with org.example.plugins.Log4jPlugins line.

  • When using a classpath, these plugin services must be included in a META-INF/services/org.apache.logging.log4j.plugins.model.PluginService file (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: