logging: add LoggerDB::updateConfig() and resetConfig()
authorAdam Simpkins <simpkins@fb.com>
Thu, 30 Nov 2017 01:35:16 +0000 (17:35 -0800)
committerFacebook Github Bot <facebook-github-bot@users.noreply.github.com>
Thu, 30 Nov 2017 01:51:07 +0000 (17:51 -0800)
Summary: Add methods for applying config changes from a LogConfig object to the LoggerDB.

Reviewed By: bolinfest

Differential Revision: D6200564

fbshipit-source-id: a25eb99e84b2885bf6853e2222db0d7432a6c37b

CMakeLists.txt
folly/experimental/logging/LogCategory.cpp
folly/experimental/logging/LogCategory.h
folly/experimental/logging/LoggerDB.cpp
folly/experimental/logging/LoggerDB.h
folly/experimental/logging/test/ConfigUpdateTest.cpp [new file with mode: 0644]
folly/experimental/logging/test/TestLogHandler.cpp [new file with mode: 0644]
folly/experimental/logging/test/TestLogHandler.h

index 8669d26..ac34e7d 100755 (executable)
@@ -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
index 15f8a0d..5651572 100644 (file)
@@ -21,6 +21,7 @@
 #include <folly/ConstexprMath.h>
 #include <folly/ExceptionString.h>
 #include <folly/FileUtil.h>
+#include <folly/MapUtil.h>
 #include <folly/experimental/logging/LogHandler.h>
 #include <folly/experimental/logging/LogMessage.h>
 #include <folly/experimental/logging/LogName.h>
@@ -144,6 +145,23 @@ std::vector<std::shared_ptr<LogHandler>> LogCategory::getHandlers() const {
   return *(handlers_.rlock());
 }
 
+void LogCategory::replaceHandlers(
+    std::vector<std::shared_ptr<LogHandler>> handlers) {
+  return handlers_.wlock()->swap(handlers);
+}
+
+void LogCategory::updateHandlers(const std::unordered_map<
+                                 std::shared_ptr<LogHandler>,
+                                 std::shared_ptr<LogHandler>>& 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
index 565373a..e861252 100644 (file)
@@ -164,6 +164,26 @@ class LogCategory {
    */
   std::vector<std::shared_ptr<LogHandler>> getHandlers() const;
 
+  /**
+   * Replace the list of LogHandlers with a completely new list.
+   */
+  void replaceHandlers(std::vector<std::shared_ptr<LogHandler>> 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<LogHandler>,
+                      std::shared_ptr<LogHandler>>& handlerMap);
+
   /* Internal methods for use by other parts of the logging library code */
 
   /**
index c7a3270..2611e23 100644 (file)
@@ -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<HandlerInfo>::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<std::string>(
+          "unknown log handler type \"", entry.second.type, "\""));
+    }
+
+    // Check to see if there is an existing LogHandler with this name
+    std::shared_ptr<LogHandler> 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<LogHandler> 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<std::string>(
+            "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<HandlerInfo>::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<std::string, std::weak_ptr<LogHandler>> 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<std::shared_ptr<LogHandler>> LoggerDB::buildCategoryHandlerList(
+    const NewHandlerMap& handlerMap,
+    StringPiece categoryName,
+    const std::vector<std::string>& categoryHandlerNames) {
+  std::vector<std::shared_ptr<LogHandler>> 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<std::string>(
+          "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<std::shared_ptr<LogHandler>> 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) {
index d65324f..a0038b3 100644 (file)
@@ -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<std::string, std::shared_ptr<LogHandler>>;
+  using OldToNewHandlerMap = std::
+      unordered_map<std::shared_ptr<LogHandler>, std::shared_ptr<LogHandler>>;
+  void startConfigUpdate(
+      const Synchronized<HandlerInfo>::LockedPtr& handlerInfo,
+      const LogConfig& config,
+      NewHandlerMap* handlers,
+      OldToNewHandlerMap* oldToNewHandlerMap);
+  void finishConfigUpdate(
+      const Synchronized<HandlerInfo>::LockedPtr& handlerInfo,
+      NewHandlerMap* handlers,
+      OldToNewHandlerMap* oldToNewHandlerMap);
+  std::vector<std::shared_ptr<LogHandler>> buildCategoryHandlerList(
+      const NewHandlerMap& handlerMap,
+      StringPiece categoryName,
+      const std::vector<std::string>& 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> handlerInfo_;
 
diff --git a/folly/experimental/logging/test/ConfigUpdateTest.cpp b/folly/experimental/logging/test/ConfigUpdateTest.cpp
new file mode 100644 (file)
index 0000000..d0743ce
--- /dev/null
@@ -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 <folly/dynamic.h>
+#include <folly/experimental/logging/LogCategory.h>
+#include <folly/experimental/logging/LogConfig.h>
+#include <folly/experimental/logging/LogConfigParser.h>
+#include <folly/experimental/logging/LogHandlerFactory.h>
+#include <folly/experimental/logging/LoggerDB.h>
+#include <folly/experimental/logging/test/TestLogHandler.h>
+#include <folly/json.h>
+#include <folly/portability/GMock.h>
+#include <folly/portability/GTest.h>
+#include <folly/test/TestUtils.h>
+
+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<std::string, std::string> 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<LogHandler>& handler) {
+  auto configHandler = std::dynamic_pointer_cast<TestLogHandler>(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<TestLogHandlerFactory>("handlerA"));
+  db.registerHandlerFactory(
+      std::make_unique<TestLogHandlerFactory>("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<TestLogHandlerFactory>("handlerA"));
+  db.registerHandlerFactory(
+      std::make_unique<TestLogHandlerFactory>("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<TestLogHandler>(
+      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 (file)
index 0000000..b5e7e98
--- /dev/null
@@ -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 <folly/experimental/logging/test/TestLogHandler.h>
+
+#include <folly/MapUtil.h>
+
+namespace folly {
+
+std::shared_ptr<LogHandler> TestLogHandlerFactory::createHandler(
+    const Options& options) {
+  return std::make_shared<TestLogHandler>(LogHandlerConfig{type_, options});
+}
+
+std::shared_ptr<LogHandler> TestLogHandlerFactory::updateHandler(
+    const std::shared_ptr<LogHandler>& 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<TestLogHandler>(existingHandler);
+  if (!existing || !get_ptr(options, "reuse_handler")) {
+    return createHandler(options);
+  }
+
+  existing->setOptions(options);
+  return existing;
+}
+
+} // namespace folly
index 6fa2173..c799d31 100644 (file)
@@ -22,6 +22,7 @@
 
 #include <folly/experimental/logging/LogHandler.h>
 #include <folly/experimental/logging/LogHandlerConfig.h>
+#include <folly/experimental/logging/LogHandlerFactory.h>
 #include <folly/experimental/logging/LogMessage.h>
 
 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<std::pair<LogMessage, const LogCategory*>> messages_;
   uint64_t flushCount_{0};
   std::map<std::string, std::string> 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<LogHandler> createHandler(const Options& options) override;
+
+  std::shared_ptr<LogHandler> updateHandler(
+      const std::shared_ptr<LogHandler>& existingHandler,
+      const Options& options) override;
+
+ private:
+  std::string type_;
+};
+
 } // namespace folly