From b577ebff5a739f8da7a5f6cba9231755d817ff29 Mon Sep 17 00:00:00 2001 From: Philipp Knobel Date: Fri, 29 Jan 2016 13:16:20 +0100 Subject: [PATCH] LOGS-494: allow composite configurations --- .../core/config/AbstractConfiguration.java | 2 +- .../core/config/CompositeConfiguration.java | 113 ++++++++++++++ .../log4j/core/config/Configurator.java | 45 +++++- .../log4j/core/impl/Log4jContextFactory.java | 52 +++++++ .../config/CompositeConfigurationTest.java | 141 ++++++++++++++++++ .../test/resources/log4j-comp-appender.json | 34 +++++ .../test/resources/log4j-comp-appender.xml | 39 +++++ .../src/test/resources/log4j-comp-filter.json | 9 ++ .../src/test/resources/log4j-comp-filter.xml | 34 +++++ .../src/test/resources/log4j-comp-logger.json | 36 +++++ .../src/test/resources/log4j-comp-logger.xml | 41 +++++ .../test/resources/log4j-comp-properties.json | 16 ++ .../test/resources/log4j-comp-properties.xml | 24 +++ 13 files changed, 583 insertions(+), 3 deletions(-) create mode 100644 log4j-core/src/main/java/org/apache/logging/log4j/core/config/CompositeConfiguration.java create mode 100644 log4j-core/src/test/java/org/apache/logging/log4j/core/config/CompositeConfigurationTest.java create mode 100644 log4j-core/src/test/resources/log4j-comp-appender.json create mode 100644 log4j-core/src/test/resources/log4j-comp-appender.xml create mode 100644 log4j-core/src/test/resources/log4j-comp-filter.json create mode 100644 log4j-core/src/test/resources/log4j-comp-filter.xml create mode 100644 log4j-core/src/test/resources/log4j-comp-logger.json create mode 100644 log4j-core/src/test/resources/log4j-comp-logger.xml create mode 100644 log4j-core/src/test/resources/log4j-comp-properties.json create mode 100644 log4j-core/src/test/resources/log4j-comp-properties.xml diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java index 00cd3978eda..6d036636e82 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java @@ -113,7 +113,7 @@ public abstract class AbstractConfiguration extends AbstractFilterable implement private LoggerConfig root = new LoggerConfig(); private final ConcurrentMap componentMap = new ConcurrentHashMap<>(); private final ConfigurationSource configurationSource; - private ScriptManager scriptManager; + protected ScriptManager scriptManager; private final ConfigurationScheduler configurationScheduler = new ConfigurationScheduler(); private final WatchManager watchManager = new WatchManager(configurationScheduler); private AsyncLoggerConfigDisruptor asyncLoggerConfigDisruptor; diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/CompositeConfiguration.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/CompositeConfiguration.java new file mode 100644 index 00000000000..a00dbe4ca18 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/CompositeConfiguration.java @@ -0,0 +1,113 @@ +package org.apache.logging.log4j.core.config; + +import org.apache.logging.log4j.core.Filter; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +public class CompositeConfiguration + extends AbstractConfiguration +{ + + private List configurations; + + private static final List names = new ArrayList<>(); + + private static final String APPENDERS = "appenders"; + + private static final String PROPERTIES = "properties"; + + private static final String LOGGERS = "loggers"; + + + static + { + names.add( APPENDERS ); + names.add( PROPERTIES ); + names.add( LOGGERS ); + } + + /** + * Constructor. + */ + public CompositeConfiguration( List configurations ) + { + super( ConfigurationSource.NULL_SOURCE ); + this.configurations = configurations; + } + + @Override + protected void setup() + { + AbstractConfiguration primaryConfiguration = configurations.get( 0 ); + staffChildConfiguration( primaryConfiguration ); + rootNode = primaryConfiguration.rootNode; + for ( AbstractConfiguration amendingConfiguration : configurations.subList( 1, configurations.size() ) ) + { + staffChildConfiguration( amendingConfiguration ); + Node currentRoot = amendingConfiguration.rootNode; + for ( Node childNode : currentRoot.getChildren() ) + { + mergeNodes( rootNode, childNode ); + } + } + } + + private void staffChildConfiguration( AbstractConfiguration childConfiguration ) + { + childConfiguration.pluginManager = pluginManager; + childConfiguration.scriptManager = scriptManager; + childConfiguration.setup(); + } + + private void mergeNodes( Node rootNode, Node childNode ) + { + // first find the right rootNode child we will merge with + boolean isFilter = Filter.class.isAssignableFrom(childNode.getType().getPluginClass()); + for ( Node rootChildNode : rootNode.getChildren() ) + { + if (isFilter && Filter.class.isAssignableFrom(rootChildNode.getType().getPluginClass())) { + //for filters we have a simple replace, as only one filter can exist + rootNode.getChildren().remove(rootChildNode); + rootNode.getChildren().add(childNode); + return; + } + if ( rootChildNode.getType() != childNode.getType() + || !names.contains( rootChildNode.getName().toLowerCase() ) ) + { + continue; + } + + List grandChilds = rootChildNode.getChildren(); + + switch ( rootChildNode.getName().toLowerCase() ) + { + case LOGGERS: /** fallthrough */ + case PROPERTIES: + grandChilds.addAll( childNode.getChildren() ); + break; + case APPENDERS: + for ( Node appender : childNode.getChildren() ) + { + Iterator it = grandChilds.iterator(); + while ( it.hasNext() ) + { + Node currentAppender = it.next(); + // check if there's already an appender with the very same name. If so remove it + if ( Objects.equals( currentAppender.getAttributes().get( "name" ), + appender.getAttributes().get( "name" ) ) ) + { + it.remove(); + break; + } + } + // add appender (might actually be replace with one with the same name was previously found + grandChilds.add( appender ); + } + break; + } + } + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configurator.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configurator.java index fe6a5d5b246..be669b07f07 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configurator.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configurator.java @@ -17,6 +17,8 @@ package org.apache.logging.log4j.core.config; import java.net.URI; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import org.apache.logging.log4j.Level; @@ -109,8 +111,30 @@ public static LoggerContext initialize(final String name, final ClassLoader load */ public static LoggerContext initialize(final String name, final ClassLoader loader, final String configLocation, final Object externalContext) { - final URI uri = Strings.isBlank(configLocation) ? null : NetUtils.toURI(configLocation); - return initialize(name, loader, uri, externalContext); + if ( Strings.isBlank( configLocation ) ) + { + return initialize( name, loader, (URI) null, externalContext ); + } + if ( configLocation.contains( ";" ) ) + { + String[] parts = configLocation.split( ";" ); + String scheme = null; + List uris = new ArrayList<>( parts.length ); + for ( String part : parts ) + { + URI uri = NetUtils.toURI( scheme != null ? scheme + ":" + part : part ); + if ( scheme == null && uri.getScheme() != null ) + { + scheme = uri.getScheme(); + } + uris.add( uri ); + } + return initialize( name, loader, uris, externalContext ); + } + else + { + return initialize( name, loader, NetUtils.toURI( configLocation ), externalContext ); + } } /** @@ -146,6 +170,23 @@ public static LoggerContext initialize(final String name, final ClassLoader load return null; } + public static LoggerContext initialize( final String name, final ClassLoader loader, + final List configLocations, final Object externalContext ) + { + try + { + final Log4jContextFactory factory = getFactory(); + return factory == null ? null + : factory.getContext( FQCN, loader, externalContext, false, configLocations, name ); + } + catch ( final Exception ex ) + { + LOGGER.error( "There was a problem initializing the LoggerContext [{}] using configurations at [{}].", name, + configLocations, ex ); + } + return null; + } + /** * Initializes the Logging Context. * @param name The Context name. diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jContextFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jContextFactory.java index c65a64fe4e5..ea3808913bc 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jContextFactory.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/Log4jContextFactory.java @@ -17,13 +17,17 @@ package org.apache.logging.log4j.core.impl; import java.net.URI; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import org.apache.logging.log4j.core.LifeCycle; import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.AbstractConfiguration; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationFactory; import org.apache.logging.log4j.core.config.ConfigurationSource; +import org.apache.logging.log4j.core.config.CompositeConfiguration; import org.apache.logging.log4j.core.selector.ClassLoaderContextSelector; import org.apache.logging.log4j.core.selector.ContextSelector; import org.apache.logging.log4j.core.util.Cancellable; @@ -241,6 +245,54 @@ public LoggerContext getContext(final String fqcn, final ClassLoader loader, fin return ctx; } + public LoggerContext getContext( final String fqcn, final ClassLoader loader, final Object externalContext, + final boolean currentContext, final List configLocations, final String name ) + { + final LoggerContext ctx = + selector.getContext( fqcn, loader, currentContext, null/*this probably needs to change*/ ); + if ( externalContext != null && ctx.getExternalContext() == null ) + { + ctx.setExternalContext( externalContext ); + } + if ( name != null ) + { + ctx.setName( name ); + } + if ( ctx.getState() == LifeCycle.State.INITIALIZED ) + { + if ( ( configLocations != null && !configLocations.isEmpty() ) ) + { + ContextAnchor.THREAD_CONTEXT.set( ctx ); + List configurations = new ArrayList<>( configLocations.size() ); + for ( URI configLocation : configLocations ) + { + Configuration currentReadConfiguration = + ConfigurationFactory.getInstance().getConfiguration( name, configLocation ); + if ( currentReadConfiguration instanceof AbstractConfiguration ) + { + configurations.add( (AbstractConfiguration) currentReadConfiguration ); + } + else + { + LOGGER.error( + "Found configuration {}, which is no AbstractConfiguration and can't be handled by CompositeConfiguration", + configLocation ); + } + } + CompositeConfiguration compositeConfiguration = new CompositeConfiguration( configurations ); + LOGGER.debug( "Starting LoggerContext[name={}] from configurations at {}", ctx.getName(), + configLocations ); + ctx.start( compositeConfiguration ); + ContextAnchor.THREAD_CONTEXT.remove(); + } + else + { + ctx.start(); + } + } + return ctx; + } + /** * Returns the ContextSelector. * @return The ContextSelector. diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/config/CompositeConfigurationTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/CompositeConfigurationTest.java new file mode 100644 index 00000000000..f4f1ebb805c --- /dev/null +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/CompositeConfigurationTest.java @@ -0,0 +1,141 @@ +package org.apache.logging.log4j.core.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.appender.ConsoleAppender; +import org.apache.logging.log4j.core.appender.FileAppender; +import org.apache.logging.log4j.core.appender.RollingFileAppender; +import org.apache.logging.log4j.core.filter.ThresholdFilter; +import org.apache.logging.log4j.junit.LoggerContextRule; +import org.junit.Test; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public class CompositeConfigurationTest +{ + + @Test + public void compositeConfigurationUsed() + { + final LoggerContextRule lcr = + new LoggerContextRule( "classpath:log4j-comp-appender.xml;log4j-comp-appender.json" ); + Statement test = new Statement() + { + @Override + public void evaluate() + throws Throwable + { + assertTrue( lcr.getConfiguration() instanceof CompositeConfiguration ); + } + }; + runTest( lcr, test ); + } + + @Test + public void compositeProperties() + { + final LoggerContextRule lcr = + new LoggerContextRule( "classpath:log4j-comp-properties.xml;log4j-comp-properties.json" ); + Statement test = new Statement() + { + @Override + public void evaluate() + throws Throwable + { + CompositeConfiguration config = (CompositeConfiguration) lcr.getConfiguration(); + assertEquals( "json", config.getStrSubstitutor().replace( "${propertyShared}" ) ); + assertEquals( "xml", config.getStrSubstitutor().replace( "${propertyXml}" ) ); + assertEquals( "json", config.getStrSubstitutor().replace( "${propertyJson}" ) ); + } + }; + runTest( lcr, test ); + } + + @Test + public void compositeAppenders() + { + final LoggerContextRule lcr = + new LoggerContextRule( "classpath:log4j-comp-appender.xml;log4j-comp-appender.json" ); + Statement test = new Statement() + { + @Override + public void evaluate() + throws Throwable + { + CompositeConfiguration config = (CompositeConfiguration) lcr.getConfiguration(); + Map appender = config.getAppenders(); + assertEquals( 3, appender.size() ); + assertTrue( appender.get( "STDOUT" ) instanceof ConsoleAppender ); + assertTrue( appender.get( "File" ) instanceof FileAppender ); + assertTrue( appender.get( "Override" ) instanceof RollingFileAppender ); + } + }; + runTest( lcr, test ); + } + + @Test + public void compositeLogger() + { + final LoggerContextRule lcr = new LoggerContextRule( "classpath:log4j-comp-logger.xml;log4j-comp-logger.json" ); + Statement test = new Statement() + { + @Override + public void evaluate() + throws Throwable + { + CompositeConfiguration config = (CompositeConfiguration) lcr.getConfiguration(); + Map appendersMap = config.getLogger( "cat1" ).getAppenders(); + assertEquals( 1, appendersMap.size() ); + assertTrue( appendersMap.get( "STDOUT" ) instanceof ConsoleAppender ); + + appendersMap = config.getLogger( "cat2" ).getAppenders(); + assertEquals( 1, appendersMap.size() ); + assertTrue( appendersMap.get( "File" ) instanceof FileAppender ); + + appendersMap = config.getLogger( "cat3" ).getAppenders(); + assertEquals( 1, appendersMap.size() ); + assertTrue( appendersMap.get( "File" ) instanceof FileAppender ); + + appendersMap = config.getRootLogger().getAppenders(); + assertEquals( 2, appendersMap.size() ); + assertTrue( appendersMap.get( "File" ) instanceof FileAppender ); + assertTrue( appendersMap.get( "STDOUT" ) instanceof ConsoleAppender ); + } + }; + runTest( lcr, test ); + } + + @Test + public void overrideFilter() + { + final LoggerContextRule lcr = new LoggerContextRule( "classpath:log4j-comp-filter.xml;log4j-comp-filter.json" ); + Statement test = new Statement() + { + @Override + public void evaluate() + throws Throwable + { + CompositeConfiguration config = (CompositeConfiguration) lcr.getConfiguration(); + assertTrue( config.getFilter() instanceof ThresholdFilter ); + } + }; + runTest( lcr, test ); + } + + private void runTest( LoggerContextRule rule, Statement statement ) + { + try + { + rule.apply( statement, Description.createTestDescription( getClass(), + Thread.currentThread().getStackTrace()[1].getMethodName() ) ).evaluate(); + } + catch ( Throwable throwable ) + { + throw new RuntimeException( throwable ); + } + } +} diff --git a/log4j-core/src/test/resources/log4j-comp-appender.json b/log4j-core/src/test/resources/log4j-comp-appender.json new file mode 100644 index 00000000000..1f0b02d2c72 --- /dev/null +++ b/log4j-core/src/test/resources/log4j-comp-appender.json @@ -0,0 +1,34 @@ +{ + "Configuration" : { + "status": "warn", + "name": "YAMLConfigTest", + "appenders": { + "Console": { + "name": "STDOUT", + "PatternLayout": { + "Pattern": "%m%n" + } + }, + "RollingFile": { + "name": "Override", + "fileName": "target/log4j-appender-override.log", + "filePattern": "target/log4j-appender-override-$${date:yyyy-MM}.log", + "PatternLayout": { + "Pattern": "%m%n" + }, + "SizeBasedTriggeringPolicy": { + "size": "500" + } + } + }, + "Loggers" : { + "Root" : { + "level" : "error", + "AppenderRef" : { + "ref" : "STDOUT" + } + + } + } + } +} diff --git a/log4j-core/src/test/resources/log4j-comp-appender.xml b/log4j-core/src/test/resources/log4j-comp-appender.xml new file mode 100644 index 00000000000..450f3b4b0d0 --- /dev/null +++ b/log4j-core/src/test/resources/log4j-comp-appender.xml @@ -0,0 +1,39 @@ + + + + + + + %d %p %C{1.} [%t] %m%n + + + + + %d %p %C{1.} [%t] %m%n + + + + + + + + + + + diff --git a/log4j-core/src/test/resources/log4j-comp-filter.json b/log4j-core/src/test/resources/log4j-comp-filter.json new file mode 100644 index 00000000000..c0102986255 --- /dev/null +++ b/log4j-core/src/test/resources/log4j-comp-filter.json @@ -0,0 +1,9 @@ +{ + "Configuration" : { + "status": "warn", + "name": "YAMLConfigTest", + "thresholdFilter" : { + "level": "debug" + } + } +} diff --git a/log4j-core/src/test/resources/log4j-comp-filter.xml b/log4j-core/src/test/resources/log4j-comp-filter.xml new file mode 100644 index 00000000000..553d44864da --- /dev/null +++ b/log4j-core/src/test/resources/log4j-comp-filter.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + diff --git a/log4j-core/src/test/resources/log4j-comp-logger.json b/log4j-core/src/test/resources/log4j-comp-logger.json new file mode 100644 index 00000000000..6d2689b5574 --- /dev/null +++ b/log4j-core/src/test/resources/log4j-comp-logger.json @@ -0,0 +1,36 @@ +{ + "Configuration" : { + "status": "warn", + "name": "YAMLConfigTest", + "Loggers" : { + "logger" : [ + { + "name" : "cat1", + "level" : "debug", + "additivity" : false, + "AppenderRef" : { + "ref" : "STDOUT" + } + }, + { + "name" : "cat2", + "level" : "debug", + "additivity" : false, + "AppenderRef" : { + "ref" : "File" + } + + } + ], + "Root" : { + "level" : "error", + "AppenderRef" : [{ + "ref" : "STDOUT" + }, + { + "ref" : "File" + }] + } + } + } +} diff --git a/log4j-core/src/test/resources/log4j-comp-logger.xml b/log4j-core/src/test/resources/log4j-comp-logger.xml new file mode 100644 index 00000000000..f1ed90cf294 --- /dev/null +++ b/log4j-core/src/test/resources/log4j-comp-logger.xml @@ -0,0 +1,41 @@ + + + + + + + + + + %d %p %C{1.} [%t] %m%n + + + + + + + + + + + + + + + diff --git a/log4j-core/src/test/resources/log4j-comp-properties.json b/log4j-core/src/test/resources/log4j-comp-properties.json new file mode 100644 index 00000000000..83c2d090d06 --- /dev/null +++ b/log4j-core/src/test/resources/log4j-comp-properties.json @@ -0,0 +1,16 @@ +{ + "Configuration" : { + "status": "warn", + "name": "YAMLConfigTest", + + "properties" : { + "property" : [{ + "name" : "propertyShared", + "value": "json" + },{ + "name" : "propertyJson", + "value": "json" + }] + } + } +} diff --git a/log4j-core/src/test/resources/log4j-comp-properties.xml b/log4j-core/src/test/resources/log4j-comp-properties.xml new file mode 100644 index 00000000000..ff127d0fde3 --- /dev/null +++ b/log4j-core/src/test/resources/log4j-comp-properties.xml @@ -0,0 +1,24 @@ + + + + + xml + xml + +