Simple log alarm based on log4j2

Posted May 25, 20205 min read

Requirements

When the system reports an ERROR error, it can be notified in real time.

Ideas

The current project is relatively small and I do n t want to rely too much on additional third-party components.
When the project is in ERROR, the ERROR log is printed, so you can send a notification message when log4j receives the ERROR log request.

Practice

Filter is the extension point of log4j2. As you can see from the picture(picture from how to write Log4j2 desensitization plugin ), Filter can be found in Global , Logger, Appender are filtered in three places.
WX20200525-195412@2x.png
The corresponding log4j.xml configuration places in the three places are as follows:

<Configuration>
    <MyFilter /> <!-Global->
    <Appenders>
        <RollingFile name = "ErrorRollingFile">
            <Filters>
                <ThresholdFilter level = "ERROR" onMatch = "NEUTRAL" />
                <MyFilter /> <!-Logger->
            </Filters>
        </RollingFile>
    </Appenders>
    <Loggers>
        <Root level = "INFO">
            <MyFilter /> <!-Appender->
            <AppenderRef ref = "ErrorRollingFile" />
        </Root>
    </Loggers>
</Configuration>

log4j provides the base class of the filter AbstractFilter:

  • The entry method of Global Filter isfilter(Logger logger, Level level, Marker marker, String msg, Object ... params), msg is the content before filling the parameters, and params is the parameter list, including Throwable object.
  • The entry method of Logger and Appender is filter(final LogEvent event), the content after filling parameters is obtained through event.getMessage(). GetFormattedMessage(), and the exception is obtained through event.getThrown() Object.

Code

@Plugin(name = "ErrorNotifyFilter", category = Node.CATEGORY, elementType = Filter.ELEMENT_TYPE, printObject = true)
public class ErrorNotifyLog4j2Filter extends AbstractFilter {

    private String projectName;
    private List <String> rtxReceivers;

    private ErrorNotifyLog4j2Filter(String projectName, String rtxReceivers) {
        super();
        this.projectName = projectName;
        this.rtxReceivers = Lists.newArrayList(rtxReceivers.split(","));
    }

    @Override
    public Result filter(LogEvent event) {
        notify(event.getLevel(), event.getMessage(). getFormattedMessage(), event.getThrown());
        return super.filter(event);
    }

