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, which is essentially composed of following attributes:

name

Name of the plugin. It is recommended to be distinct among plugins sharing the same category. name matching is case-insensitive.

category (optional)

A name used for grouping a set of plugins. category matching is case-sensitive.

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 attribute of plugins of a certain category is recommended to be distinct and this matching is case-insensitive. 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

    • Set the category attribute to Node.CATEGORY

    • Provide a @PluginBuilderFactory-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

Every plugin that needs to be represented by an element in a configuration file must declare a plugin factory using one of the following:

a @PluginFactory-annotated static method

What is expected to be received is modelled as method arguments. Intended for simple plugins that receive less than a handful of values.

See CsvParameterLayout.java for an example on @PluginFactory usage.

a @PluginBuilderFactory-annotated static method of return type Builder<T>

What is expected to be received is modelled as fields of a builder class. Intended for more sophisticated wiring needs.

Click for advantages of builder class over factory method
  • Attribute names don’t need to be specified, if they match the field name

  • Default values can be specified in code rather than through an annotation. This also allows a runtime-calculated default value, which isn’t allowed in annotations.

  • Default values are specified via code rather than relying on reflection and injection, so they work programmatically as well as in a configuration file.

  • Adding new optional parameters doesn’t require existing programmatic configuration to be refactored.

  • Easier to write unit tests using builders rather than factory methods with optional parameters.

See JsonTemplateLayout.java for an example on @PluginBuilderFactory usage.

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 @PluginFactory-annotated method (or fields for the builder class), every argument to the method must be annotated using one of the following attribute types.

@PluginAliases

Identifies a list of aliases for a @Plugin, @PluginAttribute, or @PluginBuilderAttribute

@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

The current Configuration object will be passed to the plugin as a parameter.

@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.

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. For plugin builders, the names of the fields will be used by default if no name is specified in the annotation.

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 TypeConverters for a more exhaustive listing.

You can create custom TypeConverters as follows:

  • Extend from the TypeConverter interface

  • Set the category attribute of the @Plugin annotation to TypeConverters.CATEGORY (TypeConverter). Unlike other plugins, the plugin name of a TypeConverter is purely cosmetic.

  • 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 TypeConverters.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.

@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.

Registering plugins

To properly work, each Log4j plugin needs:

  • To be registered in the Log4j Plugin Descriptor (i.e., Log4j2Plugins.dat). This file is generated using the PluginProcessor annotation processor at compile-time.

  • (Optionally) To be registered in the GraalVM reachability metadata descriptor, which will allow the plugin to be used in GraalVM native applications. The GraalVmProcessor annotation processor creates such a file at compile-time.

The GraalVmProcessor needs to know the groupId and artifactId coordinates of your project. These must be supplied to the processor using the log4j.graalvm.groupId and log4j.graalvm.artifactId annotation processor options.

You need to configure your build tool as follows to use both plugin processors:

  • 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-core` providing
               1. `org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor` that generates `Log4j2Plugins.dat`
               2. `org.apache.logging.log4j.core.config.plugins.processor.GraalVmProcessor` that generates the GraalVM reachability metadata file -->
          <path>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.24.3</version>
          </path>
        </annotationProcessorPaths>
        <annotationProcessors>
          <!-- Process sources using `PluginProcessor` to generate `Log4j2Plugins.dat` -->
          <processor>org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor</processor>
          <!-- Process sources using `GraalVmProcessor` to generate a GraalVM reachability metadata file -->
          <processor>org.apache.logging.log4j.core.config.plugins.processor.GraalVmProcessor</processor>
        </annotationProcessors>
        <compilerArgs>
          <!-- Provide the project coordinates to `GraalVmProcessor`: -->
          <arg>-Alog4j.graalvm.groupId=${project.groupId}</arg>
          <arg>-Alog4j.graalvm.artifactId=${project.artifactId}</arg>
        </compilerArgs>
      </configuration>
    </execution>
  </executions>
</plugin>
compileJava {
  // Provide the project coordinates to the `GraalVmProcessor`:
  options.compilerArgs << '-Alog4j.graalvm.groupId=org.example'
  options.compilerArgs << '-Alog4j.graalvm.artifactId=example'
}

dependencies {
  // Process sources using:
  // * `PluginProcessor` to generate `Log4j2Plugins.dat`
  // * `GraalVmProcessor` to generate a GraalVM reachability metadata file
  annotationProcessor('org.apache.logging.log4j:log4j-core:2.24.3')
}

Discovering plugins

PluginManager is responsible for discovering plugins and loading their descriptions. It locates plugins by looking in following places in given order:

  1. Plugin descriptor files on the classpath (using the class loader that loaded the log4j-core artifact). These files are generated automatically at compile-time by the Log4j plugin annotation processor. See Registering plugins for details.

  2. [OSGi only] Serialized plugin listing files in each active OSGi bundle. A BundleListener is added on activation to continue checking new bundles after Log4j Core has started.

  3. [Deprecated] A comma-separated list of packages specified by the log4j.plugin.packages system property

  4. [Deprecated] Packages passed to the static PluginManager.addPackages() method before Log4j configuration takes place

  5. [Deprecated] The packages attribute declared at the root element of your Log4j configuration file

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: