3

I'm working with a third-party log4j v2 Appender which gives me the opportunity to plug-in some custom classes of my own (the Appender will create those via Reflection).

In other words: My class gets instantiated from the Appender which again gets instantiated as part of log4j's normal bootstrap. I do not have control over the Appender which is from another library.

I would like a few properties for my own custom class to be read from the Log4j configuration. It makes sense that it lives there since it is indeed related to logging.

However, I cannot figure out how to do this. At the moment I do something like this from the constructor of my custom class:

LoggerContext loggerContext = LoggerContext.getContext(true);
if (loggerContext != null) {
    Configuration config = loggerContext.getConfiguration();
    if (config != null) {
        StrSubstitutor strSubstitutor = config.getStrSubstitutor();
        if (strSubstitutor != null) {
            StrLookup variableResolver = strSubstitutor.getVariableResolver();
            if (variableResolver != null) {
                String myVar = variableResolver.lookup("myPropertyName");
            }
        }
    }
}

but the problem seems to be that at this point in time (during bootstrap) the Log4j configuration is indeed not yet initialized so

LoggerContext.getContext(true).getConfiguration()

returns an instance of DefaultConfiguration (which obviously doesn't have my property) where I would have expected an instance of XmlConfiguration. The latter I get when I call LoggerContext.getContext(true).getConfiguration() at any later point in time and I can indeed read my custom properties from that one.

peterh
  • 18,404
  • 12
  • 87
  • 115
  • Do you have some knowledge on how your classes are instantiated? Is it a simple `Class.forName` or maybe it does some property injection using [plugin attribute annotations](https://logging.apache.org/log4j/2.x/log4j-core/apidocs/org/apache/logging/log4j/core/config/plugins/package-summary.html) or similar? – Piotr P. Karwasz Aug 23 '21 at 09:54
  • @PiotrP.Karwasz. My class is initiated via simple `Class.forName`. Although it is a third-party Appender, I do in fact have visibility into the [source code of the Appender](https://github.com/splunk/splunk-library-javalogging/blob/main/src/main/java/com/splunk/logging/HttpEventCollectorLog4jAppender.java#L98). – peterh Aug 23 '21 at 14:25
  • It may give a good start. https://stackoverflow.com/questions/1140358/how-to-initialize-log4j-properly – Nadeem Taj Aug 28 '21 at 11:24

2 Answers2

4

As you guessed, and as stated in the Log4j architecture documentation when they describe LoggerContext Configuration:

Every LoggerContext has an active Configuration. The Configuration contains all the Appenders, context-wide Filters, LoggerConfigs and contains the reference to the StrSubstitutor. During reconfiguration two Configuration objects will exist. Once all Loggers have been redirected to the new Configuration, the old Configuration will be stopped and discarded.

This reconfiguration process is performed in the LoggerContext start method, which in turn calls reconfigure, and finally setConfiguration.

Pay attention to the following line in setConfiguration:

firePropertyChangeEvent(new PropertyChangeEvent(this, PROPERTY_CONFIG, prev, config));

As you can see, LoggerContext will propagate the change in the configuration to the configured java.beans.PropertyChangeListeners.

This approach is followed in the own library in the Log4jBridgeHandler class. In its init method you can see:

if (propagateLevels) {
    @SuppressWarnings("resource")    // no need to close the AutoCloseable ctx here
    LoggerContext context = LoggerContext.getContext(false);
    context.addPropertyChangeListener(this);
    propagateLogLevels(context.getConfiguration());
    // note: java.util.logging.LogManager.addPropertyChangeListener() could also
    // be set here, but a call of JUL.readConfiguration() will be done on purpose
}

And they define the appropriate propertyChange event handler:

@Override
// impl. for PropertyChangeListener
public void propertyChange(PropertyChangeEvent evt) {
    SLOGGER.debug("Log4jBridgeHandler.propertyChange(): {}", evt);
    if (LoggerContext.PROPERTY_CONFIG.equals(evt.getPropertyName())  &&  evt.getNewValue() instanceof Configuration) {
        propagateLogLevels((Configuration) evt.getNewValue());
    }
}

I think you can follow a similar approach in your component. Please:

  1. Make it implement java.beans.PropertyChangeListener.
  2. In the corresponding propertyChange event handler method, reconfigure your component as required:
@Override
public void propertyChange(PropertyChangeEvent evt) {
    if (LoggerContext.PROPERTY_CONFIG.equals(evt.getPropertyName())  &&  evt.getNewValue() instanceof Configuration) {
        Configuration config = (Configuration) evt.getNewValue();
        if (config != null) {
            StrSubstitutor strSubstitutor = config.getStrSubstitutor();
            if (strSubstitutor != null) {
                StrLookup variableResolver = strSubstitutor.getVariableResolver();
                if (variableResolver != null) {
                    String myVar = variableResolver.lookup("myPropertyName");
                }
            }
        }
    }
}
  1. In its constructor or initialization code, register your component as a listener for the reconfiguration event:
LoggerContext context = LoggerContext.getContext(false);
context.addPropertyChangeListener(this);
jccampanero
  • 50,989
  • 3
  • 20
  • 49
  • Thanks. Yes, so Log4j fires a `propertyChange` event when it *finishes* the configuration. I'm guessing your proposal can work but it means that my class' config properties (those I wish to read from Log4j config) cannot be final. In other words: my class cannot be said to be fully initialized until it has received the event. Let me work on that. – peterh Aug 24 '21 at 06:23
  • Sorry for the late reply. You are welcome @peterh. Yes, you are right: a you can see in the library source code, the property change event will be raised when it finishes the configuration. As a consequence, you should consider your component configuration transient until your property change handler is invoked. – jccampanero Aug 24 '21 at 12:47
1

Since you do have access to the third-party appender, I would modify it to take advantage of Log4j2 injection capabilities. For example you can:

  1. Inject a Node into the appender's @PluginFactory:

    @PluginFactory
    public static HttpEventCollectorLog4jAppender createAppender(
          ...
          @PluginAttribute("middleware") final String middlewareClassName,
          @PluginAttribute("eventBodySerializer") final String eventBodySerializerClassName,
          @PluginAttribute("eventHeaderSerializer") final String eventHeaderSerializerClassName,
          ...
          @PluginNode Node node)
    
  2. Use the Node to retrieve plugins of the correct type:

       HttpSenderMiddleware middleware = null;
       EventBodySerializer eventBodySerializer = null;
       EventHeaderSerializer eventHeaderSerializer = null;
       for (final Node child : node.getChildren()) {
          final PluginType< ? > pluginType = child.getType();
          final Object component = child.getObject();
          switch (pluginType.getElementName()) {
             case "middleware" :
                middleware = (HttpSenderMiddleware) component;
                break;
             case "eventBodySerializer" :
                eventBodySerializer = (EventBodySerializer) component;
                break;
             case "eventHeaderSerializer" :
                eventHeaderSerializer = (EventHeaderSerializer) component;
          }
       }
       if (middlewareClass != null && !middlewareClass.isEmpty()) {
          try {
             middleware = (HttpEventCollectorMiddleware.HttpSenderMiddleware) (Class.forName(middlewareClass).newInstance());
          } catch (Exception ignored) {
          }
       }
      ...
    
  3. Adapt the HttpEventCollectorLog4jAppender's constructor to take class instances instead of class names as parameters.

  4. Annotate your classes as Log4j plugins:

   @Plugin(name = "MyMiddleware", elementType = "middleware", category = "Core")
   public class Middleware extends HttpSenderMiddleware {

       @PluginFactory
       public static Middleware createMiddleware(
           @PluginAttribute("one") String one,
           @PluginAttribute("two") String two
       ) {
       ...
       }
  1. Use them in your configuration:
<SplunkHttp>
    <MyMiddleware one="1" two="2" />
</SplunkHttp>
Piotr P. Karwasz
  • 12,857
  • 3
  • 20
  • 43
  • I said I had "visibility" into the Appender, nothing more. This solution presented here is not a short-term solution as it means sending a PR to that third-party project and hoping they'll accept it. Nevertheless I see it as the most correct solution ....if only I had control over that source. – peterh Aug 25 '21 at 05:49
  • 1
    Since the library is under an Apache licence it is probably better to use a fork of the library until the PR is accepted. I [forked the project](https://github.com/ppkarwasz/splunk-library-javalogging) and committed the suggestions in this answer. When I'll have time to write a couple of unit test, I'll send a PR. – Piotr P. Karwasz Aug 25 '21 at 07:20
  • 1
    I [submitted a PR](https://github.com/splunk/splunk-library-javalogging/pull/196). – Piotr P. Karwasz Aug 27 '21 at 07:29
  • 1
    Thanks. Still believe this is formally the correct approach but short-term I'll have to go with [jccampanero's solution](https://stackoverflow.com/a/68899863/1504556). Your proposal deserves all the upvotes it can get. – peterh Aug 28 '21 at 11:32