blob: 49635035425647a25a23dd6c87a313831e0db75d [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.solr.logging.log4j2;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.common.base.Throwables;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.filter.ThresholdFilter;
import org.apache.logging.log4j.message.Message;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.util.SuppressForbidden;
import org.apache.solr.logging.CircularList;
import org.apache.solr.logging.ListenerConfig;
import org.apache.solr.logging.LogWatcher;
import org.apache.solr.logging.LoggerInfo;
@SuppressForbidden(reason = "class is specific to log4j2")
public class Log4j2Watcher extends LogWatcher<LogEvent> {
private final static String LOG4J2_WATCHER_APPENDER = "Log4j2WatcherAppender";
@SuppressForbidden(reason = "class is specific to log4j2")
protected class Log4j2Appender extends AbstractAppender {
private Log4j2Watcher watcher;
private ThresholdFilter filter;
private Level threshold;
Log4j2Appender(Log4j2Watcher watcher, ThresholdFilter filter, Level threshold) {
super(LOG4J2_WATCHER_APPENDER, filter, null);
this.watcher = watcher;
this.filter = filter;
this.threshold = threshold;
}
public void append(LogEvent logEvent) {
watcher.add(logEvent, logEvent.getTimeMillis());
}
public Level getThreshold() {
return threshold;
}
public void setThreshold(Level threshold) {
this.threshold = threshold;
removeFilter(filter);
filter = ThresholdFilter.createFilter(threshold, Filter.Result.ACCEPT, Filter.Result.DENY);
addFilter(filter);
}
}
@SuppressForbidden(reason = "class is specific to log4j2")
protected class Log4j2Info extends LoggerInfo {
final Level level;
Log4j2Info(String name, Level level) {
super(name);
this.level = level;
}
@Override
public String getLevel() {
return (level != null) ? level.toString() : null;
}
@Override
public String getName() {
return name;
}
@Override
public boolean isSet() {
return (level != null) ? true : false;
}
}
public static final Logger log = LogManager.getLogger(Log4j2Watcher.class);
protected Log4j2Appender appender = null;
@Override
public String getName() {
return "Log4j2";
}
@Override
public List<String> getAllLevels() {
return Arrays.asList(
Level.ALL.toString(),
Level.TRACE.toString(),
Level.DEBUG.toString(),
Level.INFO.toString(),
Level.WARN.toString(),
Level.ERROR.toString(),
Level.FATAL.toString(),
Level.OFF.toString());
}
@Override
public void setLogLevel(String loggerName, String level) {
assert loggerName != null;
assert level != null;
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
Configuration config = ctx.getConfiguration();
LoggerConfig loggerConfig = getLoggerConfig(ctx, loggerName);
assert loggerConfig != null;
boolean madeChanges = false;
if (loggerName.equals(loggerConfig.getName()) || isRootLogger(loggerName)) {
if (level == null || "unset".equals(level) || "null".equals(level)) {
level = Level.OFF.toString();
loggerConfig.setLevel(Level.OFF);
madeChanges = true;
} else {
try {
loggerConfig.setLevel(Level.valueOf(level));
madeChanges = true;
} catch (IllegalArgumentException iae) {
log.error("{} is not a valid log level! Valid values are: {}", level, getAllLevels());
}
}
} else {
//It doesn't have its own logger yet so let's create one
LoggerConfig explicitConfig = new LoggerConfig(loggerName, Level.valueOf(level), true);
explicitConfig.setParent(loggerConfig);
config.addLogger(loggerName, explicitConfig);
madeChanges = true;
}
if (madeChanges) {
ctx.updateLoggers();
if (log.isInfoEnabled()) {
log.info("Setting log level to '{}' for logger: {}", level, loggerName);
}
}
}
protected boolean isRootLogger(String loggerName) {
return LoggerInfo.ROOT_NAME.equals(loggerName);
}
protected LoggerConfig getLoggerConfig(LoggerContext ctx, String loggerName) {
Configuration config = ctx.getConfiguration();
return isRootLogger(loggerName) ? config.getLoggerConfig(LogManager.ROOT_LOGGER_NAME)
: config.getLoggerConfig(loggerName);
}
@Override
public Collection<LoggerInfo> getAllLoggers() {
Logger root = LogManager.getRootLogger();
LoggerContext ctx = (LoggerContext)LogManager.getContext(false);
Map<String,LoggerInfo> map = new HashMap<>(ctx.getLoggers().size());
//First let's get the explicitly configured loggers
Map<String, LoggerConfig> loggers = ctx.getConfiguration().getLoggers();
for(Map.Entry<String, LoggerConfig> logger : loggers.entrySet()) {
String name = logger.getKey();
if (logger == root || root.equals(logger) || isRootLogger(name) || "".equals(name)) {
continue;
}
map.put(name, new Log4j2Info(name, logger.getValue().getLevel()));
}
for (org.apache.logging.log4j.core.Logger logger : ctx.getLoggers()) {
String name = logger.getName();
if (logger == root || root.equals(logger) || isRootLogger(name))
continue;
map.put(name, new Log4j2Info(name, logger.getLevel()));
while (true) {
int dot = name.lastIndexOf(".");
if (dot < 0)
break;
name = name.substring(0, dot);
if (!map.containsKey(name))
map.put(name, new Log4j2Info(name, null));
}
}
map.put(LoggerInfo.ROOT_NAME, new Log4j2Info(LoggerInfo.ROOT_NAME, root.getLevel()));
return map.values();
}
@Override
public void setThreshold(String level) {
Log4j2Appender app = getAppender();
Level current = app.getThreshold();
app.setThreshold(Level.toLevel(level));
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
LoggerConfig config = getLoggerConfig(ctx, LoggerInfo.ROOT_NAME);
config.removeAppender(app.getName());
config.addAppender(app, app.getThreshold(), app.getFilter());
((LoggerContext)LogManager.getContext(false)).updateLoggers();
if (log.isInfoEnabled()) {
log.info("Updated watcher threshold from {} to {} ", current, level);
}
}
@Override
public String getThreshold() {
return String.valueOf(getAppender().getThreshold());
}
protected Log4j2Appender getAppender() {
if (appender == null)
throw new IllegalStateException("No appenders configured! Must call registerListener(ListenerConfig) first.");
return appender;
}
@Override
public void registerListener(ListenerConfig cfg) {
if (history != null)
throw new IllegalStateException("History already registered");
history = new CircularList<LogEvent>(cfg.size);
Level threshold = (cfg.threshold != null) ? Level.toLevel(cfg.threshold) : Level.WARN;
ThresholdFilter filter = ThresholdFilter.createFilter(threshold, Filter.Result.ACCEPT, Filter.Result.DENY);
// If there's already an appender like this, remove it
LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
LoggerConfig config = getLoggerConfig(ctx, LoggerInfo.ROOT_NAME);
appender = new Log4j2Appender(this, filter, threshold); // "Log4j2WatcherAppender"
config.removeAppender(appender.getName());
if (!appender.isStarted())
appender.start();
config.addAppender(appender, threshold, filter);
ctx.updateLoggers();
}
@Override
public long getTimestamp(LogEvent event) {
return event.getTimeMillis();
}
@Override
public SolrDocument toSolrDocument(LogEvent event) {
SolrDocument doc = new SolrDocument();
doc.setField("time", new Date(event.getTimeMillis()));
doc.setField("level", event.getLevel().toString());
doc.setField("logger", event.getLoggerName());
Message message = event.getMessage();
doc.setField("message", message.getFormattedMessage());
Throwable t = message.getThrowable();
if (t != null)
doc.setField("trace", Throwables.getStackTraceAsString(t));
Map<String,String> contextMap = event.getContextMap();
if (contextMap != null) {
for (Map.Entry<String, String> entry : contextMap.entrySet())
doc.setField(entry.getKey(), entry.getValue());
}
if (!doc.containsKey("core"))
doc.setField("core", ""); // avoids an ugly "undefined" column in the UI
return doc;
}
}