Improve LogCtlLog4j2 use of log4j2 internals
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtl.java b/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtl.java
index b0c67b0..bba604c 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtl.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtl.java
@@ -331,7 +331,7 @@
}
// Nothing found - built-in default.
logLogging("Log4j2: built-in default");
- LogCtlLog4j2.resetLogging(LogCtlLog4j2.log4j2setup);
+ LogCtlLog4j2.reconfigureLog4j2fromString(LogCtlLog4j2.log4j2setup, LogCtlLog4j2.SyntaxHint.PROPERTIES);
} else {
if ( isSetLog4j2property(log4j2ConfigFilePropertyLegacy) )
logLogging("Already set: %s=%s", log4j2ConfigFilePropertyLegacy, System.getProperty(log4j2ConfigFilePropertyLegacy));
diff --git a/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtlLog4j2.java b/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtlLog4j2.java
index 9173972..fc976bc 100644
--- a/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtlLog4j2.java
+++ b/jena-base/src/main/java/org/apache/jena/atlas/logging/LogCtlLog4j2.java
@@ -21,48 +21,68 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Objects;
-import org.apache.jena.atlas.io.IO;
-import org.apache.jena.atlas.lib.StrUtils;
+import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.Logger;
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.Configurator;
+import org.apache.logging.log4j.core.config.json.JsonConfigurationFactory;
import org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory;
+import org.apache.logging.log4j.core.config.xml.XmlConfigurationFactory;
+import org.apache.logging.log4j.core.config.yaml.YamlConfigurationFactory;
/**
* Additional logging control, for Log4j2 as used by jena-cmds.
- * <br/>
- * This class pulls in log4j2.
- * <br/>
- * This class is split out from {@link LogCtl} to decouple the class loading dependencies.
+ * <p>
+ * This class depends on log4j2-api and also log4j2-core.
+ * These are <optional> dependencies for Jena which can use any slf4j provider.
+ * <p>
+ * This class is split out from {@link LogCtl} to decouple the dependencies.
+ * <p>
+ * This class is not used if log4j2 is not used.
*/
public class LogCtlLog4j2 {
+
+ /** Default log4j2 setup */
+ public static String log4j2setup = String.join(log4jSetupSep(),
+ log4j2setupBase(),
+ log4j2setupJenaLib(),
+ log4j2setupFuseki());
+
/**
- * Reset logging (log4j2). log4j2.properties format.
+ * Reset logging for log4j2.
+ * The string is log4j2.properties format.
*/
public static void resetLogging(String configString) {
- // Dispatch name to syntax.
- try (InputStream inputStream = new ByteArrayInputStream(StrUtils.asUTF8bytes(configString))) {
- resetLogging(inputStream, ".properties");
- } catch (IOException ex) {
- IO.exception(ex);
- }
+ // This method is previous naming.
+ reconfigureLog4j2fromString(configString, SyntaxHint.PROPERTIES);
}
- public static void resetLogging(InputStream inputStream, String syntaxHint) throws IOException {
- ConfigurationSource source = new ConfigurationSource(inputStream);
- ConfigurationFactory factory = ( syntaxHint.endsWith(".properties") )
- ? new PropertiesConfigurationFactory()
- : ConfigurationFactory.getInstance();
- Configuration configuration = factory.getConfiguration(null, source);
- LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
- // This changes existing loggers.
- ctx.setConfiguration(configuration);
+ /**
+ * Reset logging for log4j2 from a string.
+ * The resourceName is used to determine the syntax.
+ */
+ public static void resetLogging(InputStream inputStream, String resourceName) {
+ resetLogging(inputStream, determineSyntax(resourceName));
}
+ /**
+ * Reset logging for log4j2 from an {@link InputStream} with the given syntax.
+ */
+ public static void resetLogging(InputStream inputStream, SyntaxHint syntaxHint) {
+ Configuration config = log4j2Configuration(inputStream, syntaxHint);
+ reconfigureLog4j(config);
+ }
+
+ /** Check logging level of a Logger */
/*package*/ static void setLoggerlevel(String logger, Level level) {
try {
if ( !logger.equals("") )
@@ -70,47 +90,250 @@
else
org.apache.logging.log4j.core.config.Configurator.setRootLevel(level);
} catch (NoClassDefFoundError ex) {
- Log.warnOnce(LogCtl.class, "Log4j2 Configurator not found", LogCtl.class);
+ Log.warnOnce(LogCtlLog4j2.class, "Log4j2 Configurator not found", LogCtl.class);
}
}
- // basic setup.
- // @formatter:off
- /** A basic logging setup. */
- public static String log4j2setup = StrUtils.strjoinNL
- ( "## Command default log4j2 setup : log4j2 properties syntax."
- , "status = error"
- , "name = JenaLoggingDft"
-// , "filters = threshold"
-// , ""
-// , "filter.threshold.type = ThresholdFilter"
-// , "filter.threshold.level = ALL"
-// , ""
- , "appender.console.type = Console"
- , "appender.console.name = OUT"
- , "appender.console.target = SYSTEM_OUT"
- , "appender.console.layout.type = PatternLayout"
- , "appender.console.layout.pattern = %d{HH:mm:ss} %-5p %-15c{1} :: %m%n"
- , "#appender.console.layout.pattern = [%d{yyyy-MM-dd HH:mm:ss}] %-5p %-15c{1} :: %m%n"
+ /** Check logging level of a Logger */
+ /*package*/ static void setLoggerlevel(Logger logger, Level level) {
+ try {
+ org.apache.logging.log4j.core.config.Configurator.setLevel(logger, level);
+ } catch (NoClassDefFoundError ex) {
+ Log.warnOnce(LogCtlLog4j2.class, "Log4j2 Configurator not found", LogCtl.class);
+ }
+ }
- , "rootLogger.level = INFO"
- , "rootLogger.appenderRef.stdout.ref = OUT"
- , "logger.jena.name = org.apache.jena"
- , "logger.jena.level = INFO"
+ /**
+ * Enum for possible syntax of a Log4j configuration file.
+ * <p>
+ * Note that the JSON and YAML forms, require additional jars. See
+ * <a href="https://logging.apache.org/log4j/2.x/runtime-dependencies.html#log4j-core"
+ * >"dependencies for log4j-core"</a> for more information.
+ */
+ public enum SyntaxHint {
+ PROPERTIES("properties"),
+ XML("xml"),
+ JSON("json"),
+ YAML("yaml");
- , "logger.arq-exec.name = org.apache.jena.arq.exec"
- , "logger.arq-exec.level = INFO"
+ // The syntax name is assumed to be the file extension.
+ // This can be used as the name of a syntax.
+ private String syntaxName;
+ SyntaxHint(String syntaxName) { this.syntaxName = syntaxName; }
- , "logger.riot.name = org.apache.jena.riot"
- , "logger.riot.level = INFO"
+ /** Return the {@code SyntaxHint} for a name (case insensitive) or null */
+ static SyntaxHint fromName(String name) {
+ for ( SyntaxHint hint : SyntaxHint.values() ) {
+ if ( hint.syntaxName.equalsIgnoreCase(name) )
+ return hint;
+ }
+ return null;
+ }
+ }
- // If mixed with Fuseki code, and command logging happens, then ensure Jetty is WARN.
- , "logger.jetty.name = org.eclipse.jetty"
- , "logger.jetty.level = WARN"
- );
- // @formatter:on
+ /**
+ * Reconfigure log4j2 from a file.
+ * <p>
+ * The file syntax is determined by the file extension (".properties" or ".xml").
+ * <p>
+ * Existing loggers are reconfigured by this function.
+ */
+ public static void reconfigureLog4j2fromFile(String filename) {
+ if ( true ) {
+ // This particular case can be done with Log4J directly.
+ // That will extend to all plugins.
+ Configurator.initialize(null, filename);
+ return;
+ }
+ // Use the same logic as the other operations.
+ // JSON and YAML usage require addition jars on the classpath.
+ SyntaxHint syntax = determineSyntax(filename);
+ Configuration config = log4j2ConfigurationFromFile(filename, syntax);
+ reconfigureLog4j(config);
+ }
-// /** A format for commands using stderr. */
-// public static String log4j2setupCmd = log4j2setup.replace("SYSTEM_OUT", "SYSTEM_ERR");
+ /**
+ * Reconfigure log4j2 from a file.
+ * <p>
+ * The file syntax is determined by the syntax hint.
+ * <p>
+ * Existing loggers are reconfigured by this function.
+ */
+ public static void reconfigureLog4j2fromFile(String filename, SyntaxHint syntaxHint) {
+ Configuration config = log4j2ConfigurationFromFile(filename, syntaxHint);
+ reconfigureLog4j(config);
+ }
+
+ /**
+ * Reconfigure log4j2 from a string.
+ * <p>
+ * The syntax is given by the syntax hint.
+ * <p>
+ * Existing loggers are reconfigured by this function.
+ */
+ public static void reconfigureLog4j2fromString(String configString, SyntaxHint syntaxHint) {
+ Configuration config = log4j2ConfigurationFromString(configString, syntaxHint);
+ reconfigureLog4j(config);
+ }
+
+ /**
+ * Reconfigure log4j from a {@link Configuration}.
+ */
+ private static void reconfigureLog4j(Configuration config) {
+ config.initialize();
+ Configurator.reconfigure(config);
+ }
+
+ /**
+ * Create a log4j2 {@link Configuration} from a file.
+ * <p>
+ * The file syntax is determined by the syntax hint
+ */
+ private static Configuration log4j2ConfigurationFromFile(String filename, SyntaxHint syntaxHint) {
+ URI uri = Path.of(filename).toUri();
+ ConfigurationSource source = ConfigurationSource.fromUri(uri);
+ return createLog4jConfiguration(source, syntaxHint);
+ }
+
+ /**
+ * Create a log4j2 {@link Configuration} from a string.
+ * <p>
+ * TThe string syntax is determined by the syntax hint
+ */
+ private static Configuration log4j2ConfigurationFromString(String text, SyntaxHint syntaxHint) {
+ try(InputStream input = new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8))) {
+ return log4j2Configuration(input, syntaxHint);
+ } catch (IOException ex) { throw new UncheckedIOException(ex); }
+ }
+
+ /**
+ * Create a log4j2 {@link Configuration} from an {@link InputStream}.
+ * <p>
+ * The file syntax is determined by the syntax hint
+ */
+ private static Configuration log4j2Configuration(InputStream inputStream, SyntaxHint syntaxHint) {
+ try {
+ ConfigurationSource source = new ConfigurationSource(inputStream);
+ return createLog4jConfiguration(source, syntaxHint);
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+ /**
+ * Create a log4j2 {@link Configuration}.
+ * <p>
+ * @see org.apache.logging.log4j.core.config.Configurator
+ * @see org.apache.logging.log4j.core.config.ConfigurationSource
+ */
+ private static Configuration createLog4jConfiguration(ConfigurationSource source, SyntaxHint syntaxHint) {
+ Objects.requireNonNull(source);
+ Objects.requireNonNull(syntaxHint);
+ ConfigurationFactory factory = switch(syntaxHint) {
+ case PROPERTIES -> new PropertiesConfigurationFactory();
+ case XML -> new XmlConfigurationFactory();
+ case JSON -> new JsonConfigurationFactory();
+ case YAML -> new YamlConfigurationFactory();
+ default -> ConfigurationFactory.getInstance();
+ };
+ Configuration configuration = factory.getConfiguration(null, source);
+ if ( configuration == null )
+ throw new UnsupportedOperationException("Can't create a configuration for '"+source+"' using '"+syntaxHint+"'");
+ return configuration;
+ }
+
+ /**
+ * Filename to {@link SynatxHint}.
+ * <p>
+ * Identify the likely syntax of a file, or throw IllegalArgumentException
+ * if no such determination can be made.
+ */
+ private static SyntaxHint determineSyntax(String filename) {
+ String ext = FilenameUtils.getExtension(filename);
+ if ( ext == null )
+ throw new IllegalArgumentException("No file extension");
+ SyntaxHint hint = SyntaxHint.fromName(ext);
+ if ( hint == null )
+ throw new IllegalArgumentException("File extension not recognized: '"+ext+"'");
+ return hint;
+ }
+
+ /** Line separate/blank line for concatenating log4j syntax fragments. */
+ private static String log4jSetupSep() { return "\n"; }
+
+ /**
+ * A basic logging setup. Time and level INFO.
+ */
+ private static String log4j2setupBase() {
+ return """
+ ## Log4j2 properties syntax.
+ status = error
+ name = JenaLoggingDft
+
+ # filters = threshold
+ # filter.threshold.type = ThresholdFilter
+ # filter.threshold.level = ALL
+
+ appender.console.type = Console
+ appender.console.name = OUT
+ appender.console.target = SYSTEM_OUT
+ appender.console.layout.type = PatternLayout
+ appender.console.layout.pattern = %d{HH:mm:ss} %-5p %-15c{1} :: %m%n
+ # appender.console.layout.pattern = [%d{yyyy-MM-dd HH:mm:ss}] %-5p %-15c{1} :: %m%n
+
+ rootLogger.level = INFO
+ rootLogger.appenderRef.stdout.ref = OUT
+ """;
+ }
+ /** Default log4j fragment needed for Jena command line tools. */
+ private static String log4j2setupJenaLib() {
+ return """
+ logger.jena.name = org.apache.jena
+ logger.jena.level = INFO
+
+ logger.arq-exec.name = org.apache.jena.arq.exec
+ logger.arq-exec.level = INFO
+
+ logger.riot.name = org.apache.jena.riot
+ logger.riot.level = INFO
+ """;
+ }
+ /** Additional log4j fragment for Fuseki in case the general default is used with embedded Fuseki. */
+ private static String log4j2setupFuseki() {
+ return """
+ # Fuseki. In case this logging setup gets install for embedded Fuseki.
+
+ logger.fuseki.name = org.apache.jena.fuseki
+ logger.fuseki.level = INFO
+ logger.fuseki-fuseki.name = org.apache.jena.fuseki.Fuseki
+ logger.fuseki-fuseki.level = INFO
+
+ logger.fuseki-server.name = org.apache.jena.fuseki.Server
+ logger.fuseki-server.level = INFO
+
+ logger.fuseki-config.name = org.apache.jena.fuseki.Config
+ logger.fuseki-config.level = INFO
+
+ logger.fuseki-admin.name = org.apache.jena.fuseki.Admin
+ logger.fuseki-admin.level = INFO
+
+ logger.jetty.name = org.eclipse.jetty
+ logger.jetty.level = WARN
+
+ logger.shiro.name = org.apache.shiro
+ logger.shiro.level = WARN
+
+ # This goes out in NCSA format
+ appender.plain.type = Console
+ appender.plain.name = PLAIN
+ appender.plain.layout.type = PatternLayout
+ appender.plain.layout.pattern = %m%n
+
+ logger.fuseki-request.name = org.apache.jena.fuseki.Request
+ logger.fuseki-request.additivity = false
+ logger.fuseki-request.level = OFF
+ logger.fuseki-request.appenderRef.plain.ref = PLAIN
+ """;
+ }
}