Commit cd7a67ad authored by Phil Willoughby's avatar Phil Willoughby Committed by Facebook Github Bot

add a message propagation barrier

Summary:
Allows a category to be configured to selectively propagate messages to its parent category.

I preferred this over a simple `true`/`false` filter because it can be useful to have one log file for all DBG logs from a component while still limiting it to `INFO` or whatever in the main application log file.

Reviewed By: simpkins

Differential Revision: D18360804

fbshipit-source-id: a45098c81fdc8b984e440748880e8943df95acd2
parent 9feea971
......@@ -115,12 +115,9 @@ void LogCategory::processMessage(const LogMessage& message) const {
}
// Propagate the message up to our parent LogCategory.
//
// Maybe in the future it might be worth adding a flag to control if a
// LogCategory should propagate messages to its parent or not. (This would
// be similar to log4j's "additivity" flag.)
// For now I don't have a strong use case for this.
if (parent_) {
if (parent_ &&
message.getLevel() >=
propagateLevelMessagesToParent_.load(std::memory_order_relaxed)) {
parent_->processMessage(message);
}
}
......@@ -170,6 +167,14 @@ void LogCategory::setLevel(LogLevel level, bool inherit) {
db_->setLevel(this, level, inherit);
}
void LogCategory::setPropagateLevelMessagesToParent(LogLevel level) {
propagateLevelMessagesToParent_.store(level, std::memory_order_relaxed);
}
LogLevel LogCategory::getPropagateLevelMessagesToParentRelaxed() {
return propagateLevelMessagesToParent_.load(std::memory_order_relaxed);
}
void LogCategory::setLevelLocked(LogLevel level, bool inherit) {
// Clamp the value to MIN_LEVEL and MAX_LEVEL.
//
......
......@@ -139,6 +139,24 @@ class LogCategory {
*/
void setLevel(LogLevel level, bool inherit = true);
/**
* Set which messages processed by this category will be propagated up to the
* parent category
*
* The default is `LogLevel::MIN_LEVEL` meaning that all messages will be
* passed to the parent. You can set this to any higher log level to prevent
* some messages being passed to the parent. `LogLevel::MAX_LEVEL` is a good
* choice if you've attached a Handler to this category and you don't want
* any of these logs to also appear in the parent's Handler.
*/
void setPropagateLevelMessagesToParent(LogLevel propagate);
/**
* Get which messages processed by this category will be processed by the
* parent category
*/
LogLevel getPropagateLevelMessagesToParentRelaxed();
/**
* Get the LoggerDB that this LogCategory belongs to.
*
......@@ -241,6 +259,15 @@ class LogCategory {
void updateEffectiveLevel(LogLevel newEffectiveLevel);
void parentLevelUpdated(LogLevel parentEffectiveLevel);
/**
* Which log messages processed at this category should propagate to the
* parent category. The usual case is `LogLevel::MIN_LEVEL` which means all
* messages will be propagated. `LogLevel::MAX_LEVEL` generally means that
* this category and its children are directed to different destinations
* and the user does not want the messages duplicated.
*/
std::atomic<LogLevel> propagateLevelMessagesToParent_{LogLevel::MIN_LEVEL};
/**
* The minimum log level of this category and all of its parents.
*/
......
......@@ -30,6 +30,7 @@ LogCategoryConfig::LogCategoryConfig(
bool LogCategoryConfig::operator==(const LogCategoryConfig& other) const {
return level == other.level &&
inheritParentLevel == other.inheritParentLevel &&
propagateLevelMessagesToParent == other.propagateLevelMessagesToParent &&
handlers == other.handlers;
}
......
......@@ -51,6 +51,11 @@ class LogCategoryConfig {
*/
bool inheritParentLevel{true};
/**
* Which messages at this category should propagate to its parent category.
*/
LogLevel propagateLevelMessagesToParent{LogLevel::MIN_LEVEL};
/**
* An optional list of LogHandler names to use for this category.
*
......
......@@ -145,6 +145,19 @@ LogCategoryConfig parseJsonCategoryConfig(
config.inheritParentLevel = inherit->asBool();
}
auto* propagate = value.get_ptr("propagate");
if (propagate) {
if (!parseJsonLevel(
*propagate, categoryName, config.propagateLevelMessagesToParent)) {
throw LogConfigParseError{to<string>(
"unexpected data type for propagate field of category \"",
categoryName,
"\": got ",
dynamicTypename(*propagate),
", expected a string or integer")};
}
}
auto* handlers = value.get_ptr("handlers");
if (handlers) {
if (!handlers->isArray()) {
......@@ -580,7 +593,8 @@ dynamic logConfigToDynamic(const LogHandlerConfig& config) {
dynamic logConfigToDynamic(const LogCategoryConfig& config) {
auto value = dynamic::object("level", logLevelToString(config.level))(
"inherit", config.inheritParentLevel);
"inherit", config.inheritParentLevel)(
"propagate", logLevelToString(config.propagateLevelMessagesToParent));
if (config.handlers.hasValue()) {
auto handlers = dynamic::array();
for (const auto& handlerName : config.handlers.value()) {
......
......@@ -178,6 +178,8 @@ LogConfig LoggerDB::getConfigImpl(bool includeAllCategories) const {
LogCategoryConfig categoryConfig(
levelInfo.first, levelInfo.second, handlerNames);
categoryConfig.propagateLevelMessagesToParent =
category->getPropagateLevelMessagesToParentRelaxed();
categoryConfigs.emplace(category->getName(), std::move(categoryConfig));
}
}
......@@ -381,6 +383,10 @@ void LoggerDB::updateConfig(const LogConfig& config) {
// Update the level settings
category->setLevelLocked(
entry.second.level, entry.second.inheritParentLevel);
// Update the propagation settings
category->setPropagateLevelMessagesToParent(
entry.second.propagateLevelMessagesToParent);
}
finishConfigUpdate(handlerInfo, &handlers, &oldToNewHandlerMap);
......
......@@ -36,7 +36,7 @@ In general the basic configuration syntax is convenient for controlling log
levels, and making minor log handler setting changes (such as controlling if
logging goes to stdout or stderr, and whether it is logged asynchronously or
not). However the JSON format is easier to use to describe more complicated
settings.
settings. Some features are not available in the basic configuration syntax.
Basic Configuration Syntax
......@@ -250,6 +250,13 @@ following fields:
This field is optional, and defaults to true if not present.
* `propagate`
This should be a a string or positive integer value specifying the minimum
log level of messages that should be propagated to the parent category.
This field is optional, and defaults to the minimum log level if not present.
Alternatively, the value for a log category may be a plain string or integer
instead of a JSON object, in which case case the string or integer is treated
as the log level for that category, with the inherit setting enabled.
......
......@@ -65,6 +65,9 @@ std::ostream& operator<<(std::ostream& os, const LogCategoryConfig& config) {
if (!config.inheritParentLevel) {
os << "!";
}
if (config.propagateLevelMessagesToParent > LogLevel::MIN_LEVEL) {
os << "[" << config.propagateLevelMessagesToParent << "]";
}
if (config.handlers.hasValue()) {
os << ":" << join(",", config.handlers.value());
}
......
......@@ -293,6 +293,40 @@ TEST(LogConfig, parseJson) {
Pair("folly", LogCategoryConfig{LogLevel::FATAL, false})));
EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
config = parseLogConfig(R"JSON({
"categories": {
".": { "level": "INFO" },
"folly": { "level": "DEBUG", "inherit": false, "propagate": "WARN" },
}
})JSON");
{
LogCategoryConfig expectedFolly{LogLevel::DBG, false};
expectedFolly.propagateLevelMessagesToParent = LogLevel::WARN;
EXPECT_THAT(
config.getCategoryConfigs(),
UnorderedElementsAre(
Pair("", LogCategoryConfig{LogLevel::INFO, true}),
Pair("folly", expectedFolly)));
}
EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
config = parseLogConfig(R"JSON({
"categories": {
".": { "level": "INFO" },
"folly": { "level": "DEBUG", "inherit": false, "propagate": 9 },
}
})JSON");
{
LogCategoryConfig expectedFolly{LogLevel::DBG, false};
expectedFolly.propagateLevelMessagesToParent = static_cast<LogLevel>(9);
EXPECT_THAT(
config.getCategoryConfigs(),
UnorderedElementsAre(
Pair("", LogCategoryConfig{LogLevel::INFO, true}),
Pair("folly", expectedFolly)));
}
EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
config = parseLogConfig(R"JSON({
"categories": {
"my.category": { "level": "INFO", "inherit": true },
......@@ -427,6 +461,25 @@ TEST(LogConfig, parseJsonErrors) {
LogConfigParseError,
R"(unexpected data type for level field of category "folly": )"
"got array, expected a string or integer");
input = R"JSON({
"categories": {
"folly": { "level": "INFO", "propagate": [], },
}
})JSON";
EXPECT_THROW_RE(
parseLogConfig(input),
LogConfigParseError,
R"(unexpected data type for propagate field of category "folly": )"
"got array, expected a string or integer");
input = R"JSON({
"categories": {
"folly": { "level": "INFO", "propagate": "FOO", },
}
})JSON";
EXPECT_THROW_RE(
parseLogConfig(input),
LogConfigParseError,
R"MSG(invalid log level "FOO" for category "folly")MSG");
input = R"JSON({
"categories": {
5: {}
......@@ -567,16 +620,19 @@ TEST(LogConfig, toJson) {
"" : {
"inherit" : true,
"level" : "ERR",
"handlers" : ["h1"]
"handlers" : ["h1"],
"propagate": "NONE"
},
"folly" : {
"inherit" : true,
"level" : "INFO",
"handlers" : []
"handlers" : [],
"propagate": "NONE"
},
"foo.bar" : {
"inherit" : false,
"level" : "FATAL"
"level" : "FATAL",
"propagate": "NONE"
}
},
"handlers" : {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment