package de.bwaldvogel.log4j; import com.sun.jna.Native; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.Layout; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.appender.AbstractAppender; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.plugins.Plugin; import org.apache.logging.log4j.core.config.plugins.PluginAttribute; import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; import org.apache.logging.log4j.core.config.plugins.PluginElement; import org.apache.logging.log4j.core.config.plugins.PluginFactory; import org.apache.logging.log4j.core.util.Booleans; import org.apache.logging.log4j.util.ReadOnlyStringMap; import java.io.PrintWriter; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; @Plugin(name = "SystemdJournal", category = "Core", elementType = "appender", printObject = true) public class SystemdJournalAppender extends AbstractAppender { private final SystemdJournalLibrary journalLibrary; private final boolean logStacktrace; private final boolean logSource; private final boolean logThreadName; private final boolean logLoggerName; private final boolean logAppenderName; private final boolean logThreadContext; private final String threadContextPrefix; private final String syslogIdentifier; private final String syslogFacility; SystemdJournalAppender(String name, Filter filter, Layout<?> layout, boolean ignoreExceptions, SystemdJournalLibrary journalLibrary, boolean logSource, boolean logStacktrace, boolean logThreadName, boolean logLoggerName, boolean logAppenderName, boolean logThreadContext, String threadContextPrefix, String syslogIdentifier, String syslogFacility) { super(name, filter, layout, ignoreExceptions); this.journalLibrary = journalLibrary; this.logSource = logSource; this.logStacktrace = logStacktrace; this.logThreadName = logThreadName; this.logLoggerName = logLoggerName; this.logAppenderName = logAppenderName; this.logThreadContext = logThreadContext; if (threadContextPrefix == null) { this.threadContextPrefix = "THREAD_CONTEXT_"; } else { this.threadContextPrefix = normalizeKey(threadContextPrefix); } this.syslogIdentifier = syslogIdentifier; this.syslogFacility = syslogFacility; } @PluginFactory public static SystemdJournalAppender createAppender(@PluginAttribute("name") final String name, @PluginAttribute("ignoreExceptions") final String ignoreExceptionsString, @PluginAttribute("logSource") final String logSourceString, @PluginAttribute("logStacktrace") final String logStacktraceString, @PluginAttribute("logLoggerName") final String logLoggerNameString, @PluginAttribute("logAppenderName") final String logAppenderNameString, @PluginAttribute("logThreadName") final String logThreadNameString, @PluginAttribute("logThreadContext") final String logThreadContextString, @PluginAttribute("threadContextPrefix") final String threadContextPrefix, @PluginAttribute("syslogIdentifier") final String syslogIdentifier, @PluginAttribute("syslogFacility") final String syslogFacility, @PluginElement("Layout") final Layout<?> layout, @PluginElement("Filter") final Filter filter, @PluginConfiguration final Configuration config) { final boolean ignoreExceptions = Booleans.parseBoolean(ignoreExceptionsString, true); final boolean logSource = Booleans.parseBoolean(logSourceString, false); final boolean logStacktrace = Booleans.parseBoolean(logStacktraceString, true); final boolean logThreadName = Booleans.parseBoolean(logThreadNameString, true); final boolean logLoggerName = Booleans.parseBoolean(logLoggerNameString, true); final boolean logAppenderName = Booleans.parseBoolean(logAppenderNameString, true); final boolean logThreadContext = Booleans.parseBoolean(logThreadContextString, true); if (name == null) { LOGGER.error("No name provided for SystemdJournalAppender"); return null; } final SystemdJournalLibrary journalLibrary; try { journalLibrary = Native.loadLibrary("systemd", SystemdJournalLibrary.class); } catch (UnsatisfiedLinkError e) { throw new RuntimeException("Failed to load systemd library." + " Please note that JNA requires an executable temporary folder." + " It can be explicitly defined with -Djna.tmpdir", e); } return new SystemdJournalAppender(name, filter, layout, ignoreExceptions, journalLibrary, logSource, logStacktrace, logThreadName, logLoggerName, logAppenderName, logThreadContext, threadContextPrefix, syslogIdentifier, syslogFacility); } private int log4jLevelToJournalPriority(Level level) { // // syslog.h // // #define LOG_EMERG 0 - system is unusable // #define LOG_ALERT 1 - action must be taken immediately // #define LOG_CRIT 2 - critical conditions // #define LOG_ERR 3 - error conditions // #define LOG_WARNING 4 - warning conditions // #define LOG_NOTICE 5 - normal but significant condition // #define LOG_INFO 6 - informational // #define LOG_DEBUG 7 - debug-level messages // switch (level.getStandardLevel()) { case FATAL: return 2; // LOG_CRIT case ERROR: return 3; // LOG_ERR case WARN: return 4; // LOG_WARNING case INFO: return 6; // LOG_INFO case DEBUG: case TRACE: return 7; // LOG_DEBUG default: throw new IllegalArgumentException("Cannot map log level: " + level); } } @Override public void append(LogEvent event) { List<Object> args = new ArrayList<>(); args.add(buildFormattedMessage(event)); args.add("PRIORITY=%d"); args.add(Integer.valueOf(log4jLevelToJournalPriority(event.getLevel()))); if (logThreadName) { args.add("THREAD_NAME=%s"); args.add(event.getThreadName()); } if (logLoggerName) { args.add("LOG4J_LOGGER=%s"); args.add(event.getLoggerName()); } if (logAppenderName) { args.add("LOG4J_APPENDER=%s"); args.add(getName()); } if (logStacktrace && event.getThrown() != null) { StringWriter stacktrace = new StringWriter(); event.getThrown().printStackTrace(new PrintWriter(stacktrace)); args.add("STACKTRACE=%s"); args.add(stacktrace.toString()); } if (logSource && event.getSource() != null) { String fileName = event.getSource().getFileName(); args.add("CODE_FILE=%s"); args.add(fileName); String methodName = event.getSource().getMethodName(); args.add("CODE_FUNC=%s"); args.add(methodName); int lineNumber = event.getSource().getLineNumber(); args.add("CODE_LINE=%d"); args.add(Integer.valueOf(lineNumber)); } if (logThreadContext) { ReadOnlyStringMap context = event.getContextData(); if (context != null) { for (Entry<String, String> entry : context.toMap().entrySet()) { String key = entry.getKey(); args.add(threadContextPrefix + normalizeKey(key) + "=%s"); args.add(entry.getValue()); } } } if (syslogIdentifier != null && !syslogIdentifier.isEmpty()) { args.add("SYSLOG_IDENTIFIER=%s"); args.add(syslogIdentifier); } if (syslogFacility != null && !syslogFacility.isEmpty()) { args.add("SYSLOG_FACILITY=%d"); args.add(Integer.valueOf(syslogFacility)); } args.add(null); // null terminated journalLibrary.sd_journal_send("MESSAGE=%s", args.toArray()); } private String buildFormattedMessage(LogEvent event) { if (getLayout() != null) { byte[] message = getLayout().toByteArray(event); return new String(message, StandardCharsets.UTF_8); } return event.getMessage().getFormattedMessage(); } private static String normalizeKey(String key) { return key.toUpperCase().replaceAll("[^_A-Z0-9]", "_"); } }