    @Override
    public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) {
        notify(level, msg.getFormattedMessage(), t);
        return super.filter(logger, level, marker, msg, t);
    }

    @Override
    public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) {
        notify(level, msg == null? "":msg.toString(), t);
        return super.filter(logger, level, marker, msg, t);
    }

    @Override
    public Result filter(Logger logger, Level level, Marker marker, String msg, Object ... params) {
        notify(level, msg, getExceptionParam(params));
        return super.filter(logger, level, marker, msg, params);
    }

   /**
     * @param level
     * @param msg
     * @param t
     * @author
     * @date
     * /
    private void notify(Level level, String msg, Throwable t) {
        try {
            if(level == null || level.intLevel()! = Level.ERROR.intLevel()) {
                return;
            }
            if(StringUtils.isBlank(msg) && t == null) {
                return;
            }
            Log4j2AsyncExecutor.executorService.submit(()-> {
                try {
                    String notifyMsg = getNotifyMsg(msg, t);
                    String actualActiveProfiles = getActiveProfiles();
                    MessageUtil.postMessage(Lists.newArrayList(MessageTypeEnum.RTX),
                            rtxReceivers,
                           (StringUtils.isBlank(actualActiveProfiles)? "":"[" + ActualActiveProfiles + "]") + "[" + projectName + "]Project Abnormal Alarm-" + DateUtils.formatDateTime(new Date()),
                            notifyMsg);
                } catch(Exception ignoreException) {
                    ignoreException.printStackTrace();
                }
            });
        } catch(Throwable ignoreException) {
            ignoreException.printStackTrace();
        }
    }

   /**
     * @param params
     * @return java.lang.Throwable
     * @author
     * @date
     * /
    private Throwable getExceptionParam(Object ... params) {
        if(params == null || params.length == 0) {
            return null;
        }
        for(Object param:params) {
            if(param instanceof Throwable) {
                return(Throwable) param;
            }
        }
        return null;
    }

   /**
     * In order to make the alarm more clear, two rows of stacks are obtained here, but the same also reduces the performance
     *
     * @param msg
     * @param t
     * @return java.lang.String
     * @author
     * @date
     * /
    private String getNotifyMsg(String msg, Throwable t) {
        String errorMsg = "Information:" +(msg == null? "":Msg);
        String exceptionMsg = "";
        if(t! = null) {
            exceptionMsg + = "\ nException:" + t.toString();
            StackTraceElement []stackTraceElements = t.getStackTrace();
            if(stackTraceElements! = null) {
                if(stackTraceElements.length> 0) {
                    exceptionMsg + = "\ n" + stackTraceElements [0];
                }
                if(stackTraceElements.length> 1) {
                    exceptionMsg + = "\ n" + stackTraceElements [1];
                }
            }
        }
        return errorMsg + exceptionMsg;
    }

   /**
     * @return java.lang.String
     * @author
     * @date
     * /
    private String getActiveProfiles() {
        String []activeProfiles = SpringContextUtil.getApplicationContext() == null? Null:SpringContextUtil.getActiveProfile();
        if(activeProfiles == null || activeProfiles.length == 0) {
            return "";
        }
        List <String> actualActiveProfiles = Arrays.stream(activeProfiles)
                //Part of the project's profile is useful to include, including "-" in the include, to filter out the included data
                .filter(str->! str.contains("-"))
                .collect(Collectors.toList());
        return StringUtils.join(actualActiveProfiles, ",");
    }

   /**
     * @return com.tencent.tscm.purchase.interfacer.log4j2.ErrorNotifyFilter
     * @author
     * @date
     * /
    @PluginFactory
    public static ErrorNotifyLog4j2Filter createFilter(@PluginAttribute("projectName") final String projectName,
                                                       @PluginAttribute("rtxReceivers") final String rtxReceivers) {
        return new ErrorNotifyLog4j2Filter(projectName, rtxReceivers);
    }
}

@Component
public class Log4j2AsyncExecutor {
     //Configure thread pool information according to your own project
    public static ExecutorService executorService = new ThreadPoolExecutor(1,
            1,
            0L,
            TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue <>(),
            new ThreadFactoryBuilder(). setNameFormat("log4j2-async-executor-pool-%d"). build());

   /**
     * @author
     * @date
     * /
    @PreDestroy
    public synchronized void shutdown() {
        if(executorService! = null) {
            ThreadUtils.shutdown(executorService, 5, TimeUnit.SECONDS);
            executorService = null;
        }
    }
}

Special note here:why should there be Log4j2AsyncExecutor this class?
Because the alarm is sent asynchronously, a thread pool is used, so when the process is stopped, the thread pool must be destroyed; and although AbstractFilter implements the LifeCycle interface and has a stop method, in fact the stop method does not It won't be called; so it depends on Spring's PreDestroy to destroy it.

Configuration

The alarm is definitely to take the content after fill parameters, so Filter will be more appropriate in Logger or Appender; but the code is also compatible with the filter placed in global configuration.

<Configuration>
    ...
    <Appenders>
        <RollingFile name = "ErrorRollingFile">
            <Filters>
                <ThresholdFilter level = "ERROR" onMatch = "NEUTRAL" />
                <ErrorNotifyFilter projectName = "xxxx" rtxReceivers = "xxxx" />
            </Filters>
            ...
        </RollingFile>
    </Appenders>
    <Loggers>
        <Root level = "INFO">
            ...
            <AppenderRef ref = "ErrorRollingFile" />
        </Root>
    </Loggers>
</Configuration>

Avoiding pits

The difference between NEUTRAL and ACCEPT is:

  1. If the configuration is ACCEPT, it means that the filter passes without going through the subsequent Filter.
  2. If the configuration is NEUTRAL, also pass and continue the subsequent Filter.

Remarks

The method of this article is that the alarm is in the same process as the project, which has many disadvantages, such as:

  1. If there is a problem with the alarm function, such as a memory leak, it may affect the normal operation of the project.
  2. Inconvenient upgrade:when the function is improved/BUG repaired, the business party needs to upgrade the jar package, which will cause trouble to the business party; and the version of each business party cannot be unified, and the compatibility of each version needs to be considered when subsequent versions are upgraded Happening.

In summary, this article is more suitable for small systems.