/*
* 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.geode.internal.logging.log4j;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Date;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
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.LoggerConfig;
import org.apache.logging.log4j.core.layout.PatternLayout;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.internal.DistributionManager;
import org.apache.geode.distributed.internal.InternalDistributedSystem;
import org.apache.geode.internal.admin.Alert;
import org.apache.geode.internal.admin.remote.AlertListenerMessage;
import org.apache.geode.internal.lang.ThreadUtils;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.internal.tcp.ReenteredConnectException;
/**
* A Log4j Appender which will notify listeners whenever a message of the requested level is written
* to the log file.
*
*/
public final class AlertAppender extends AbstractAppender implements PropertyChangeListener {
private static final String APPENDER_NAME = AlertAppender.class.getName();
private static final Logger logger = LogService.getLogger();
private static final AlertAppender instance = createAlertAppender();
/** Is this thread in the process of alerting? */
private static final ThreadLocal<Boolean> alerting = new ThreadLocal<Boolean>() {
@Override
protected Boolean initialValue() {
return Boolean.FALSE;
}
};
// Listeners are ordered with the narrowest levels (e.g. FATAL) at the end
private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<Listener>();
private final AppenderContext appenderContext = LogService.getAppenderContext();
private final AtomicReference<InternalDistributedSystem> systemRef = new AtomicReference<>();
// This can be set by a loner distributed sytem to disable alerting
private volatile boolean alertingDisabled = false;
private static AlertAppender createAlertAppender() {
AlertAppender alertAppender = new AlertAppender();
alertAppender.start();
return alertAppender;
}
private AlertAppender() {
super(APPENDER_NAME, null, PatternLayout.createDefaultLayout());
}
public void onConnect(final InternalDistributedSystem system) {
this.systemRef.set(system);
}
public static AlertAppender getInstance() {
return instance;
}
/**
* Returns true if the current thread is in the process of delivering an alert message.
*/
public static boolean isThreadAlerting() {
return alerting.get();
}
public boolean isAlertingDisabled() {
return alertingDisabled;
}
public void setAlertingDisabled(final boolean alertingDisabled) {
this.alertingDisabled = alertingDisabled;
}
public static void setIsAlerting(boolean isAlerting) {
alerting.set(isAlerting ? Boolean.TRUE : Boolean.FALSE);
}
/**
* This method is optimized with the assumption that at least one listener has set a level which
* requires that the event be sent. This is ensured by modifying the appender's configuration
* whenever a listener is added or removed.
*/
@Override
public void append(final LogEvent event) {
if (this.alertingDisabled) {
return;
}
// If already appending then don't send to avoid infinite recursion
if ((alerting.get())) {
return;
}
setIsAlerting(true);
try {
final boolean isDebugEnabled = logger.isDebugEnabled();
if (isDebugEnabled) {
logger.debug("Delivering an alert event: {}", event);
}
InternalDistributedSystem ds = this.systemRef.get();
if (ds == null) {
// Use info level to avoid triggering another alert
logger.info("Did not append alert event because the distributed system is set to null.");
return;
}
DistributionManager distMgr = (DistributionManager) ds.getDistributionManager();
final int intLevel = logLevelToAlertLevel(event.getLevel().intLevel());
final Date date = new Date(event.getTimeMillis());
final String threadName = event.getThreadName();
final String logMessage = event.getMessage().getFormattedMessage();
final String stackTrace = ThreadUtils.stackTraceToString(event.getThrown(), true);
final String connectionName = ds.getConfig().getName();
for (Listener listener : this.listeners) {
if (event.getLevel().intLevel() > listener.getLevel().intLevel()) {
break;
}
try {
AlertListenerMessage alertMessage =
AlertListenerMessage.create(listener.getMember(), intLevel, date, connectionName,
threadName, Thread.currentThread().getId(), logMessage, stackTrace);
if (listener.getMember().equals(distMgr.getDistributionManagerId())) {
if (isDebugEnabled) {
logger.debug("Delivering local alert message: {}, {}, {}, {}, {}, [{}], [{}].",
listener.getMember(), intLevel, date, connectionName, threadName, logMessage,
stackTrace);
}
alertMessage.process(distMgr);
} else {
if (isDebugEnabled) {
logger.debug("Delivering remote alert message: {}, {}, {}, {}, {}, [{}], [{}].",
listener.getMember(), intLevel, date, connectionName, threadName, logMessage,
stackTrace);
}
distMgr.putOutgoing(alertMessage);
}
} catch (ReenteredConnectException e) {
// OK. We can't send to this recipient because we're in the middle of
// trying to connect to it.
}
}
} finally {
setIsAlerting(false);
}
}
public synchronized void addAlertListener(final DistributedMember member, final int alertLevel) {
final Level level = LogService.toLevel(alertLevelToLogLevel(alertLevel));
if (this.listeners.size() == 0) {
this.appenderContext.getLoggerContext().addPropertyChangeListener(this);
}
addListenerToSortedList(new Listener(level, member));
LoggerConfig loggerConfig = this.appenderContext.getLoggerConfig();
loggerConfig.addAppender(this, this.listeners.get(0).getLevel(), null);
if (logger.isDebugEnabled()) {
logger.debug("Added/Replaced alert listener for member {} at level {}", member, level);
}
}
public synchronized boolean removeAlertListener(final DistributedMember member) {
final boolean memberWasFound = this.listeners.remove(new Listener(null, member));
if (memberWasFound) {
if (this.listeners.size() == 0) {
this.appenderContext.getLoggerContext().removePropertyChangeListener(this);
this.appenderContext.getLoggerConfig().removeAppender(APPENDER_NAME);
} else {
LoggerConfig loggerConfig = this.appenderContext.getLoggerConfig();
loggerConfig.addAppender(this, this.listeners.get(0).getLevel(), null);
}
if (logger.isDebugEnabled()) {
logger.debug("Removed alert listener for member {}", member);
}
}
return memberWasFound;
}
public synchronized boolean hasAlertListener(final DistributedMember member,
final int alertLevel) {
final Level level = LogService.toLevel(alertLevelToLogLevel(alertLevel));
for (Listener listener : this.listeners) {
if (listener.getMember().equals(member) && listener.getLevel().equals(level)) {
return true;
}
}
// Special case for alert level Alert.OFF (NONE_LEVEL), because we can never have an actual
// listener with
// this level (see AlertLevelChangeMessage.process()).
if (alertLevel == Alert.OFF) {
for (Listener listener : this.listeners) {
if (listener.getMember().equals(member)) {
return false;
}
}
return true;
}
return false;
}
@Override
public synchronized void propertyChange(final PropertyChangeEvent evt) {
if (logger.isDebugEnabled()) {
logger.debug("Responding to a property change event. Property name is {}.",
evt.getPropertyName());
}
if (evt.getPropertyName().equals(LoggerContext.PROPERTY_CONFIG)) {
LoggerConfig loggerConfig = this.appenderContext.getLoggerConfig();
if (!loggerConfig.getAppenders().containsKey(APPENDER_NAME)) {
loggerConfig.addAppender(this, this.listeners.get(0).getLevel(), null);
}
}
}
/**
* Will add (or replace) a listener to the list of sorted listeners such that listeners with a
* narrower level (e.g. FATAL) will be at the end of the list.
*
* @param listener The listener to add to the list.
*/
private void addListenerToSortedList(final Listener listener) {
if (this.listeners.contains(listener)) {
this.listeners.remove(listener);
}
for (int i = 0; i < this.listeners.size(); i++) {
if (listener.getLevel().compareTo(this.listeners.get(i).getLevel()) >= 0) {
this.listeners.add(i, listener);
return;
}
}
this.listeners.add(listener);
}
/**
* Converts an int alert level to an int log level.
*
* @param alertLevel The int value for the alert level
* @return The int value for the matching log level
* @throws java.lang.IllegalArgumentException If there is no matching log level
*/
public static int alertLevelToLogLevel(final int alertLevel) {
switch (alertLevel) {
case Alert.SEVERE:
return Level.FATAL.intLevel();
case Alert.ERROR:
return Level.ERROR.intLevel();
case Alert.WARNING:
return Level.WARN.intLevel();
case Alert.OFF:
return Level.OFF.intLevel();
}
throw new IllegalArgumentException("Unknown Alert level [" + alertLevel + "].");
}
/**
* Converts an int log level to an int alert level.
*
* @param logLevel The int value for the log level
* @return The int value for the matching alert level
* @throws java.lang.IllegalArgumentException If there is no matching log level
*/
public static int logLevelToAlertLevel(final int logLevel) {
if (logLevel == Level.FATAL.intLevel()) {
return Alert.SEVERE;
} else if (logLevel == Level.ERROR.intLevel()) {
return Alert.ERROR;
} else if (logLevel == Level.WARN.intLevel()) {
return Alert.WARNING;
} else if (logLevel == Level.OFF.intLevel()) {
return Alert.OFF;
}
throw new IllegalArgumentException("Unknown Log level [" + logLevel + "].");
}
public synchronized void shuttingDown() {
this.listeners.clear();
this.appenderContext.getLoggerContext().removePropertyChangeListener(this);
this.appenderContext.getLoggerConfig().removeAppender(APPENDER_NAME);
this.systemRef.set(null);
}
/**
* Simple value object which holds an InteralDistributedMember and Level pair.
*/
static class Listener {
private Level level;
private DistributedMember member;
public Level getLevel() {
return this.level;
}
public DistributedMember getMember() {
return this.member;
}
Listener(final Level level, final DistributedMember member) {
this.level = level;
this.member = member;
}
/**
* Never used, but maintain the hashCode/equals contract.
*/
@Override
public int hashCode() {
return 31 + ((this.member == null) ? 0 : this.member.hashCode());
}
/**
* Ignore the level when determining equality.
*/
@Override
public boolean equals(Object other) {
return (this.member.equals(((Listener) other).member)) ? true : false;
}
@Override
public String toString() {
return "Listener [level=" + this.level + ", member=" + this.member + "]";
}
}
}