blob: 73ec276dabf42525627968277ea2ad60cb43f450 [file] [log] [blame]
/*
* 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
*
* http://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.
*/
package org.apache.ignite.logger.log4j2;
import java.io.File;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.UUID;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.internal.util.tostring.GridToStringExclude;
import org.apache.ignite.internal.util.typedef.C1;
import org.apache.ignite.internal.util.typedef.internal.A;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteClosure;
import org.apache.ignite.logger.LoggerNodeIdAndApplicationAware;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.MarkerManager;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.LoggerContext;
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.appender.routing.RoutingAppender;
import org.apache.logging.log4j.core.config.AppenderControl;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.Configurator;
import org.apache.logging.log4j.core.config.DefaultConfiguration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.jetbrains.annotations.Nullable;
import static org.apache.ignite.IgniteSystemProperties.IGNITE_CONSOLE_APPENDER;
import static org.apache.ignite.IgniteSystemProperties.IGNITE_QUIET;
import static org.apache.logging.log4j.core.appender.ConsoleAppender.Target.SYSTEM_ERR;
import static org.apache.logging.log4j.core.appender.ConsoleAppender.Target.SYSTEM_OUT;
/**
* Log4j2-based implementation for logging. This logger should be used
* by loaders that have prefer <a target=_new href="http://logging.apache.org/log4j/2.x/index.html">log4j2</a>-based logging.
* <p>
* Here is a typical example of configuring log4j2 logger in Ignite configuration file:
* <pre name="code" class="xml">
* &lt;property name="gridLogger"&gt;
* &lt;bean class="org.apache.ignite.logger.log4j2.Log4J2Logger"&gt;
* &lt;constructor-arg type="java.lang.String" value="config/ignite-log4j.xml"/&gt;
* &lt;/bean>
* &lt;/property&gt;
* </pre>
* and from your code:
* <pre name="code" class="java">
* IgniteConfiguration cfg = new IgniteConfiguration();
* ...
* URL xml = U.resolveIgniteUrl("config/custom-log4j2.xml");
* IgniteLogger log = new Log4J2Logger(xml);
* ...
* cfg.setGridLogger(log);
* </pre>
*
* Please take a look at <a target=_new href="http://logging.apache.org/log4j/2.x/index.html">Apache Log4j 2</a>
* for additional information.
* <p>
* It's recommended to use Ignite logger injection instead of using/instantiating
* logger in your task/job code. See {@link org.apache.ignite.resources.LoggerResource} annotation about logger
* injection.
*/
public class Log4J2Logger implements IgniteLogger, LoggerNodeIdAndApplicationAware {
/** */
private static final String NODE_ID = "nodeId";
/** */
private static final String APP_ID = "appId";
/** */
private static final String CONSOLE_APPENDER = "autoConfiguredIgniteConsoleAppender";
/** */
private static volatile boolean inited;
/** */
private static volatile boolean quiet0;
/** */
private static final Object mux = new Object();
/** Logger implementation. */
@GridToStringExclude
@SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized")
private Logger impl;
/** Path to configuration file. */
@GridToStringExclude
private final String cfg;
/** Quiet flag. */
private final boolean quiet;
/** Node ID. */
@GridToStringExclude
private volatile UUID nodeId;
/**
* Creates new logger and automatically detects if root logger already
* has appenders configured. If it does not, the root logger will be
* configured with default appender, otherwise, existing appenders will be used.
*/
public Log4J2Logger() {
addConsoleAppenderIfNeeded(init -> LogManager.getRootLogger());
quiet = quiet0;
cfg = null;
}
/**
* Creates new logger with given implementation.
*
* @param impl Log4j2 implementation to use.
*/
private Log4J2Logger(final Logger impl, String path) {
assert impl != null;
addConsoleAppenderIfNeeded(new C1<Boolean, Logger>() {
@Override public Logger apply(Boolean init) {
return impl;
}
});
quiet = quiet0;
cfg = path;
}
/**
* Creates new logger with given configuration {@code path}.
*
* @param path Path to log4j2 configuration XML file.
* @throws IgniteCheckedException Thrown in case logger can't be created.
*/
public Log4J2Logger(String path) throws IgniteCheckedException {
if (path == null)
throw new IgniteCheckedException("Configuration XML file for Log4j2 must be specified.");
final URL cfgUrl = U.resolveIgniteUrl(path);
if (cfgUrl == null)
throw new IgniteCheckedException("Log4j2 configuration path was not found: " + path);
addConsoleAppenderIfNeeded(new C1<Boolean, Logger>() {
@Override public Logger apply(Boolean init) {
if (init)
Configurator.initialize(LoggerConfig.ROOT, cfgUrl.toString());
return LogManager.getRootLogger();
}
});
quiet = quiet0;
cfg = path;
}
/**
* Creates new logger with given configuration {@code cfgFile}.
*
* @param cfgFile Log4j configuration XML file.
* @throws IgniteCheckedException Thrown in case logger can't be created.
*/
public Log4J2Logger(File cfgFile) throws IgniteCheckedException {
if (cfgFile == null)
throw new IgniteCheckedException("Configuration XML file for Log4j must be specified.");
if (!cfgFile.exists() || cfgFile.isDirectory())
throw new IgniteCheckedException("Log4j2 configuration path was not found or is a directory: " + cfgFile);
final String path = cfgFile.getAbsolutePath();
addConsoleAppenderIfNeeded(new C1<Boolean, Logger>() {
@Override public Logger apply(Boolean init) {
if (init)
Configurator.initialize(LoggerConfig.ROOT, path);
return LogManager.getRootLogger();
}
});
quiet = quiet0;
cfg = cfgFile.getPath();
}
/**
* Creates new logger with given configuration {@code cfgUrl}.
*
* @param cfgUrl URL for Log4j configuration XML file.
* @throws IgniteCheckedException Thrown in case logger can't be created.
*/
public Log4J2Logger(final URL cfgUrl) throws IgniteCheckedException {
if (cfgUrl == null)
throw new IgniteCheckedException("Configuration XML file for Log4j must be specified.");
addConsoleAppenderIfNeeded(new C1<Boolean, Logger>() {
@Override public Logger apply(Boolean init) {
if (init)
Configurator.initialize(LoggerConfig.ROOT, cfgUrl.toString());
return LogManager.getRootLogger();
}
});
quiet = quiet0;
cfg = cfgUrl.getPath();
}
/**
* Cleans up the logger configuration. Should be used in unit tests only for sequential tests run with
* different configurations
*/
static void cleanup() {
synchronized (mux) {
System.clearProperty(APP_ID);
if (inited)
LogManager.shutdown();
inited = false;
}
}
/** {@inheritDoc} */
@Nullable @Override public String fileName() {
Configuration cfg = LoggerContext.getContext(false).getConfiguration();
for (LoggerConfig logCfg = cfg.getLoggerConfig(impl.getName()); logCfg != null; logCfg = logCfg.getParent()) {
for (Appender a : logCfg.getAppenders().values()) {
if (a instanceof FileAppender)
return ((FileAppender)a).getFileName();
if (a instanceof RollingFileAppender)
return ((RollingFileAppender)a).getFileName();
if (a instanceof RoutingAppender) {
RoutingAppender routing = (RoutingAppender)a;
Map<String, AppenderControl> appenders = routing.getAppenders();
for (AppenderControl control : appenders.values()) {
Appender innerApp = control.getAppender();
if (innerApp instanceof FileAppender)
return normalize(((FileAppender)innerApp).getFileName());
if (innerApp instanceof RollingFileAppender)
return normalize(((RollingFileAppender)innerApp).getFileName());
}
}
}
}
return null;
}
/**
* Normalizes given path for windows.
* Log4j2 doesn't replace unix directory delimiters which used at 'fileName' to windows.
*
* @param path Path.
* @return Normalized path.
*/
private String normalize(String path) {
if (!U.isWindows())
return path;
return path.replace('/', File.separatorChar);
}
/**
* Adds console appender when needed with some default logging settings.
*
* @param initLogClo Optional log implementation init closure.
*/
private void addConsoleAppenderIfNeeded(@Nullable IgniteClosure<Boolean, Logger> initLogClo) {
if (inited) {
// Do not init.
impl = initLogClo.apply(false);
return;
}
synchronized (mux) {
if (inited) {
// Do not init.
impl = initLogClo.apply(false);
return;
}
// Init logger impl.
impl = initLogClo.apply(true);
boolean quiet = Boolean.parseBoolean(System.getProperty(IGNITE_QUIET, "true"));
boolean consoleAppenderFound = isConsoleAppenderConfigured();
if (consoleAppenderFound && quiet)
quiet = false; // User configured console appender, but log is quiet.
if (!consoleAppenderFound && !quiet && Boolean.parseBoolean(System.getProperty(IGNITE_CONSOLE_APPENDER, "true"))) {
// User launched ignite in verbose mode and did not add console appender with INFO level
// to configuration and did not set IGNITE_CONSOLE_APPENDER to false.
configureConsoleAppender();
}
quiet0 = quiet;
inited = true;
}
}
/** */
private boolean isConsoleAppenderConfigured() {
Configuration cfg = LoggerContext.getContext(false).getConfiguration();
if (cfg instanceof DefaultConfiguration)
return false;
for (LoggerConfig logCfg = cfg.getLoggerConfig(impl.getName()); logCfg != null; logCfg = logCfg.getParent()) {
for (Appender appender : logCfg.getAppenders().values()) {
if (appender instanceof ConsoleAppender) {
if (((ConsoleAppender)appender).getTarget() == SYSTEM_ERR)
continue;
return true;
}
}
}
return false;
}
/**
* Creates console appender with some reasonable default logging settings.
*
* @return Logger with auto configured console appender.
*/
public Logger configureConsoleAppender() {
// from http://logging.apache.org/log4j/2.x/manual/customconfig.html
LoggerContext ctx = LoggerContext.getContext(false);
Configuration cfg = ctx.getConfiguration();
if (cfg instanceof DefaultConfiguration) {
ConfigurationBuilder<BuiltConfiguration> cfgBuilder = ConfigurationBuilderFactory.newConfigurationBuilder();
RootLoggerComponentBuilder rootLog = cfgBuilder.newRootLogger(Level.INFO);
cfg = cfgBuilder.add(rootLog).build();
addConsoleAppender(cfg);
ctx.reconfigure(cfg);
}
else {
addConsoleAppender(cfg);
ctx.updateLoggers();
}
return ctx.getRootLogger();
}
/** */
private void addConsoleAppender(Configuration logCfg) {
PatternLayout layout = PatternLayout.newBuilder()
.withPattern("%d{ISO8601}][%-5p][%t][%c{1}] %m%n")
.withCharset(Charset.defaultCharset())
.withAlwaysWriteExceptions(false)
.withNoConsoleNoAnsi(false)
.build();
ConsoleAppender consoleApp = ConsoleAppender.newBuilder()
.setTarget(SYSTEM_OUT)
.setName(CONSOLE_APPENDER)
.setLayout(layout)
.build();
consoleApp.start();
logCfg.addAppender(consoleApp);
logCfg.getRootLogger().addAppender(consoleApp, Level.TRACE, null);
}
/**
* Checks if Log4j is already configured within this VM or not.
*
* @return {@code True} if log4j was already configured, {@code false} otherwise.
*/
public static boolean isConfigured() {
return !(LoggerContext.getContext(false).getConfiguration() instanceof DefaultConfiguration);
}
/** {@inheritDoc} */
@Override public void setApplicationAndNode(@Nullable String application, UUID nodeId) {
A.notNull(nodeId, "nodeId");
this.nodeId = nodeId;
// Set nodeId as system variable to be used at configuration.
System.setProperty(NODE_ID, U.id8(nodeId));
System.setProperty(APP_ID, application != null
? application
: System.getProperty(APP_ID, "ignite"));
if (inited) {
synchronized (mux) {
inited = false;
}
addConsoleAppenderIfNeeded(new C1<Boolean, Logger>() {
@Override public Logger apply(Boolean init) {
if (init)
LoggerContext.getContext(false).reconfigure();
return LogManager.getRootLogger();
}
});
}
}
/** {@inheritDoc} */
@Override public UUID getNodeId() {
return nodeId;
}
/**
* Gets {@link IgniteLogger} wrapper around log4j logger for the given
* category. If category is {@code null}, then root logger is returned. If
* category is an instance of {@link Class} then {@code (Class)ctgr).getName()}
* is used as category name.
*
* @param ctgr {@inheritDoc}
* @return {@link IgniteLogger} wrapper around log4j logger.
*/
@Override public Log4J2Logger getLogger(Object ctgr) {
if (ctgr == null)
return new Log4J2Logger(LogManager.getRootLogger(), cfg);
if (ctgr instanceof Class) {
String name = ((Class<?>)ctgr).getName();
return new Log4J2Logger(LogManager.getLogger(name), cfg);
}
String name = ctgr.toString();
return new Log4J2Logger(LogManager.getLogger(name), cfg);
}
/** {@inheritDoc} */
@Override public void trace(String msg) {
trace(null, msg);
}
/** {@inheritDoc} */
@Override public void trace(@Nullable String marker, String msg) {
if (!isTraceEnabled())
warning("Logging at TRACE level without checking if TRACE level is enabled: " + msg);
impl.trace(getMarkerOrNull(marker), msg);
}
/** {@inheritDoc} */
@Override public void debug(String msg) {
debug(null, msg);
}
/** {@inheritDoc} */
@Override public void debug(@Nullable String marker, String msg) {
if (!isDebugEnabled())
warning("Logging at DEBUG level without checking if DEBUG level is enabled: " + msg);
impl.debug(getMarkerOrNull(marker), msg);
}
/** {@inheritDoc} */
@Override public void info(String msg) {
info(null, msg);
}
/** {@inheritDoc} */
@Override public void info(@Nullable String marker, String msg) {
if (!isInfoEnabled())
warning("Logging at INFO level without checking if INFO level is enabled: " + msg);
impl.info(getMarkerOrNull(marker), msg);
}
/** {@inheritDoc} */
@Override public void warning(String msg, @Nullable Throwable e) {
warning(null, msg, e);
}
/** {@inheritDoc} */
@Override public void warning(@Nullable String marker, String msg, @Nullable Throwable e) {
impl.warn(getMarkerOrNull(marker), msg, e);
}
/** {@inheritDoc} */
@Override public void error(String msg, @Nullable Throwable e) {
error(null, msg, e);
}
/** {@inheritDoc} */
@Override public void error(@Nullable String marker, String msg, @Nullable Throwable e) {
impl.error(getMarkerOrNull(marker), msg, e);
}
/** Returns Marker object for the specified name, or null if the name is null */
private Marker getMarkerOrNull(@Nullable String marker) {
return marker != null ? MarkerManager.getMarker(marker) : null;
}
/** {@inheritDoc} */
@Override public boolean isTraceEnabled() {
return impl.isTraceEnabled();
}
/** {@inheritDoc} */
@Override public boolean isDebugEnabled() {
return impl.isDebugEnabled();
}
/** {@inheritDoc} */
@Override public boolean isInfoEnabled() {
return impl.isInfoEnabled();
}
/** {@inheritDoc} */
@Override public boolean isQuiet() {
return quiet;
}
/** {@inheritDoc} */
@Override public String toString() {
return S.toString(Log4J2Logger.class, this, "config", cfg);
}
}