| //// |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| https://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| //// |
| = Extending Log4j 2 |
| Ralph Goers <rgoers@apache.org> |
| |
| Log4j 2 provides numerous ways that it can be manipulated and extended. |
| This section includes an overview of the various ways that are directly |
| supported by the Log4j 2 implementation. |
| |
| [#LoggerContextFactory] |
| == LoggerContextFactory |
| |
| The `LoggerContextFactory` binds the Log4j API to its implementation. |
| The Log4j `LogManager` locates a `LoggerContextFactory` by using |
| java.util.ServiceLoader to locate all instances of |
| `org.apache.logging.log4j.spi.Provider`. Each implementation must |
| provide a class that extends`org.apache.logging.log4j.spi.Provider` and |
| should have a no-arg constructor that delegates to Provider's |
| constructor passing the Priority, the API versions it is compatible |
| with, and the class that implements |
| `org.apache.logging.log4j.spi.LoggerContextFactory`. Log4j will compare |
| the current API version and if it is compatible the implementation will |
| be added to the list of providers. The API version in |
| `org.apache.logging.log4j.LogManager` is only changed when a feature is |
| added to the API that implementations need to be aware of. If more than |
| one valid implementation is located the value for the Priority will be |
| used to identify the factory with the highest priority. Finally, the |
| class that implements |
| `org.apache.logging.log4j.spi.LoggerContextFactory` will be instantiated |
| and bound to the LogManager. In Log4j 2 this is provided by |
| `Log4jContextFactory`. |
| |
| Applications may change the LoggerContextFactory that will be used by |
| |
| 1. Create a binding to the logging implementation. |
| .. Implement a new `LoggerContextFactory`. |
| .. Implement a class that extends `org.apache.logging.spi.Provider.` |
| with a no-arg constructor that calls super-class's constructor with the |
| Priority, the API version(s), `LoggerContextFactory` class, and |
| optionally, a `ThreadContextMap` implementation class. |
| .. Create a `META-INF/services/org.apache.logging.spi.Provider` file |
| that contains the name of the class that implements |
| `org.apache.logging.spi.Provider`. |
| 2. Setting the system property log4j2.loggerContextFactory to the name |
| of the `LoggerContextFactory` class to use. |
| 3. Setting the property "log4j2.loggerContextFactory" in a properties |
| file named "log4j2.LogManager.properties" to the name of the |
| LoggerContextFactory class to use. The properties file must be on the |
| classpath. |
| |
| [#ContextSelector] |
| == ContextSelector |
| |
| ContextSelectors are called by the |
| link:../log4j-core/apidocs/org/apache/logging/log4j/core/impl/Log4jContextFactory.html[Log4j |
| LoggerContext factory]. They perform the actual work of locating or |
| creating a LoggerContext, which is the anchor for Loggers and their |
| configuration. ContextSelectors are free to implement any mechanism they |
| desire to manage LoggerContexts. The default Log4jContextFactory checks |
| for the presence of a System Property named "Log4jContextSelector". If |
| found, the property is expected to contain the name of the Class that |
| implements the ContextSelector to be used. |
| |
| Log4j provides five ContextSelectors: |
| |
| link:../log4j-core/apidocs/org/apache/logging/log4j/core/selector/BasicContextSelector.html[`BasicContextSelector`]:: |
| Uses either a LoggerContext that has been stored in a ThreadLocal or a |
| common LoggerContext. |
| link:../log4j-core/apidocs/org/apache/logging/log4j/core/selector/ClassLoaderContextSelector.html[`ClassLoaderContextSelector`]:: |
| Associates LoggerContexts with the ClassLoader that created the caller |
| of the getLogger call. This is the default ContextSelector. |
| link:../log4j-core/apidocs/org/apache/logging/log4j/core/selector/JndiContextSelector.html[`JndiContextSelector`]:: |
| Locates the LoggerContext by querying JNDI. |
| link:../log4j-core/apidocs/org/apache/logging/log4j/core/async/AsyncLoggerContextSelector.html[`AsyncLoggerContextSelector`]:: |
| Creates a LoggerContext that ensures that all loggers are |
| AsyncLoggers. |
| link:../log4j-core/apidocs/org/apache/logging/log4j/core/osgi/BundleContextSelector.html[`BundleContextSelector`]:: |
| Associates LoggerContexts with the ClassLoader of the bundle that |
| created the caller of the getLogger call. This is enabled by default |
| in OSGi environments. |
| |
| [#ConfigurationFactory] |
| == ConfigurationFactory |
| |
| Modifying the way in which logging can be configured is usually one of |
| the areas with the most interest. The primary method for doing that is |
| by implementing or extending a |
| link:../log4j-core/apidocs/org/apache/logging/log4j/core/config/ConfigurationFactory.html[`ConfigurationFactory`]. |
| Log4j provides two ways of adding new ConfigurationFactories. The first |
| is by defining the system property named "log4j.configurationFactory" to |
| the name of the class that should be searched first for a configuration. |
| The second method is by defining the `ConfigurationFactory` as a `Plugin`. |
| |
| All the ConfigurationFactories are then processed in order. Each factory |
| is called on its `getSupportedTypes` method to determine the file |
| extensions it supports. If a configuration file is located with one of |
| the specified file extensions then control is passed to that |
| `ConfigurationFactory` to load the configuration and create the |
| `Configuration` object. |
| |
| Most `Configuration` extend the `BaseConfiguration` class. This class |
| expects that the subclass will process the configuration file and create |
| a hierarchy of `Node` objects. Each `Node` is fairly simple in that it |
| consists of the name of the node, the name/value pairs associated with |
| the node, The `PluginType` of the node and a List of all of its child |
| Nodes. `BaseConfiguration` will then be passed the `Node` tree and |
| instantiate the configuration objects from that. |
| |
| [source,java] |
| ---- |
| @Plugin(name = "XMLConfigurationFactory", category = "ConfigurationFactory") |
| @Order(5) |
| public class XMLConfigurationFactory extends ConfigurationFactory { |
| |
| /** |
| * Valid file extensions for XML files. |
| */ |
| public static final String[] SUFFIXES = new String[] {".xml", "*"}; |
| |
| /** |
| * Returns the Configuration. |
| * @param loggerContext The logger context. |
| * @param source The InputSource. |
| * @return The Configuration. |
| */ |
| @Override |
| public Configuration getConfiguration(final LoggerContext loggerContext, final ConfigurationSource source) { |
| return new XmlConfiguration(loggerContext, source); |
| } |
| |
| /** |
| * Returns the file suffixes for XML files. |
| * @return An array of File extensions. |
| */ |
| public String[] getSupportedTypes() { |
| return SUFFIXES; |
| } |
| } |
| ---- |
| |
| [#LoggerConfig] |
| == LoggerConfig |
| |
| `LoggerConfig` objects are where Loggers created by applications tie into |
| the configuration. The Log4j implementation requires that all |
| LoggerConfigs be based on the LoggerConfig class, so applications |
| wishing to make changes must do so by extending the `LoggerConfig` class. |
| To declare the new `LoggerConfig`, declare it as a Plugin of type "Core" |
| and providing the name that applications should specify as the element |
| name in the configuration. The `LoggerConfig` should also define a |
| PluginFactory that will create an instance of the `LoggerConfig`. |
| |
| The following example shows how the root `LoggerConfig` simply extends a |
| generic `LoggerConfig`. |
| |
| [source,java] |
| ---- |
| @Plugin(name = "root", category = "Core", printObject = true) |
| public static class RootLogger extends LoggerConfig { |
| |
| @PluginFactory |
| public static LoggerConfig createLogger(@PluginAttribute(value = "additivity", defaultBooleanValue = true) boolean additivity, |
| @PluginAttribute(value = "level", defaultStringValue = "ERROR") Level level, |
| @PluginElement("AppenderRef") AppenderRef[] refs, |
| @PluginElement("Filters") Filter filter) { |
| List<AppenderRef> appenderRefs = Arrays.asList(refs); |
| return new LoggerConfig(LogManager.ROOT_LOGGER_NAME, appenderRefs, filter, level, additivity); |
| } |
| } |
| ---- |
| |
| [#LogEventFactory] |
| == LogEventFactory |
| |
| A `LogEventFactory` is used to generate LogEvents. Applications may |
| replace the standard `LogEventFactory` by setting the value of the system |
| property `log4j2.logEventFactory` to the name of the custom `LogEventFactory` |
| class. |
| |
| Note: When log4j is configured to have link:async.html#AllAsync[all |
| loggers asynchronous], log events are pre-allocated in a ring buffer and |
| the `LogEventFactory` is not used. |
| |
| [#MessageFactory] |
| == MessageFactory |
| |
| A `MessageFactory` is used to generate `Message` objects. Applications may |
| replace the standard `ParameterizedMessageFactory` (or |
| `ReusableMessageFactory` in garbage-free mode) by setting the value of the |
| system property `log4j2.messageFactory` to the name of the custom |
| `MessageFactory` class. |
| |
| Flow messages for the `Logger.entry()` and `Logger.exit()` methods have |
| a separate `FlowMessageFactory`. Applications may replace the |
| `DefaultFlowMessageFactory` by setting the value of the system property |
| `log4j2.flowMessageFactory` to the name of the custom `FlowMessageFactory` |
| class. |
| |
| [#Lookups] |
| == Lookups |
| |
| Lookups are the means in which parameter substitution is performed. |
| During Configuration initialization an "Interpolator" is created that |
| locates all the Lookups and registers them for use when a variable needs |
| to be resolved. The interpolator matches the "prefix" portion of the |
| variable name to a registered Lookup and passes control to it to resolve |
| the variable. |
| |
| A Lookup must be declared using a `@Plugin` annotation with a `type` of |
| "Lookup". The `name` specified on the `@Plugin` annotation will be used to |
| match the prefix. Unlike other Plugins, Lookups do not use a |
| `@PluginFactory`. Instead, they are required to provide a constructor that |
| accepts no arguments. The example below shows a Lookup that will return |
| the value of a System Property. |
| |
| The provided Lookups are documented here: link:./lookups.html[Lookups] |
| |
| [source,java] |
| ---- |
| @Plugin(name = "sys", category = "Lookup") |
| public class SystemPropertiesLookup implements StrLookup { |
| |
| /** |
| * Lookup the value for the key. |
| * @param key the key to be looked up, may be null |
| * @return The value for the key. |
| */ |
| public String lookup(String key) { |
| return System.getProperty(key); |
| } |
| |
| /** |
| * Lookup the value for the key using the data in the LogEvent. |
| * @param event The current LogEvent. |
| * @param key the key to be looked up, may be null |
| * @return The value associated with the key. |
| */ |
| public String lookup(LogEvent event, String key) { |
| return System.getProperty(key); |
| } |
| } |
| ---- |
| |
| [#Filters] |
| == Filters |
| |
| As might be expected, Filters are the used to reject or accept log |
| events as they pass through the logging system. A Filter is declared |
| using a `@Plugin` annotation of `type` "Core" and an `elementType` of "filter". |
| The `name` attribute on the Plugin annotation is used to specify the name |
| of the element users should use to enable the Filter. Specifying the |
| `printObject` attribute with a value of "true" indicates that a call to |
| `toString` will format the arguments to the filter as the configuration is |
| being processed. The Filter must also specify a `@PluginFactory` method |
| or `@PluginFactoryBuilder` builder class and method |
| that will be called to create the Filter |
| |
| The example below shows a Filter used to reject LogEvents based upon |
| their logging level. Notice the typical pattern where all the filter |
| methods resolve to a single filter method. |
| |
| [source,java] |
| ---- |
| @Plugin(name = "ThresholdFilter", category = "Core", elementType = "filter", printObject = true) |
| public final class ThresholdFilter extends AbstractFilter { |
| |
| private final Level level; |
| |
| private ThresholdFilter(Level level, Result onMatch, Result onMismatch) { |
| super(onMatch, onMismatch); |
| this.level = level; |
| } |
| |
| public Result filter(Logger logger, Level level, Marker marker, String msg, Object[] params) { |
| return filter(level); |
| } |
| |
| public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) { |
| return filter(level); |
| } |
| |
| public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) { |
| return filter(level); |
| } |
| |
| @Override |
| public Result filter(LogEvent event) { |
| return filter(event.getLevel()); |
| } |
| |
| private Result filter(Level level) { |
| return level.isAtLeastAsSpecificAs(this.level) ? onMatch : onMismatch; |
| } |
| |
| @Override |
| public String toString() { |
| return level.toString(); |
| } |
| |
| /** |
| * Create a ThresholdFilter. |
| * @param loggerLevel The log Level. |
| * @param match The action to take on a match. |
| * @param mismatch The action to take on a mismatch. |
| * @return The created ThresholdFilter. |
| */ |
| @PluginFactory |
| public static ThresholdFilter createFilter(@PluginAttribute(value = "level", defaultStringValue = "ERROR") Level level, |
| @PluginAttribute(value = "onMatch", defaultStringValue = "NEUTRAL") Result onMatch, |
| @PluginAttribute(value = "onMismatch", defaultStringValue = "DENY") Result onMismatch) { |
| return new ThresholdFilter(level, onMatch, onMismatch); |
| } |
| } |
| ---- |
| |
| [#Appenders] |
| == Appenders |
| |
| Appenders are passed an event, (usually) invoke a Layout to format the |
| event, and then "publish" the event in whatever manner is desired. |
| Appenders are declared as Plugins with a `type` of "Core" and an |
| `elementType` of "appender". The `name` attribute on the Plugin annotation |
| specifies the name of the element users must provide in their |
| configuration to use the Appender. Appenders should specify `printObject` |
| as "true" if the toString method renders the values of the attributes |
| passed to the Appender. |
| |
| Appenders must also declare a `@PluginFactory` method or `@PluginFactoryBuilder` |
| builder class and method that will create the appender. The example below shows |
| an Appender named "Stub" that can be used as an initial template. |
| |
| Most Appenders use Managers. A manager actually "owns" the resources, |
| such as an `OutputStream` or socket. When a reconfiguration occurs a new |
| Appender will be created. However, if nothing significant in the |
| previous Manager has changed, the new Appender will simply reference it |
| instead of creating a new one. This insures that events are not lost |
| while a reconfiguration is taking place without requiring that logging |
| pause while the reconfiguration takes place. |
| |
| [source,java] |
| ---- |
| @Plugin(name = "Stub", category = "Core", elementType = "appender", printObject = true) |
| public final class StubAppender extends OutputStreamAppender { |
| |
| private StubAppender(String name, Layout layout, Filter filter, StubManager manager, |
| boolean ignoreExceptions) { |
| } |
| |
| @PluginFactory |
| public static StubAppender createAppender(@PluginAttribute("name") String name, |
| @PluginAttribute("ignoreExceptions") boolean ignoreExceptions, |
| @PluginElement("Layout") Layout layout, |
| @PluginElement("Filters") Filter filter) { |
| |
| if (name == null) { |
| LOGGER.error("No name provided for StubAppender"); |
| return null; |
| } |
| |
| StubManager manager = StubManager.getStubManager(name); |
| if (manager == null) { |
| return null; |
| } |
| if (layout == null) { |
| layout = PatternLayout.createDefaultLayout(); |
| } |
| return new StubAppender(name, layout, filter, manager, ignoreExceptions); |
| } |
| } |
| ---- |
| |
| [#Layouts] |
| == Layouts |
| |
| Layouts perform the formatting of events into the printable text that is |
| written by Appenders to some destination. All Layouts must implement the |
| `Layout` interface. Layouts that format the event into a `String` should |
| extend `AbstractStringLayout`, which will take care of converting the |
| `String` into the required byte array. |
| |
| Every Layout must declare itself as a plugin using the `@Plugin` |
| annotation. The `type` must be "Core", and the `elementType` must be |
| "layout". `printObject` should be set to "true" if the plugin's `toString` |
| method will provide a representation of the object and its parameters. |
| The name of the plugin must match the value users should use to specify |
| it as an element in their Appender configuration. The plugin also must |
| provide a static method annotated as a `@PluginFactory` and with each of |
| the methods parameters annotated with `@PluginAttribute` or `@PluginElement` as |
| appropriate. The plugin can alternatively use the plugin builder notation. |
| |
| [source,java] |
| ---- |
| @Plugin(name = "SampleLayout", category = "Core", elementType = "layout", printObject = true) |
| public class SampleLayout extends AbstractStringLayout { |
| |
| protected SampleLayout(boolean locationInfo, boolean properties, boolean complete, |
| Charset charset) { |
| } |
| |
| @PluginFactory |
| public static SampleLayout createLayout(@PluginAttribute("locationInfo") boolean locationInfo, |
| @PluginAttribute("properties") boolean properties, |
| @PluginAttribute("complete") boolean complete, |
| @PluginAttribute(value = "charset", defaultStringValue = "UTF-8") Charset charset) { |
| return new SampleLayout(locationInfo, properties, complete, charset); |
| } |
| } |
| ---- |
| |
| [#PatternConverters] |
| == PatternConverters |
| |
| PatternConverters are used by the PatternLayout to format the log event |
| into a printable `String`. Each Converter is responsible for a single kind |
| of manipulation, however Converters are free to format the event in |
| complex ways. For example, there are several converters that manipulate |
| Throwables and format them in various ways. |
| |
| A PatternConverter must first declare itself as a Plugin using the |
| standard `@Plugin` annotation but must specify value of "Converter" on the |
| `type` attribute. Furthermore, the Converter must also specify the |
| `@ConverterKeys` annotation to define the tokens that can be specified in |
| the pattern (preceded by a '%' character) to identify the Converter. |
| |
| Unlike most other Plugins, Converters do not use a `@PluginFactory`. |
| Instead, each Converter is required to provide a static `newInstance` |
| method that accepts an array of `String` as the only parameter. The |
| `String[]` is the values that are specified within the curly braces |
| that can follow the converter key. |
| |
| The following shows the skeleton of a Converter plugin. |
| |
| [source,java] |
| ---- |
| @Plugin(name = "query", category = "Converter") |
| @ConverterKeys({"q", "query"}) |
| public final class QueryConverter extends LogEventPatternConverter { |
| |
| public QueryConverter(String[] options) { |
| } |
| |
| public static QueryConverter newInstance(final String[] options) { |
| return new QueryConverter(options); |
| } |
| } |
| ---- |
| |
| [#Plugin_Builders] |
| == Plugin Builders |
| |
| Some plugins take a lot of optional configuration options. When a plugin |
| takes many options, it is more maintainable to use a builder class |
| rather than a factory method (see _Item 2: Consider a builder when faced |
| with many constructor parameters_ in _Effective Java_ by Joshua Bloch). |
| There are some other advantages to using an annotated builder class over |
| an annotated 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 (also allowing a runtime-calculated default value which isn't |
| allowed in annotations). |
| * 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. |
| * Default values are specified via code rather than relying on |
| reflection and injection, so they work programmatically as well as in a |
| configuration file. |
| |
| Here is an example of a plugin factory from `ListAppender`: |
| |
| [source,java] |
| ---- |
| @PluginFactory |
| public static ListAppender createAppender( |
| @PluginAttribute("name") @Required(message = "No name provided for ListAppender") final String name, |
| @PluginAttribute("entryPerNewLine") final boolean newLine, |
| @PluginAttribute("raw") final boolean raw, |
| @PluginElement("Layout") final Layout<? extends Serializable> layout, |
| @PluginElement("Filter") final Filter filter) { |
| return new ListAppender(name, filter, layout, newLine, raw); |
| } |
| ---- |
| |
| Here is that same factory using a builder pattern instead: |
| |
| [source,java] |
| ---- |
| @PluginBuilderFactory |
| public static Builder newBuilder() { |
| return new Builder(); |
| } |
| |
| public static class Builder implements org.apache.logging.log4j.core.util.Builder<ListAppender> { |
| |
| @PluginBuilderAttribute |
| @Required(message = "No name provided for ListAppender") |
| private String name; |
| |
| @PluginBuilderAttribute |
| private boolean entryPerNewLine; |
| |
| @PluginBuilderAttribute |
| private boolean raw; |
| |
| @PluginElement("Layout") |
| private Layout<? extends Serializable> layout; |
| |
| @PluginElement("Filter") |
| private Filter filter; |
| |
| public Builder setName(final String name) { |
| this.name = name; |
| return this; |
| } |
| |
| public Builder setEntryPerNewLine(final boolean entryPerNewLine) { |
| this.entryPerNewLine = entryPerNewLine; |
| return this; |
| } |
| |
| public Builder setRaw(final boolean raw) { |
| this.raw = raw; |
| return this; |
| } |
| |
| public Builder setLayout(final Layout<? extends Serializable> layout) { |
| this.layout = layout; |
| return this; |
| } |
| |
| public Builder setFilter(final Filter filter) { |
| this.filter = filter; |
| return this; |
| } |
| |
| @Override |
| public ListAppender build() { |
| return new ListAppender(name, filter, layout, entryPerNewLine, raw); |
| } |
| } |
| ---- |
| |
| The only difference in annotations is using `@PluginBuilderAttribute` |
| instead of `@PluginAttribute` so that default values and reflection can |
| be used instead of specifying them in the annotation. Either annotation |
| can be used in a builder, but the former is better suited for field |
| injection while the latter is better suited for parameter injection. |
| Otherwise, the same annotations (`@PluginConfiguration`, |
| `@PluginElement`, `@PluginNode`, and `@PluginValue`) are all supported |
| on fields. Note that a factory method is still required to supply a |
| builder, and this factory method should be annotated with |
| `@PluginBuilderFactory`. |
| // TODO: this will change with LOG4J2-1188 |
| |
| When plugins are being constructed after a configuration has been |
| parsed, a plugin builder will be used if available, otherwise a plugin |
| factory method will be used as a fallback. If a plugin contains neither |
| factory, then it cannot be used from a configuration file (it can still |
| be used programmatically of course). |
| |
| Here is an example of using a plugin factory versus a plugin builder |
| programmatically: |
| |
| [source,java] |
| ---- |
| ListAppender list1 = ListAppender.createAppender("List1", true, false, null, null); |
| ListAppender list2 = ListAppender.newBuilder().setName("List1").setEntryPerNewLine(true).build(); |
| ---- |
| |
| [#Custom_ContextDataInjector] |
| == Custom ContextDataInjector |
| |
| The `ContextDataInjector` (introduced in Log4j 2.7) is responsible for |
| populating the LogEvent's |
| link:../log4j-core/apidocs/org/apache/logging/log4j/core/LogEvent.html#getContextData()[context |
| data] with key-value pairs or replacing it completely. The default |
| implementation is `ThreadContextDataInjector`, which obtains context |
| attributes from the ThreadContext. |
| |
| Applications may replace the default `ContextDataInjector` by setting the |
| value of the system property `log4j2.contextDataInjector` to the name of |
| the custom `ContextDataInjector` class. |
| |
| Implementors should be aware there are some subtleties related to |
| thread-safety and implementing a context data injector in a garbage-free |
| manner. See the |
| link:../log4j-core/apidocs/org/apache/logging/log4j/core/ContextDataInjector.html[`ContextDataInjector`] |
| javadoc for detail. |
| |
| == Custom ThreadContextMap implementations |
| |
| A garbage-free `StringMap`-based context map can be installed by setting |
| system property `log4j2.garbagefreeThreadContextMap` to true. (Log4j |
| must be link:garbagefree.html#Config[enabled] to use ThreadLocals.) |
| |
| Any custom `ThreadContextMap` implementation can be installed by setting |
| system property `log4j2.threadContextMap` to the fully qualified class |
| name of the class implementing the `ThreadContextMap` interface. By also |
| implementing the `ReadOnlyThreadContextMap` interface, your custom |
| `ThreadContextMap` implementation will be accessible to applications via the |
| link:../log4j-api/apidocs/org/apache/logging/log4j/ThreadContext.html#getThreadContextMap()[`ThreadContext::getThreadContextMap`] |
| method. |
| |
| [#Custom_Plugins] |
| == Custom Plugins |
| |
| // TODO |
| See the link:plugins.html[Plugins] section of the manual. |