From: Adam Simpkins Date: Thu, 30 Nov 2017 01:35:16 +0000 (-0800) Subject: logging: add LoggerDB::updateConfig() and resetConfig() X-Git-Tag: v2017.12.04.00~17 X-Git-Url: http://demsky.eecs.uci.edu/git/?a=commitdiff_plain;h=01d4b7d6db80480e2977fcfa35775506ca5de41f;p=folly.git logging: add LoggerDB::updateConfig() and resetConfig() Summary: Add methods for applying config changes from a LogConfig object to the LoggerDB. Reviewed By: bolinfest Differential Revision: D6200564 fbshipit-source-id: a25eb99e84b2885bf6853e2222db0d7432a6c37b --- diff --git a/CMakeLists.txt b/CMakeLists.txt index 8669d265..ac34e7d4 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -289,6 +289,7 @@ if (BUILD_TESTS) ${FOLLY_DIR}/test/SingletonTestStructs.cpp ${FOLLY_DIR}/test/SocketAddressTestHelper.cpp ${FOLLY_DIR}/test/SocketAddressTestHelper.h + ${FOLLY_DIR}/experimental/logging/test/TestLogHandler.cpp ${FOLLY_DIR}/experimental/logging/test/TestLogHandler.h ${FOLLY_DIR}/futures/test/TestExecutor.cpp ${FOLLY_DIR}/futures/test/TestExecutor.h @@ -386,6 +387,7 @@ if (BUILD_TESTS) DIRECTORY experimental/logging/test/ TEST async_file_writer_test SOURCES AsyncFileWriterTest.cpp TEST config_parser_test SOURCES ConfigParserTest.cpp + TEST config_update_test SOURCES ConfigUpdateTest.cpp TEST file_handler_factory_test SOURCES FileHandlerFactoryTest.cpp TEST glog_formatter_test SOURCES GlogFormatterTest.cpp TEST immediate_file_writer_test SOURCES ImmediateFileWriterTest.cpp diff --git a/folly/experimental/logging/LogCategory.cpp b/folly/experimental/logging/LogCategory.cpp index 15f8a0dd..56515725 100644 --- a/folly/experimental/logging/LogCategory.cpp +++ b/folly/experimental/logging/LogCategory.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -144,6 +145,23 @@ std::vector> LogCategory::getHandlers() const { return *(handlers_.rlock()); } +void LogCategory::replaceHandlers( + std::vector> handlers) { + return handlers_.wlock()->swap(handlers); +} + +void LogCategory::updateHandlers(const std::unordered_map< + std::shared_ptr, + std::shared_ptr>& handlerMap) { + auto handlers = handlers_.wlock(); + for (auto& entry : *handlers) { + auto* ptr = get_ptr(handlerMap, entry); + if (ptr) { + entry = *ptr; + } + } +} + void LogCategory::setLevel(LogLevel level, bool inherit) { // We have to set the level through LoggerDB, since we require holding // the LoggerDB lock to iterate through our children in case our effective diff --git a/folly/experimental/logging/LogCategory.h b/folly/experimental/logging/LogCategory.h index 565373a7..e8612526 100644 --- a/folly/experimental/logging/LogCategory.h +++ b/folly/experimental/logging/LogCategory.h @@ -164,6 +164,26 @@ class LogCategory { */ std::vector> getHandlers() const; + /** + * Replace the list of LogHandlers with a completely new list. + */ + void replaceHandlers(std::vector> handlers); + + /** + * Update the LogHandlers attached to this LogCategory by replacing + * currently attached handlers with new LogHandler objects. + * + * The handlerMap argument is a map of (old_handler -> new_handler) + * If any of the LogHandlers currently attached to this category are found in + * the handlerMap, replace them with the new handler indicated in the map. + * + * This is used when the LogHandler configuration is changed requiring one or + * more LogHandler objects to be replaced with new ones. + */ + void updateHandlers(const std::unordered_map< + std::shared_ptr, + std::shared_ptr>& handlerMap); + /* Internal methods for use by other parts of the logging library code */ /** diff --git a/folly/experimental/logging/LoggerDB.cpp b/folly/experimental/logging/LoggerDB.cpp index c7a32705..2611e23c 100644 --- a/folly/experimental/logging/LoggerDB.cpp +++ b/folly/experimental/logging/LoggerDB.cpp @@ -208,6 +208,229 @@ LogConfig LoggerDB::getConfig() const { return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)}; } +/** + * Process handler config information when starting a config update operation. + */ +void LoggerDB::startConfigUpdate( + const Synchronized::LockedPtr& handlerInfo, + const LogConfig& config, + NewHandlerMap* handlers, + OldToNewHandlerMap* oldToNewHandlerMap) { + // Get a map of all currently existing LogHandlers. + // This resolves weak_ptrs to shared_ptrs, and ignores expired weak_ptrs. + // This prevents any of these LogHandler pointers from expiring during the + // config update. + for (const auto& entry : handlerInfo->handlers) { + auto handler = entry.second.lock(); + if (handler) { + handlers->emplace(entry.first, std::move(handler)); + } + } + + // Create all of the new LogHandlers needed from this configuration + for (const auto& entry : config.getHandlerConfigs()) { + // Look up the LogHandlerFactory + auto factoryIter = handlerInfo->factories.find(entry.second.type); + if (factoryIter == handlerInfo->factories.end()) { + throw std::invalid_argument(to( + "unknown log handler type \"", entry.second.type, "\"")); + } + + // Check to see if there is an existing LogHandler with this name + std::shared_ptr oldHandler; + auto iter = handlers->find(entry.first); + if (iter != handlers->end()) { + oldHandler = iter->second; + } + + // Create the new log handler + const auto& factory = factoryIter->second; + std::shared_ptr handler; + if (oldHandler) { + handler = factory->updateHandler(oldHandler, entry.second.options); + if (handler != oldHandler) { + oldToNewHandlerMap->emplace(oldHandler, handler); + } + } else { + handler = factory->createHandler(entry.second.options); + } + handlerInfo->handlers[entry.first] = handler; + (*handlers)[entry.first] = handler; + } + + // Before we start making any LogCategory changes, confirm that all handlers + // named in the category configs are known handlers. + for (const auto& entry : config.getCategoryConfigs()) { + if (!entry.second.handlers.hasValue()) { + continue; + } + for (const auto& handlerName : entry.second.handlers.value()) { + auto iter = handlers->find(handlerName); + if (iter == handlers->end()) { + throw std::invalid_argument(to( + "unknown log handler \"", + handlerName, + "\" configured for log category \"", + entry.first, + "\"")); + } + } + } +} + +/** + * Update handlerInfo_ at the end of a config update operation. + */ +void LoggerDB::finishConfigUpdate( + const Synchronized::LockedPtr& handlerInfo, + NewHandlerMap* handlers, + OldToNewHandlerMap* oldToNewHandlerMap) { + // Build a new map to replace handlerInfo->handlers + // This will contain only the LogHandlers that are still in use by the + // current LogCategory settings. + std::unordered_map> newHandlerMap; + for (const auto& entry : *handlers) { + newHandlerMap.emplace(entry.first, entry.second); + } + // Drop all of our shared_ptr references to LogHandler objects, + // and then remove entries in newHandlerMap that are unreferenced. + handlers->clear(); + oldToNewHandlerMap->clear(); + handlerInfo->handlers.clear(); + for (auto iter = newHandlerMap.begin(); iter != newHandlerMap.end(); /**/) { + if (iter->second.expired()) { + iter = newHandlerMap.erase(iter); + } else { + ++iter; + } + } + handlerInfo->handlers.swap(newHandlerMap); +} + +std::vector> LoggerDB::buildCategoryHandlerList( + const NewHandlerMap& handlerMap, + StringPiece categoryName, + const std::vector& categoryHandlerNames) { + std::vector> catHandlers; + for (const auto& handlerName : categoryHandlerNames) { + auto iter = handlerMap.find(handlerName); + if (iter == handlerMap.end()) { + // This really shouldn't be possible; the checks in startConfigUpdate() + // should have already bailed out if there was an unknown handler. + throw std::invalid_argument(to( + "bug: unknown log handler \"", + handlerName, + "\" configured for log category \"", + categoryName, + "\"")); + } + catHandlers.push_back(iter->second); + } + + return catHandlers; +} + +void LoggerDB::updateConfig(const LogConfig& config) { + // Grab the handlerInfo_ lock. + // We hold it in write mode for the entire config update operation. This + // ensures that only a single config update operation ever runs at once. + auto handlerInfo = handlerInfo_.wlock(); + + NewHandlerMap handlers; + OldToNewHandlerMap oldToNewHandlerMap; + startConfigUpdate(handlerInfo, config, &handlers, &oldToNewHandlerMap); + + // If an existing LogHandler was replaced with a new one, + // walk all current LogCategories and replace this handler. + if (!oldToNewHandlerMap.empty()) { + auto loggerMap = loggersByName_.rlock(); + for (const auto& entry : *loggerMap) { + entry.second->updateHandlers(oldToNewHandlerMap); + } + } + + // Update log levels and handlers mentioned in the config update + auto loggersByName = loggersByName_.wlock(); + for (const auto& entry : config.getCategoryConfigs()) { + LogCategory* category = + getOrCreateCategoryLocked(*loggersByName, entry.first); + + // Update the log handlers + if (entry.second.handlers.hasValue()) { + auto catHandlers = buildCategoryHandlerList( + handlers, entry.first, entry.second.handlers.value()); + category->replaceHandlers(std::move(catHandlers)); + } + + // Update the level settings + category->setLevelLocked( + entry.second.level, entry.second.inheritParentLevel); + } + + finishConfigUpdate(handlerInfo, &handlers, &oldToNewHandlerMap); +} + +void LoggerDB::resetConfig(const LogConfig& config) { + // Grab the handlerInfo_ lock. + // We hold it in write mode for the entire config update operation. This + // ensures that only a single config update operation ever runs at once. + auto handlerInfo = handlerInfo_.wlock(); + + NewHandlerMap handlers; + OldToNewHandlerMap oldToNewHandlerMap; + startConfigUpdate(handlerInfo, config, &handlers, &oldToNewHandlerMap); + + // Make sure all log categories mentioned in the new config exist. + // This ensures that we will cover them in our walk below. + LogCategory* rootCategory; + { + auto loggersByName = loggersByName_.wlock(); + rootCategory = getOrCreateCategoryLocked(*loggersByName, ""); + for (const auto& entry : config.getCategoryConfigs()) { + getOrCreateCategoryLocked(*loggersByName, entry.first); + } + } + + { + // Update all log categories + auto loggersByName = loggersByName_.rlock(); + for (const auto& entry : *loggersByName) { + auto* category = entry.second.get(); + + auto configIter = config.getCategoryConfigs().find(category->getName()); + if (configIter == config.getCategoryConfigs().end()) { + // This category is not listed in the config settings. + // Reset it to the default settings. + category->clearHandlers(); + + if (category == rootCategory) { + category->setLevelLocked(LogLevel::ERR, false); + } else { + category->setLevelLocked(LogLevel::MAX_LEVEL, true); + } + continue; + } + + const auto& catConfig = configIter->second; + + // Update the category log level + category->setLevelLocked(catConfig.level, catConfig.inheritParentLevel); + + // Update the category handlers list. + // If the handler list is not set in the config, clear out any existing + // handlers rather than leaving it as-is. + std::vector> catHandlers; + if (catConfig.handlers.hasValue()) { + catHandlers = buildCategoryHandlerList( + handlers, entry.first, catConfig.handlers.value()); + } + category->replaceHandlers(std::move(catHandlers)); + } + } + + finishConfigUpdate(handlerInfo, &handlers, &oldToNewHandlerMap); +} + LogCategory* LoggerDB::getOrCreateCategoryLocked( LoggerNameMap& loggersByName, StringPiece name) { diff --git a/folly/experimental/logging/LoggerDB.h b/folly/experimental/logging/LoggerDB.h index d65324f7..a0038b3f 100644 --- a/folly/experimental/logging/LoggerDB.h +++ b/folly/experimental/logging/LoggerDB.h @@ -88,6 +88,29 @@ class LoggerDB { */ LogConfig getConfig() const; + /** + * Update the current LoggerDB state with the specified LogConfig settings. + * + * Log categories and handlers listed in the LogConfig object will be updated + * to the new state listed in the LogConfig. Settings on categories and + * handlers not listed in the config will be left as-is. + */ + void updateConfig(const LogConfig& config); + + /** + * Reset the current LoggerDB state to the specified LogConfig settings. + * + * All LogCategories not mentioned in the new LogConfig will have all + * currently configured log handlers removed and their log level set to its + * default state. For the root category the default log level is ERR; for + * all other categories the default level is MAX_LEVEL with log level + * inheritance enabled. + * + * LogCategories listed in the new config but without LogHandler information + * defined will have all existing handlers removed. + */ + void resetConfig(const LogConfig& config); + /** * Apply a configuration string specifying a series a log levels. * @@ -237,6 +260,24 @@ class LoggerDB { folly::StringPiece name, LogCategory* parent); + using NewHandlerMap = + std::unordered_map>; + using OldToNewHandlerMap = std:: + unordered_map, std::shared_ptr>; + void startConfigUpdate( + const Synchronized::LockedPtr& handlerInfo, + const LogConfig& config, + NewHandlerMap* handlers, + OldToNewHandlerMap* oldToNewHandlerMap); + void finishConfigUpdate( + const Synchronized::LockedPtr& handlerInfo, + NewHandlerMap* handlers, + OldToNewHandlerMap* oldToNewHandlerMap); + std::vector> buildCategoryHandlerList( + const NewHandlerMap& handlerMap, + StringPiece categoryName, + const std::vector& categoryHandlerNames); + static void internalWarningImpl( folly::StringPiece filename, int lineNumber, @@ -256,6 +297,9 @@ class LoggerDB { /** * The LogHandlers and LogHandlerFactories. + * + * For lock ordering purposes, if you need to acquire both the loggersByName_ + * and handlerInfo_ locks, the handlerInfo_ lock must be acquired first. */ folly::Synchronized handlerInfo_; diff --git a/folly/experimental/logging/test/ConfigUpdateTest.cpp b/folly/experimental/logging/test/ConfigUpdateTest.cpp new file mode 100644 index 00000000..d0743ced --- /dev/null +++ b/folly/experimental/logging/test/ConfigUpdateTest.cpp @@ -0,0 +1,319 @@ +/* + * Copyright 2004-present Facebook, Inc. + * + * Licensed 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. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace folly; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; + +namespace { + +MATCHER_P(LogHandlerMatcherImpl, config, "") { + return arg->getConfig() == config; +} + +/** + * A helper function to use in EXPECT_THAT() for matching a TestLogHandler + * with the specified type and options. + */ +auto MatchLogHandler( + StringPiece type, + std::unordered_map options) { + return LogHandlerMatcherImpl(LogHandlerConfig{type, std::move(options)}); +} +auto MatchLogHandler(const LogHandlerConfig& config) { + return LogHandlerMatcherImpl(config); +} + +} // namespace + +namespace folly { +/** + * Print TestLogHandler objects nicely in test failure messages + */ +std::ostream& operator<<( + std::ostream& os, + const std::shared_ptr& handler) { + auto configHandler = std::dynamic_pointer_cast(handler); + if (!configHandler) { + os << "unknown handler type"; + return os; + } + + auto config = configHandler->getConfig(); + os << "ConfigHandler(" << config.type; + for (const auto& entry : config.options) { + os << ", " << entry.first << "=" << entry.second; + } + os << ")"; + return os; +} + +std::ostream& operator<<(std::ostream& os, const LogConfig& config) { + os << toPrettyJson(logConfigToDynamic(config)); + return os; +} + +std::ostream& operator<<(std::ostream& os, const LogHandlerConfig& config) { + os << toPrettyJson(logConfigToDynamic(config)); + return os; +} +} // namespace folly + +TEST(ConfigUpdate, updateLogLevels) { + LoggerDB db{LoggerDB::TESTING}; + db.updateConfig(parseLogConfig("foo.bar=dbg5")); + EXPECT_EQ(LogLevel::DBG5, db.getCategory("foo.bar")->getLevel()); + EXPECT_EQ(LogLevel::DBG5, db.getCategory("foo.bar")->getEffectiveLevel()); + EXPECT_EQ(LogLevel::MAX_LEVEL, db.getCategory("foo")->getLevel()); + EXPECT_EQ(LogLevel::ERR, db.getCategory("foo")->getEffectiveLevel()); + EXPECT_EQ(LogLevel::ERR, db.getCategory("")->getLevel()); + EXPECT_EQ(LogLevel::ERR, db.getCategory("")->getEffectiveLevel()); + + EXPECT_EQ(LogLevel::MAX_LEVEL, db.getCategory("foo.bar.test")->getLevel()); + EXPECT_EQ( + LogLevel::DBG5, db.getCategory("foo.bar.test")->getEffectiveLevel()); + + db.updateConfig( + parseLogConfig("sys=warn,foo.test=debug,foo.test.stuff=warn")); + EXPECT_EQ(LogLevel::WARN, db.getCategory("sys")->getLevel()); + EXPECT_EQ(LogLevel::WARN, db.getCategory("sys")->getEffectiveLevel()); + EXPECT_EQ(LogLevel::DEBUG, db.getCategory("foo.test")->getLevel()); + EXPECT_EQ(LogLevel::DEBUG, db.getCategory("foo.test")->getEffectiveLevel()); + EXPECT_EQ(LogLevel::WARN, db.getCategory("foo.test.stuff")->getLevel()); + EXPECT_EQ( + LogLevel::DEBUG, db.getCategory("foo.test.stuff")->getEffectiveLevel()); + EXPECT_EQ(LogLevel::DBG5, db.getCategory("foo.bar")->getEffectiveLevel()); +} + +TEST(ConfigUpdate, updateConfig) { + LoggerDB db{LoggerDB::TESTING}; + db.registerHandlerFactory( + std::make_unique("handlerA")); + db.registerHandlerFactory( + std::make_unique("handlerB")); + EXPECT_EQ(parseLogConfig(".:=ERROR:"), db.getConfig()); + + // Create some categories that aren't affected by our config updates below, + // just to ensure that they don't show up in getConfig() results since they + // have the default config settings. + db.getCategory("test.category1"); + db.getCategory("test.category2"); + EXPECT_EQ(parseLogConfig(".:=ERROR:"), db.getConfig()); + + // Apply an update + db.updateConfig(parseLogConfig("INFO:stderr; stderr=handlerA,stream=stderr")); + EXPECT_EQ(LogLevel::INFO, db.getCategory("")->getLevel()); + EXPECT_THAT( + db.getCategory("")->getHandlers(), + UnorderedElementsAre( + MatchLogHandler("handlerA", {{"stream", "stderr"}}))); + EXPECT_EQ( + parseLogConfig(".:=INFO:stderr; stderr=handlerA,stream=stderr"), + db.getConfig()); + + // Update the log level for category "foo" + // This should not affect the existing settings for the root category + EXPECT_EQ(LogLevel::MAX_LEVEL, db.getCategory("foo")->getLevel()); + EXPECT_EQ(true, db.getCategory("foo")->getLevelInfo().second); + db.updateConfig(parseLogConfig("foo:=DBG2")); + EXPECT_EQ(LogLevel::DBG2, db.getCategory("foo")->getLevel()); + EXPECT_EQ(false, db.getCategory("foo")->getLevelInfo().second); + EXPECT_EQ(LogLevel::INFO, db.getCategory("")->getLevel()); + EXPECT_EQ(1, db.getCategory("")->getHandlers().size()); + EXPECT_EQ( + parseLogConfig( + ".:=INFO:stderr, foo:=DBG2:; stderr=handlerA,stream=stderr"), + db.getConfig()); + + // Add 2 log handlers to the "bar" log category. + db.updateConfig( + parseLogConfig("bar=ERROR:new:h2; " + "new=handlerB,key=value; " + "h2=handlerA,foo=bar")); + EXPECT_EQ(LogLevel::INFO, db.getCategory("")->getLevel()); + EXPECT_THAT( + db.getCategory("")->getHandlers(), + UnorderedElementsAre( + MatchLogHandler("handlerA", {{"stream", "stderr"}}))); + EXPECT_EQ(LogLevel::ERR, db.getCategory("bar")->getLevel()); + EXPECT_THAT( + db.getCategory("bar")->getHandlers(), + UnorderedElementsAre( + MatchLogHandler("handlerB", {{"key", "value"}}), + MatchLogHandler("handlerA", {{"foo", "bar"}}))); + EXPECT_EQ( + parseLogConfig(".:=INFO:stderr, foo:=DBG2:, bar=ERROR:new:h2; " + "stderr=handlerA,stream=stderr; " + "new=handlerB,key=value; " + "h2=handlerA,foo=bar"), + db.getConfig()); + + // Updating the "new" log handler settings should automatically update + // the settings we see on the "bar" category, even if we don't explicitly + // list "bar" in the config update + db.updateConfig(parseLogConfig("; new=handlerB,newkey=newvalue")); + EXPECT_EQ(LogLevel::INFO, db.getCategory("")->getLevel()); + EXPECT_THAT( + db.getCategory("")->getHandlers(), + UnorderedElementsAre( + MatchLogHandler("handlerA", {{"stream", "stderr"}}))); + EXPECT_EQ(LogLevel::ERR, db.getCategory("bar")->getLevel()); + EXPECT_THAT( + db.getCategory("bar")->getHandlers(), + UnorderedElementsAre( + MatchLogHandler("handlerB", {{"newkey", "newvalue"}}), + MatchLogHandler("handlerA", {{"foo", "bar"}}))); + EXPECT_EQ( + parseLogConfig(".:=INFO:stderr, foo:=DBG2:, bar=ERROR:new:h2; " + "stderr=handlerA,stream=stderr; " + "new=handlerB,newkey=newvalue; " + "h2=handlerA,foo=bar"), + db.getConfig()); + + // Updating the level settings for the "bar" handler should leave its + // handlers unchanged. + db.updateConfig(parseLogConfig("bar=WARN")); + EXPECT_EQ(LogLevel::INFO, db.getCategory("")->getLevel()); + EXPECT_THAT( + db.getCategory("")->getHandlers(), + UnorderedElementsAre( + MatchLogHandler("handlerA", {{"stream", "stderr"}}))); + EXPECT_EQ(LogLevel::WARN, db.getCategory("bar")->getLevel()); + EXPECT_THAT( + db.getCategory("bar")->getHandlers(), + UnorderedElementsAre( + MatchLogHandler("handlerB", {{"newkey", "newvalue"}}), + MatchLogHandler("handlerA", {{"foo", "bar"}}))); + EXPECT_EQ( + parseLogConfig(".:=INFO:stderr, foo:=DBG2:, bar=WARN:new:h2; " + "stderr=handlerA,stream=stderr; " + "new=handlerB,newkey=newvalue; " + "h2=handlerA,foo=bar"), + db.getConfig()); + + // Update the options for the h2 handler in place, and also add it to the + // "test.foo" category. The changes should also be reflected on the "bar" + // category. + db.updateConfig( + parseLogConfig("test.foo=INFO:h2; h2=handlerA,reuse_handler=1,foo=xyz")); + EXPECT_EQ(LogLevel::INFO, db.getCategory("")->getLevel()); + EXPECT_THAT( + db.getCategory("")->getHandlers(), + UnorderedElementsAre( + MatchLogHandler("handlerA", {{"stream", "stderr"}}))); + EXPECT_EQ(LogLevel::WARN, db.getCategory("bar")->getLevel()); + EXPECT_THAT( + db.getCategory("bar")->getHandlers(), + UnorderedElementsAre( + MatchLogHandler("handlerB", {{"newkey", "newvalue"}}), + MatchLogHandler( + "handlerA", {{"foo", "xyz"}, {"reuse_handler", "1"}}))); + EXPECT_EQ(LogLevel::INFO, db.getCategory("test.foo")->getLevel()); + EXPECT_THAT( + db.getCategory("test.foo")->getHandlers(), + UnorderedElementsAre(MatchLogHandler( + "handlerA", {{"foo", "xyz"}, {"reuse_handler", "1"}}))); + EXPECT_EQ( + parseLogConfig(".:=INFO:stderr, foo:=DBG2:, bar=WARN:new:h2, " + "test.foo=INFO:h2; " + "stderr=handlerA,stream=stderr; " + "new=handlerB,newkey=newvalue; " + "h2=handlerA,reuse_handler=1,foo=xyz"), + db.getConfig()); + + // Explicitly clear the handlers for the "bar" category + // This should remove the "new" handler from the LoggerDB since bar was the + // only category referring to it. + db.updateConfig(parseLogConfig("bar=WARN:")); + EXPECT_EQ(LogLevel::INFO, db.getCategory("")->getLevel()); + EXPECT_THAT( + db.getCategory("")->getHandlers(), + UnorderedElementsAre( + MatchLogHandler("handlerA", {{"stream", "stderr"}}))); + EXPECT_EQ(LogLevel::WARN, db.getCategory("bar")->getLevel()); + EXPECT_THAT(db.getCategory("bar")->getHandlers(), UnorderedElementsAre()); + EXPECT_EQ( + parseLogConfig(".:=INFO:stderr, foo:=DBG2:, bar=WARN:, " + "test.foo=INFO:h2; " + "stderr=handlerA,stream=stderr; " + "h2=handlerA,reuse_handler=1,foo=xyz"), + db.getConfig()); + + // Now test resetConfig() + db.resetConfig( + parseLogConfig("bar=INFO:h2, test.abc=DBG3; " + "h2=handlerB,abc=xyz")); + EXPECT_EQ( + parseLogConfig(".:=ERR:, bar=INFO:h2, test.abc=DBG3:; " + "h2=handlerB,abc=xyz"), + db.getConfig()); +} + +TEST(ConfigUpdate, getConfigAnonymousHandlers) { + LoggerDB db{LoggerDB::TESTING}; + db.registerHandlerFactory( + std::make_unique("handlerA")); + db.registerHandlerFactory( + std::make_unique("handlerB")); + EXPECT_EQ(parseLogConfig(".:=ERROR:"), db.getConfig()); + + // Manually attach a handler to a category. + // It should be reported as "anonymousHandler1" + auto handlerFoo = std::make_shared( + LogHandlerConfig{"foo", {{"abc", "xyz"}}}); + db.setLevel("x.y.z", LogLevel::DBG2); + db.getCategory("x.y.z")->addHandler(handlerFoo); + EXPECT_EQ( + parseLogConfig(".:=ERR:, x.y.z=DBG2:anonymousHandler1; " + "anonymousHandler1=foo,abc=xyz"), + db.getConfig()); + + // If we attach the same handler to another category it should still only be + // reported once. + db.setLevel("test.category", LogLevel::DBG1); + db.getCategory("test.category")->addHandler(handlerFoo); + EXPECT_EQ( + parseLogConfig(".:=ERR:, " + "x.y.z=DBG2:anonymousHandler1, " + "test.category=DBG1:anonymousHandler1; " + "anonymousHandler1=foo,abc=xyz"), + db.getConfig()); + + // If we use updateConfig() to explicitly define a handler named + // "anonymousHandler1", the unnamed handler will be reported as + // "anonymousHandler2" instead now. + db.updateConfig(parseLogConfig( + "a.b.c=INFO:anonymousHandler1; anonymousHandler1=handlerA,key=value")); + EXPECT_EQ( + parseLogConfig(".:=ERR:, " + "a.b.c=INFO:anonymousHandler1, " + "x.y.z=DBG2:anonymousHandler2, " + "test.category=DBG1:anonymousHandler2; " + "anonymousHandler1=handlerA,key=value; " + "anonymousHandler2=foo,abc=xyz"), + db.getConfig()); +} diff --git a/folly/experimental/logging/test/TestLogHandler.cpp b/folly/experimental/logging/test/TestLogHandler.cpp new file mode 100644 index 00000000..b5e7e98f --- /dev/null +++ b/folly/experimental/logging/test/TestLogHandler.cpp @@ -0,0 +1,41 @@ +/* + * Copyright 2004-present Facebook, Inc. + * + * Licensed 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. + */ +#include + +#include + +namespace folly { + +std::shared_ptr TestLogHandlerFactory::createHandler( + const Options& options) { + return std::make_shared(LogHandlerConfig{type_, options}); +} + +std::shared_ptr TestLogHandlerFactory::updateHandler( + const std::shared_ptr& existingHandler, + const Options& options) { + // Only re-use an existing handler in-place if it is a TestLogHandler + // and if the new options contain reuse_handler + auto existing = std::dynamic_pointer_cast(existingHandler); + if (!existing || !get_ptr(options, "reuse_handler")) { + return createHandler(options); + } + + existing->setOptions(options); + return existing; +} + +} // namespace folly diff --git a/folly/experimental/logging/test/TestLogHandler.h b/folly/experimental/logging/test/TestLogHandler.h index 6fa21733..c799d316 100644 --- a/folly/experimental/logging/test/TestLogHandler.h +++ b/folly/experimental/logging/test/TestLogHandler.h @@ -22,6 +22,7 @@ #include #include +#include #include namespace folly { @@ -34,6 +35,8 @@ namespace folly { */ class TestLogHandler : public LogHandler { public: + using Options = LogHandlerConfig::Options; + TestLogHandler() : config_{"test"} {} explicit TestLogHandler(LogHandlerConfig config) : config_{std::move(config)} {} @@ -60,10 +63,36 @@ class TestLogHandler : public LogHandler { return config_; } - private: + void setOptions(const Options& options) { + config_.options = options; + } + + protected: std::vector> messages_; uint64_t flushCount_{0}; std::map options_; LogHandlerConfig config_; }; + +/** + * A LogHandlerFactory to create TestLogHandler objects. + */ +class TestLogHandlerFactory : public LogHandlerFactory { + public: + explicit TestLogHandlerFactory(StringPiece type) : type_{type.str()} {} + + StringPiece getType() const override { + return type_; + } + + std::shared_ptr createHandler(const Options& options) override; + + std::shared_ptr updateHandler( + const std::shared_ptr& existingHandler, + const Options& options) override; + + private: + std::string type_; +}; + } // namespace folly