-
Notifications
You must be signed in to change notification settings - Fork 24.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Backport Structured Audit Logging #33894
Changes from 2 commits
bb5928f
1ca9d19
259e3cc
b3adbdb
0e69812
f19dce5
d933333
c81b362
29e2aea
32b90c4
b49f504
6bf666b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,45 +8,134 @@ | |
import org.apache.logging.log4j.Level; | ||
import org.apache.logging.log4j.LogManager; | ||
import org.apache.logging.log4j.Logger; | ||
import org.apache.logging.log4j.core.Layout; | ||
import org.apache.logging.log4j.core.LogEvent; | ||
import org.apache.logging.log4j.core.LoggerContext; | ||
import org.apache.logging.log4j.core.StringLayout; | ||
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.RegexFilter; | ||
import org.elasticsearch.common.Nullable; | ||
import org.elasticsearch.common.logging.ESLoggerFactory; | ||
import org.elasticsearch.common.logging.Loggers; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
/** | ||
* Logger that captures events and appends them to in memory lists, with one | ||
* list for each log level. This works with the global log manager context, | ||
* meaning that there could only be a single logger with the same name. | ||
*/ | ||
public class CapturingLogger { | ||
|
||
public static Logger newCapturingLogger(final Level level) throws IllegalAccessException { | ||
private static final String IMPLICIT_APPENDER_NAME = "__implicit"; | ||
|
||
/** | ||
* Constructs a new {@link CapturingLogger} named as the fully qualified name of | ||
* the invoking method. One name can be assigned to a single logger globally, so | ||
* don't call this method multiple times in the same method. | ||
* | ||
* @param level | ||
* The minimum priority level of events that will be captured. | ||
* @param layout | ||
* Optional parameter allowing to set the layout format of events. | ||
* This is useful because events are captured to be inspected (and | ||
* parsed) later. When parsing, it is useful to be in control of the | ||
* printing format as well. If not specified, | ||
* {@code event.getMessage().getFormattedMessage()} is called to | ||
* format the event. | ||
* @return The new logger. | ||
*/ | ||
public static Logger newCapturingLogger(final Level level, @Nullable StringLayout layout) throws IllegalAccessException { | ||
// careful, don't "bury" this on the call stack, unless you know what you're doing | ||
final StackTraceElement caller = Thread.currentThread().getStackTrace()[2]; | ||
final String name = caller.getClassName() + "." + caller.getMethodName() + "." + level.toString(); | ||
final Logger logger = ESLoggerFactory.getLogger(name); | ||
Loggers.setLevel(logger, level); | ||
final MockAppender appender = new MockAppender(name); | ||
attachNewMockAppender(logger, IMPLICIT_APPENDER_NAME, layout); | ||
return logger; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
||
public static void attachNewMockAppender(final Logger logger, final String appenderName, @Nullable StringLayout layout) | ||
throws IllegalAccessException { | ||
final MockAppender appender = new MockAppender(buildAppenderName(logger.getName(), appenderName), layout); | ||
appender.start(); | ||
Loggers.addAppender(logger, appender); | ||
return logger; | ||
} | ||
|
||
private static MockAppender getMockAppender(final String name) { | ||
private static String buildAppenderName(final String loggerName, final String appenderName) { | ||
// appender name also has to be unique globally (logging context globally) | ||
return loggerName + "." + appenderName; | ||
} | ||
|
||
private static MockAppender getMockAppender(final String loggerName, final String appenderName) { | ||
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false); | ||
final Configuration config = ctx.getConfiguration(); | ||
final LoggerConfig loggerConfig = config.getLoggerConfig(name); | ||
return (MockAppender) loggerConfig.getAppenders().get(name); | ||
final LoggerConfig loggerConfig = config.getLoggerConfig(loggerName); | ||
final String mockAppenderName = buildAppenderName(loggerName, appenderName); | ||
return (MockAppender) loggerConfig.getAppenders().get(mockAppenderName); | ||
} | ||
|
||
public static boolean isEmpty(final String name) { | ||
final MockAppender appender = getMockAppender(name); | ||
return appender.isEmpty(); | ||
/** | ||
* Checks if the logger's appender(s) has captured any events. | ||
albertzaharovits marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* @param loggerName | ||
* The unique global name of the logger. | ||
* @param appenderNames | ||
* Names of other appenders nested under this same logger. | ||
* @return {@code true} if no event has been captured, {@code false} otherwise. | ||
*/ | ||
public static boolean isEmpty(final String loggerName, final String... appenderNames) { | ||
// check if implicit appender is empty | ||
final MockAppender implicitAppender = getMockAppender(loggerName, IMPLICIT_APPENDER_NAME); | ||
assert implicitAppender != null; | ||
if (false == implicitAppender.isEmpty()) { | ||
return false; | ||
} | ||
if (null == appenderNames) { | ||
return true; | ||
} | ||
// check if any named appenders are empty | ||
for (String appenderName : appenderNames) { | ||
final MockAppender namedAppender = getMockAppender(loggerName, appenderName); | ||
if (namedAppender != null && false == namedAppender.isEmpty()) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is empty can optionally check multiple appenders. |
||
|
||
/** | ||
* Gets the captured events for a logger by its name. Events are those of the | ||
* implicit appender of the logger. | ||
* | ||
* @param loggerName | ||
* The unique global name of the logger. | ||
* @param level | ||
* The priority level of the captured events to be returned. | ||
* @return A list of captured events formated to {@code String}. | ||
*/ | ||
public static List<String> output(final String loggerName, final Level level) { | ||
return output(loggerName, IMPLICIT_APPENDER_NAME, level); | ||
} | ||
|
||
public static List<String> output(final String name, final Level level) { | ||
final MockAppender appender = getMockAppender(name); | ||
/** | ||
* Gets the captured events for a logger and an appender by their respective | ||
* names. There is a one to many relationship between loggers and appenders. | ||
* | ||
* @param loggerName | ||
* The unique global name of the logger. | ||
* @param appenderName | ||
* The name of an appender associated with the {@code loggerName} | ||
* logger. | ||
* @param level | ||
* The priority level of the captured events to be returned. | ||
* @return A list of captured events formated to {@code String}. | ||
*/ | ||
public static List<String> output(final String loggerName, final String appenderName, final Level level) { | ||
final MockAppender appender = getMockAppender(loggerName, appenderName); | ||
return appender.output(level); | ||
} | ||
|
||
|
@@ -58,8 +147,8 @@ private static class MockAppender extends AbstractAppender { | |
public final List<String> debug = new ArrayList<>(); | ||
public final List<String> trace = new ArrayList<>(); | ||
|
||
private MockAppender(final String name) throws IllegalAccessException { | ||
super(name, RegexFilter.createFilter(".*(\n.*)*", new String[0], false, null, null), null); | ||
private MockAppender(final String name, StringLayout layout) throws IllegalAccessException { | ||
super(name, RegexFilter.createFilter(".*(\n.*)*", new String[0], false, null, null), layout); | ||
} | ||
|
||
@Override | ||
|
@@ -68,25 +157,34 @@ public void append(LogEvent event) { | |
// we can not keep a reference to the event here because Log4j is using a thread | ||
// local instance under the hood | ||
case "ERROR": | ||
error.add(event.getMessage().getFormattedMessage()); | ||
error.add(formatMessage(event)); | ||
break; | ||
case "WARN": | ||
warn.add(event.getMessage().getFormattedMessage()); | ||
warn.add(formatMessage(event)); | ||
break; | ||
case "INFO": | ||
info.add(event.getMessage().getFormattedMessage()); | ||
info.add(formatMessage(event)); | ||
break; | ||
case "DEBUG": | ||
debug.add(event.getMessage().getFormattedMessage()); | ||
debug.add(formatMessage(event)); | ||
break; | ||
case "TRACE": | ||
trace.add(event.getMessage().getFormattedMessage()); | ||
trace.add(formatMessage(event)); | ||
break; | ||
default: | ||
throw invalidLevelException(event.getLevel()); | ||
} | ||
} | ||
|
||
private String formatMessage(LogEvent event) { | ||
final Layout<?> layout = getLayout(); | ||
if (layout instanceof StringLayout) { | ||
return ((StringLayout) layout).toSerializable(event); | ||
} else { | ||
return event.getMessage().getFormattedMessage(); | ||
} | ||
} | ||
|
||
private IllegalArgumentException invalidLevelException(Level level) { | ||
return new IllegalArgumentException("invalid level, expected [ERROR|WARN|INFO|DEBUG|TRACE] but was [" + level + "]"); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean that when customers upgrade to say 6.5 version, they will have two logs being populated:
*_access.log
and*_audit.log
at the same time? Depending on whether the customer has done right steps to get away from old logging, it might result into two logs being populated (unknowingly constraining the system).I think it would be better to keep old one enabled and the new one disabled and let customers go and move from the old appender to new appender.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bizybot See the discussion starting here #31046 (comment)
Basically, Filebeat has to support whatever format we're defaulting too. So if we're not enabling the new appender in 6.x Filebeat will not use this in 6.x, so we might as well not port this to 6.x because Filebeat is the important reason we do this (sort of, the root reason is to get rid of the
IndexAuditTrail
, but we need Filebeat to consume or logs before we can do that).And since we ❤️ our users, we don't want to break any systems they have set up right now that are consuming the present format. So, the compromise is to enable both of them.