logging: add a LogConfig class and parsing code
authorAdam Simpkins <simpkins@fb.com>
Tue, 21 Nov 2017 18:44:02 +0000 (10:44 -0800)
committerFacebook Github Bot <facebook-github-bot@users.noreply.github.com>
Tue, 21 Nov 2017 18:51:16 +0000 (10:51 -0800)
Summary:
Add a new LogConfig class to represent the current configuration state of the
LoggerDB.  This also includes code to parse config strings in both JSON and a
simpler more human-writable format (intended primarily for use in command line
arguments).

I generally expect the human-writable format to be used mainly to configure log
levels.  It also supports configuring log handler details as well, but the
format for this data is slightly more cumbersome and will probably be harder
for people to easily remember.

The parsing code is intentionally kept as part of the 'init' library rather
than the core 'logging' library so that other libraries that simply wish to log
messages do not need to depend on it.  For instance, this would allow the folly
JSON library to use the logging library without causing a circular dependency.

Reviewed By: bolinfest

Differential Revision: D6200560

fbshipit-source-id: e4e3c7f941808251b6c7bcbbdac0210118675fb0

13 files changed:
CMakeLists.txt
folly/Makefile.am
folly/experimental/logging/LogCategoryConfig.cpp [new file with mode: 0644]
folly/experimental/logging/LogCategoryConfig.h [new file with mode: 0644]
folly/experimental/logging/LogConfig.cpp [new file with mode: 0644]
folly/experimental/logging/LogConfig.h [new file with mode: 0644]
folly/experimental/logging/LogConfigParser.cpp [new file with mode: 0644]
folly/experimental/logging/LogConfigParser.h [new file with mode: 0644]
folly/experimental/logging/LogHandlerConfig.cpp [new file with mode: 0644]
folly/experimental/logging/LogHandlerConfig.h [new file with mode: 0644]
folly/experimental/logging/Makefile.am
folly/experimental/logging/docs/Config.md [new file with mode: 0644]
folly/experimental/logging/test/ConfigParserTest.cpp [new file with mode: 0644]

index 3d7c1533af161c31f96ef9e087e57ad4610108f6..ec5a985cc91ccfc00c2cf04fd069d186b1a719dc 100755 (executable)
@@ -385,6 +385,7 @@ if (BUILD_TESTS)
 
     DIRECTORY experimental/logging/test/
       TEST async_file_writer_test SOURCES AsyncFileWriterTest.cpp
 
     DIRECTORY experimental/logging/test/
       TEST async_file_writer_test SOURCES AsyncFileWriterTest.cpp
+      TEST config_parser_test SOURCES ConfigParserTest.cpp
       TEST glog_formatter_test SOURCES GlogFormatterTest.cpp
       TEST immediate_file_writer_test SOURCES ImmediateFileWriterTest.cpp
       TEST log_category_test SOURCES LogCategoryTest.cpp
       TEST glog_formatter_test SOURCES GlogFormatterTest.cpp
       TEST immediate_file_writer_test SOURCES ImmediateFileWriterTest.cpp
       TEST log_category_test SOURCES LogCategoryTest.cpp
index 2bf6bae77d18166229d4bff3dfc00288778458d9..c046a8df3cfa33be21a332a376c5eec5446bff6a 100644 (file)
@@ -160,10 +160,14 @@ nobase_follyinclude_HEADERS = \
        experimental/logging/ImmediateFileWriter.h \
        experimental/logging/Init.h \
        experimental/logging/LogCategory.h \
        experimental/logging/ImmediateFileWriter.h \
        experimental/logging/Init.h \
        experimental/logging/LogCategory.h \
+       experimental/logging/LogCategoryConfig.h \
+       experimental/logging/LogConfig.h \
+       experimental/logging/LogConfigParser.h \
        experimental/logging/LogFormatter.h \
        experimental/logging/Logger.h \
        experimental/logging/LoggerDB.h \
        experimental/logging/LogHandler.h \
        experimental/logging/LogFormatter.h \
        experimental/logging/Logger.h \
        experimental/logging/LoggerDB.h \
        experimental/logging/LogHandler.h \
+       experimental/logging/LogHandlerConfig.h \
        experimental/logging/LogLevel.h \
        experimental/logging/LogMessage.h \
        experimental/logging/LogName.h \
        experimental/logging/LogLevel.h \
        experimental/logging/LogMessage.h \
        experimental/logging/LogName.h \
diff --git a/folly/experimental/logging/LogCategoryConfig.cpp b/folly/experimental/logging/LogCategoryConfig.cpp
new file mode 100644 (file)
index 0000000..b100445
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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/LogCategoryConfig.h>
+
+namespace folly {
+
+LogCategoryConfig::LogCategoryConfig(LogLevel l, bool inherit)
+    : level{l}, inheritParentLevel{inherit} {}
+
+LogCategoryConfig::LogCategoryConfig(
+    LogLevel l,
+    bool inherit,
+    std::vector<std::string> h)
+    : level{l}, inheritParentLevel{inherit}, handlers{h} {}
+
+bool LogCategoryConfig::operator==(const LogCategoryConfig& other) const {
+  return level == other.level &&
+      inheritParentLevel == other.inheritParentLevel &&
+      handlers == other.handlers;
+}
+
+bool LogCategoryConfig::operator!=(const LogCategoryConfig& other) const {
+  return !(*this == other);
+}
+
+} // namespace folly
diff --git a/folly/experimental/logging/LogCategoryConfig.h b/folly/experimental/logging/LogCategoryConfig.h
new file mode 100644 (file)
index 0000000..4b9d7a9
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <folly/Optional.h>
+#include <folly/experimental/logging/LogLevel.h>
+
+namespace folly {
+
+/**
+ * Configuration for a LogCategory
+ */
+class LogCategoryConfig {
+ public:
+  explicit LogCategoryConfig(
+      LogLevel level = LogLevel::WARNING,
+      bool inheritParentLevel = true);
+  LogCategoryConfig(
+      LogLevel level,
+      bool inheritParentLevel,
+      std::vector<std::string> handlers);
+
+  bool operator==(const LogCategoryConfig& other) const;
+  bool operator!=(const LogCategoryConfig& other) const;
+
+  /**
+   * The LogLevel for this category.
+   */
+  LogLevel level{LogLevel::WARNING};
+
+  /**
+   * Whether this category should inherit its effective log level from its
+   * parent category, if the parent category has a more verbose log level.
+   */
+  bool inheritParentLevel{true};
+
+  /**
+   * An optional list of LogHandler names to use for this category.
+   *
+   * When applying config changes to an existing LogCategory, the existing
+   * LogHandler list will be left unchanged if this field is unset.
+   */
+  Optional<std::vector<std::string>> handlers;
+};
+
+} // namespace folly
diff --git a/folly/experimental/logging/LogConfig.cpp b/folly/experimental/logging/LogConfig.cpp
new file mode 100644 (file)
index 0000000..1164f12
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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/LogConfig.h>
+
+namespace folly {
+
+bool LogConfig::operator==(const LogConfig& other) const {
+  return handlerConfigs_ == other.handlerConfigs_ &&
+      categoryConfigs_ == other.categoryConfigs_;
+}
+
+bool LogConfig::operator!=(const LogConfig& other) const {
+  return !(*this == other);
+}
+
+} // namespace folly
diff --git a/folly/experimental/logging/LogConfig.h b/folly/experimental/logging/LogConfig.h
new file mode 100644 (file)
index 0000000..6b0b9f8
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <string>
+#include <unordered_map>
+
+#include <folly/experimental/logging/LogCategoryConfig.h>
+#include <folly/experimental/logging/LogHandlerConfig.h>
+
+namespace folly {
+
+/**
+ * LogConfig contains configuration for the LoggerDB.
+ *
+ * This includes information about the log levels for log categories,
+ * as well as what log handlers are configured and which categories they are
+ * attached to.
+ */
+class LogConfig {
+ public:
+  using CategoryConfigMap = std::unordered_map<std::string, LogCategoryConfig>;
+  using HandlerConfigMap = std::unordered_map<std::string, LogHandlerConfig>;
+
+  LogConfig() = default;
+  explicit LogConfig(
+      HandlerConfigMap handlerConfigs,
+      CategoryConfigMap catConfigs)
+      : handlerConfigs_{std::move(handlerConfigs)},
+        categoryConfigs_{std::move(catConfigs)} {}
+
+  const CategoryConfigMap& getCategoryConfigs() const {
+    return categoryConfigs_;
+  }
+  const HandlerConfigMap& getHandlerConfigs() const {
+    return handlerConfigs_;
+  }
+
+  bool operator==(const LogConfig& other) const;
+  bool operator!=(const LogConfig& other) const;
+
+ private:
+  HandlerConfigMap handlerConfigs_;
+  CategoryConfigMap categoryConfigs_;
+};
+
+} // namespace folly
diff --git a/folly/experimental/logging/LogConfigParser.cpp b/folly/experimental/logging/LogConfigParser.cpp
new file mode 100644 (file)
index 0000000..7e51472
--- /dev/null
@@ -0,0 +1,551 @@
+/*
+ * 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/LogConfigParser.h>
+
+#include <folly/Conv.h>
+#include <folly/String.h>
+#include <folly/dynamic.h>
+#include <folly/experimental/logging/LogName.h>
+#include <folly/json.h>
+#include <cassert>
+
+using std::shared_ptr;
+using std::string;
+
+namespace folly {
+
+namespace {
+
+/**
+ * Get the type of a folly::dynamic object as a string, for inclusion in
+ * exception messages.
+ */
+std::string dynamicTypename(const dynamic& value) {
+  switch (value.type()) {
+    case dynamic::NULLT:
+      return "null";
+    case dynamic::ARRAY:
+      return "array";
+    case dynamic::BOOL:
+      return "boolean";
+    case dynamic::DOUBLE:
+      return "double";
+    case dynamic::INT64:
+      return "integer";
+    case dynamic::OBJECT:
+      return "object";
+    case dynamic::STRING:
+      return "string";
+  }
+  return "unknown type";
+}
+
+/**
+ * Parse a LogLevel from a JSON value.
+ *
+ * This accepts the log level either as an integer value or a string that can
+ * be parsed with stringToLogLevel()
+ *
+ * On success updates the result parameter and returns true.
+ * Returns false if the input is not a string or integer.
+ * Throws a LogConfigParseError on other errors.
+ */
+bool parseJsonLevel(
+    const dynamic& value,
+    StringPiece categoryName,
+    LogLevel& result) {
+  if (value.isString()) {
+    auto levelString = value.asString();
+    try {
+      result = stringToLogLevel(levelString);
+      return true;
+    } catch (const std::exception& ex) {
+      throw LogConfigParseError{to<string>(
+          "invalid log level \"",
+          levelString,
+          "\" for category \"",
+          categoryName,
+          "\"")};
+    }
+  } else if (value.isInt()) {
+    auto level = static_cast<LogLevel>(value.asInt());
+    if (level < LogLevel::MIN_LEVEL || level > LogLevel::MAX_LEVEL) {
+      throw LogConfigParseError{to<string>(
+          "invalid log level ",
+          value.asInt(),
+          " for category \"",
+          categoryName,
+          "\": outside of valid range")};
+    }
+    result = level;
+    return true;
+  }
+
+  return false;
+}
+
+LogCategoryConfig parseJsonCategoryConfig(
+    const dynamic& value,
+    StringPiece categoryName) {
+  LogCategoryConfig config;
+
+  // If the input is not an object, allow it to be
+  // just a plain level specification
+  if (!value.isObject()) {
+    if (!parseJsonLevel(value, categoryName, config.level)) {
+      throw LogConfigParseError{to<string>(
+          "unexpected data type for configuration of category \"",
+          categoryName,
+          "\": got ",
+          dynamicTypename(value),
+          ", expected an object, string, or integer")};
+    }
+    return config;
+  }
+
+  auto* level = value.get_ptr("level");
+  if (!level) {
+    // Require that level information be present for each log category.
+    throw LogConfigParseError{to<string>(
+        "no log level specified for category \"", categoryName, "\"")};
+  }
+  if (!parseJsonLevel(*level, categoryName, config.level)) {
+    throw LogConfigParseError{to<string>(
+        "unexpected data type for level field of category \"",
+        categoryName,
+        "\": got ",
+        dynamicTypename(*level),
+        ", expected a string or integer")};
+  }
+
+  auto* inherit = value.get_ptr("inherit");
+  if (inherit) {
+    if (!inherit->isBool()) {
+      throw LogConfigParseError{to<string>(
+          "unexpected data type for inherit field of category \"",
+          categoryName,
+          "\": got ",
+          dynamicTypename(*inherit),
+          ", expected a boolean")};
+    }
+    config.inheritParentLevel = inherit->asBool();
+  }
+
+  auto* handlers = value.get_ptr("handlers");
+  if (handlers) {
+    if (!handlers->isArray()) {
+      throw LogConfigParseError{to<string>(
+          "the \"handlers\" field for category ",
+          categoryName,
+          " must be a list")};
+    }
+    config.handlers = std::vector<std::string>{};
+    for (const auto& item : *handlers) {
+      if (!item.isString()) {
+        throw LogConfigParseError{to<string>(
+            "the \"handlers\" list for category ",
+            categoryName,
+            " must be contain only strings")};
+      }
+      config.handlers->push_back(item.asString());
+    }
+  }
+
+  return config;
+}
+
+LogHandlerConfig parseJsonHandlerConfig(
+    const dynamic& value,
+    StringPiece handlerName) {
+  if (!value.isObject()) {
+    throw LogConfigParseError{to<string>(
+        "unexpected data type for configuration of handler \"",
+        handlerName,
+        "\": got ",
+        dynamicTypename(value),
+        ", expected an object")};
+  }
+
+  // Parse the handler type
+  auto* type = value.get_ptr("type");
+  if (!type) {
+    throw LogConfigParseError{to<string>(
+        "no handler type specified for log handler \"", handlerName, "\"")};
+  }
+  if (!type->isString()) {
+    throw LogConfigParseError{to<string>(
+        "unexpected data type for \"type\" field of handler \"",
+        handlerName,
+        "\": got ",
+        dynamicTypename(*type),
+        ", expected a string")};
+  }
+  LogHandlerConfig config{type->asString()};
+
+  // Parse the handler options
+  auto* options = value.get_ptr("options");
+  if (options) {
+    if (!options->isObject()) {
+      throw LogConfigParseError{to<string>(
+          "unexpected data type for \"options\" field of handler \"",
+          handlerName,
+          "\": got ",
+          dynamicTypename(*options),
+          ", expected an object")};
+    }
+
+    for (const auto& item : options->items()) {
+      if (!item.first.isString()) {
+        // This shouldn't really ever happen.
+        // We deserialize the json with allow_non_string_keys set to False.
+        throw LogConfigParseError{to<string>(
+            "unexpected data type for option of handler \"",
+            handlerName,
+            "\": got ",
+            dynamicTypename(item.first),
+            ", expected string")};
+      }
+      if (!item.second.isString()) {
+        throw LogConfigParseError{to<string>(
+            "unexpected data type for option \"",
+            item.first.asString(),
+            "\" of handler \"",
+            handlerName,
+            "\": got ",
+            dynamicTypename(item.second),
+            ", expected a string")};
+      }
+      config.options[item.first.asString()] = item.second.asString();
+    }
+  }
+
+  return config;
+}
+
+LogConfig::CategoryConfigMap parseCategoryConfigs(StringPiece value) {
+  LogConfig::CategoryConfigMap categoryConfigs;
+
+  // Allow empty (or all whitespace) input
+  value = trimWhitespace(value);
+  if (value.empty()) {
+    return categoryConfigs;
+  }
+
+  std::unordered_map<string, string> seenCategories;
+  std::vector<StringPiece> pieces;
+  folly::split(",", value, pieces);
+  for (const auto& piece : pieces) {
+    LogCategoryConfig categoryConfig;
+    StringPiece categoryName;
+    StringPiece configString;
+
+    auto equalIndex = piece.find('=');
+    if (equalIndex == StringPiece::npos) {
+      // If level information is supplied without a category name,
+      // apply it to the root log category.
+      categoryName = StringPiece{"."};
+      configString = trimWhitespace(piece);
+    } else {
+      categoryName = piece.subpiece(0, equalIndex);
+      configString = piece.subpiece(equalIndex + 1);
+
+      // If ":=" is used instead of just "=", disable inheriting the parent's
+      // effective level if it is lower than this category's level.
+      if (categoryName.endsWith(':')) {
+        categoryConfig.inheritParentLevel = false;
+        categoryName.subtract(1);
+      }
+
+      // Remove whitespace from the category name
+      categoryName = trimWhitespace(categoryName);
+    }
+
+    // Split the configString into level and handler information.
+    std::vector<StringPiece> handlerPieces;
+    folly::split(":", configString, handlerPieces);
+    // folly::split() always returns a list of length 1
+    assert(handlerPieces.size() >= 1);
+    auto levelString = trimWhitespace(handlerPieces[0]);
+
+    bool hasHandlerConfig = handlerPieces.size() > 1;
+    if (handlerPieces.size() == 2 && trimWhitespace(handlerPieces[1]).empty()) {
+      // This is an explicitly empty handler list.
+      // This requests LoggerDB::updateConfig() to clear all existing log
+      // handlers from this category.
+      categoryConfig.handlers = std::vector<std::string>{};
+    } else if (hasHandlerConfig) {
+      categoryConfig.handlers = std::vector<std::string>{};
+      for (size_t n = 1; n < handlerPieces.size(); ++n) {
+        auto handlerName = trimWhitespace(handlerPieces[n]);
+        if (handlerName.empty()) {
+          throw LogConfigParseError{to<string>(
+              "error parsing configuration for log category \"",
+              categoryName,
+              "\": log handler name cannot be empty")};
+        }
+        categoryConfig.handlers->push_back(handlerName.str());
+      }
+    }
+
+    // Parse the levelString into a LogLevel
+    levelString = trimWhitespace(levelString);
+    try {
+      categoryConfig.level = stringToLogLevel(levelString);
+    } catch (const std::exception&) {
+      throw LogConfigParseError{to<string>(
+          "invalid log level \"",
+          levelString,
+          "\" for category \"",
+          categoryName,
+          "\"")};
+    }
+
+    // Check for multiple entries for the same category with different but
+    // equivalent names.
+    auto canonicalName = LogName::canonicalize(categoryName);
+    auto ret = seenCategories.emplace(canonicalName, categoryName.str());
+    if (!ret.second) {
+      throw LogConfigParseError{to<string>(
+          "category \"",
+          canonicalName,
+          "\" listed multiple times under different names: \"",
+          ret.first->second,
+          "\" and \"",
+          categoryName,
+          "\"")};
+    }
+
+    auto emplaceResult =
+        categoryConfigs.emplace(canonicalName, std::move(categoryConfig));
+    assert(emplaceResult.second);
+  }
+
+  return categoryConfigs;
+}
+
+bool splitNameValue(
+    StringPiece input,
+    StringPiece* outName,
+    StringPiece* outValue) {
+  size_t equalIndex = input.find('=');
+  if (equalIndex == StringPiece::npos) {
+    return false;
+  }
+
+  StringPiece name{input.begin(), input.begin() + equalIndex};
+  StringPiece value{input.begin() + equalIndex + 1, input.end()};
+
+  *outName = trimWhitespace(name);
+  *outValue = trimWhitespace(value);
+  return true;
+}
+
+std::pair<std::string, LogHandlerConfig> parseHandlerConfig(StringPiece value) {
+  std::vector<StringPiece> pieces;
+  folly::split(",", value, pieces);
+  // "folly::split() always returns a list of length 1";
+  assert(pieces.size() >= 1);
+
+  StringPiece handlerName;
+  StringPiece handlerType;
+  if (!splitNameValue(pieces[0], &handlerName, &handlerType)) {
+    throw LogConfigParseError{to<string>(
+        "error parsing log handler configuration \"",
+        pieces[0],
+        "\": expected data in the form NAME=TYPE")};
+  }
+  if (handlerName.empty()) {
+    throw LogConfigParseError{
+        "error parsing log handler configuration: empty log handler name"};
+  }
+  if (handlerType.empty()) {
+    throw LogConfigParseError{to<string>(
+        "error parsing configuration for log handler \"",
+        handlerName,
+        "\": empty log handler type")};
+  }
+
+  LogHandlerConfig config{handlerType};
+  for (size_t n = 1; n < pieces.size(); ++n) {
+    StringPiece optionName;
+    StringPiece optionValue;
+    if (!splitNameValue(pieces[n], &optionName, &optionValue)) {
+      throw LogConfigParseError{to<string>(
+          "error parsing configuration for log handler \"",
+          handlerName,
+          "\": options must be of the form NAME=VALUE")};
+    }
+
+    auto ret = config.options.emplace(optionName.str(), optionValue.str());
+    if (!ret.second) {
+      throw LogConfigParseError{to<string>(
+          "error parsing configuration for log handler \"",
+          handlerName,
+          "\": duplicate configuration for option \"",
+          optionName,
+          "\"")};
+    }
+  }
+
+  return std::make_pair(handlerName.str(), std::move(config));
+}
+
+} // namespace
+
+LogConfig parseLogConfig(StringPiece value) {
+  value = trimWhitespace(value);
+  if (value.startsWith('{')) {
+    return parseLogConfigJson(value);
+  }
+
+  // Split the input string on semicolons.
+  // Everything up to the first semicolon specifies log category configs.
+  // From then on each section specifies a single LogHandler config.
+  std::vector<StringPiece> pieces;
+  folly::split(";", value, pieces);
+  // "folly::split() always returns a list of length 1";
+  assert(pieces.size() >= 1);
+
+  auto categoryConfigs = parseCategoryConfigs(pieces[0]);
+  LogConfig::HandlerConfigMap handlerConfigs;
+  for (size_t n = 1; n < pieces.size(); ++n) {
+    auto handlerInfo = parseHandlerConfig(pieces[n]);
+    auto ret = handlerConfigs.emplace(
+        handlerInfo.first, std::move(handlerInfo.second));
+    if (!ret.second) {
+      throw LogConfigParseError{to<string>(
+          "configuration for log category \"",
+          handlerInfo.first,
+          "\" specified multiple times")};
+    }
+  }
+
+  return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
+}
+
+LogConfig parseLogConfigJson(StringPiece value) {
+  json::serialization_opts opts;
+  opts.allow_trailing_comma = true;
+  auto jsonData = folly::parseJson(json::stripComments(value), opts);
+  return parseLogConfigDynamic(jsonData);
+}
+
+LogConfig parseLogConfigDynamic(const dynamic& value) {
+  if (!value.isObject()) {
+    throw LogConfigParseError{"JSON config input must be an object"};
+  }
+
+  std::unordered_map<string, string> seenCategories;
+  LogConfig::CategoryConfigMap categoryConfigs;
+  auto* categories = value.get_ptr("categories");
+  if (categories) {
+    if (!categories->isObject()) {
+      throw LogConfigParseError{to<string>(
+          "unexpected data type for log categories config: got ",
+          dynamicTypename(*categories),
+          ", expected an object")};
+    }
+
+    for (const auto& entry : categories->items()) {
+      if (!entry.first.isString()) {
+        // This shouldn't really ever happen.
+        // We deserialize the json with allow_non_string_keys set to False.
+        throw LogConfigParseError{"category name must be a string"};
+      }
+      auto categoryName = entry.first.asString();
+      auto categoryConfig = parseJsonCategoryConfig(entry.second, categoryName);
+
+      // Check for multiple entries for the same category with different but
+      // equivalent names.
+      auto canonicalName = LogName::canonicalize(categoryName);
+      auto ret = seenCategories.emplace(canonicalName, categoryName);
+      if (!ret.second) {
+        throw LogConfigParseError{to<string>(
+            "category \"",
+            canonicalName,
+            "\" listed multiple times under different names: \"",
+            ret.first->second,
+            "\" and \"",
+            categoryName,
+            "\"")};
+      }
+
+      categoryConfigs[canonicalName] = std::move(categoryConfig);
+    }
+  }
+
+  LogConfig::HandlerConfigMap handlerConfigs;
+  auto* handlers = value.get_ptr("handlers");
+  if (handlers) {
+    if (!handlers->isObject()) {
+      throw LogConfigParseError{to<string>(
+          "unexpected data type for log handlers config: got ",
+          dynamicTypename(*handlers),
+          ", expected an object")};
+    }
+
+    for (const auto& entry : handlers->items()) {
+      if (!entry.first.isString()) {
+        // This shouldn't really ever happen.
+        // We deserialize the json with allow_non_string_keys set to False.
+        throw LogConfigParseError{"handler name must be a string"};
+      }
+      auto handlerName = entry.first.asString();
+      handlerConfigs.emplace(
+          handlerName, parseJsonHandlerConfig(entry.second, handlerName));
+    }
+  }
+
+  return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
+}
+
+dynamic logConfigToDynamic(const LogConfig& config) {
+  dynamic categories = dynamic::object;
+  for (const auto& entry : config.getCategoryConfigs()) {
+    categories.insert(entry.first, logConfigToDynamic(entry.second));
+  }
+
+  dynamic handlers = dynamic::object;
+  for (const auto& entry : config.getHandlerConfigs()) {
+    handlers.insert(entry.first, logConfigToDynamic(entry.second));
+  }
+
+  return dynamic::object("categories", std::move(categories))(
+      "handlers", std::move(handlers));
+}
+
+dynamic logConfigToDynamic(const LogHandlerConfig& config) {
+  dynamic options = dynamic::object;
+  for (const auto& opt : config.options) {
+    options.insert(opt.first, opt.second);
+  }
+  return dynamic::object("type", config.type)("options", options);
+}
+
+dynamic logConfigToDynamic(const LogCategoryConfig& config) {
+  auto value = dynamic::object("level", logLevelToString(config.level))(
+      "inherit", config.inheritParentLevel);
+  if (config.handlers.hasValue()) {
+    auto handlers = dynamic::array();
+    for (const auto& handlerName : config.handlers.value()) {
+      handlers.push_back(handlerName);
+    }
+    value("handlers", std::move(handlers));
+  }
+  return value;
+}
+
+} // namespace folly
diff --git a/folly/experimental/logging/LogConfigParser.h b/folly/experimental/logging/LogConfigParser.h
new file mode 100644 (file)
index 0000000..fae3863
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <stdexcept>
+
+#include <folly/Range.h>
+#include <folly/experimental/logging/LogConfig.h>
+
+/*
+ * This file contains utility functions for parsing and serializing
+ * LogConfig strings.
+ *
+ * This is separate from the LogConfig class itself, to reduce the dependencies
+ * of the core logging library.  Other code that wants to use the logging
+ * library to log messages but does not need to parse log config strings
+ * therefore does not need to depend on the folly JSON library.
+ */
+
+namespace folly {
+
+struct dynamic;
+
+class LogConfigParseError : public std::invalid_argument {
+ public:
+  using std::invalid_argument::invalid_argument;
+};
+
+/**
+ * Parse a log configuration string.
+ *
+ * See the documentation in logging/docs/Config.md for a description of the
+ * configuration string syntax.
+ *
+ * Throws a LogConfigParseError on error.
+ */
+LogConfig parseLogConfig(StringPiece value);
+
+/**
+ * Parse a JSON configuration string.
+ *
+ * See the documentation in logging/docs/Config.md for a description of the
+ * JSON configuration object format.
+ *
+ * This function uses relaxed JSON parsing, allowing C and C++ style
+ * comments, as well as trailing commas.
+ */
+LogConfig parseLogConfigJson(StringPiece value);
+
+/**
+ * Parse a folly::dynamic object.
+ *
+ * The input should be an object data type, and is parsed the same as a JSON
+ * object accpted by parseLogConfigJson().
+ */
+LogConfig parseLogConfigDynamic(const dynamic& value);
+
+/**
+ * Convert a LogConfig object to a folly::dynamic object.
+ *
+ * This can be used to serialize it as a JSON string, which can later be read
+ * back using parseLogConfigJson().
+ */
+dynamic logConfigToDynamic(const LogConfig& config);
+dynamic logConfigToDynamic(const LogHandlerConfig& config);
+dynamic logConfigToDynamic(const LogCategoryConfig& config);
+
+} // namespace folly
diff --git a/folly/experimental/logging/LogHandlerConfig.cpp b/folly/experimental/logging/LogHandlerConfig.cpp
new file mode 100644 (file)
index 0000000..009f820
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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/LogHandlerConfig.h>
+
+namespace folly {
+
+LogHandlerConfig::LogHandlerConfig(StringPiece t) : type{t.str()} {}
+
+LogHandlerConfig::LogHandlerConfig(StringPiece t, Options opts)
+    : type{t.str()}, options{std::move(opts)} {}
+
+bool LogHandlerConfig::operator==(const LogHandlerConfig& other) const {
+  return type == other.type && options == other.options;
+}
+
+bool LogHandlerConfig::operator!=(const LogHandlerConfig& other) const {
+  return !(*this == other);
+}
+
+} // namespace folly
diff --git a/folly/experimental/logging/LogHandlerConfig.h b/folly/experimental/logging/LogHandlerConfig.h
new file mode 100644 (file)
index 0000000..4fe3a3f
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+#pragma once
+
+#include <string>
+#include <unordered_map>
+
+#include <folly/Range.h>
+
+namespace folly {
+
+/**
+ * Configuration for a LogHandler
+ */
+class LogHandlerConfig {
+ public:
+  using Options = std::unordered_map<std::string, std::string>;
+
+  explicit LogHandlerConfig(folly::StringPiece type);
+  LogHandlerConfig(folly::StringPiece type, Options options);
+
+  bool operator==(const LogHandlerConfig& other) const;
+  bool operator!=(const LogHandlerConfig& other) const;
+
+  std::string type;
+  Options options;
+};
+
+} // namespace folly
index ff9deb8328b434b4f8c66d20cb7b1914c150f9ae..561210259d4cb3af8d3ce28ea5ac1731c15ef090 100644 (file)
@@ -8,8 +8,12 @@ libfollylogging_la_SOURCES = \
        ImmediateFileWriter.cpp \
        Init.cpp \
        LogCategory.cpp \
        ImmediateFileWriter.cpp \
        Init.cpp \
        LogCategory.cpp \
+       LogCategoryConfig.cpp \
+       LogConfig.cpp \
+       LogConfigParser.cpp \
        Logger.cpp \
        LoggerDB.cpp \
        Logger.cpp \
        LoggerDB.cpp \
+       LogHandlerConfig.cpp \
        LogLevel.cpp \
        LogMessage.cpp \
        LogName.cpp \
        LogLevel.cpp \
        LogMessage.cpp \
        LogName.cpp \
diff --git a/folly/experimental/logging/docs/Config.md b/folly/experimental/logging/docs/Config.md
new file mode 100644 (file)
index 0000000..b7cb425
--- /dev/null
@@ -0,0 +1,284 @@
+Logging Configuration
+=====================
+
+Overview
+--------
+
+The logging library is normally configured using configuration strings.
+
+In its most basic format, the configuration string consists of a comma
+separated list of `CATEGORY=LEVEL` pairs, e.g.:
+
+```
+folly=INFO,folly.io.async=DBG2
+```
+
+A log level name can also be specified by itself to affect the root log
+category:
+
+```
+WARN
+```
+
+These are the two forms that users will probably use most often for customizing
+log levels via command line arguments.  Additional settings, including log
+handler settings, can also be included.  The syntax is documented more
+completely in the [Basic Configuration Syntax](#basic-configuration-syntax)
+section.
+
+Log configuration can also be specified using JSON as well.  If the log
+configuration string starts with a leading `{` character (optionally after
+leading whitespace), it is parsed as a JSON object.  The JSON configuration
+format is documented in the
+[JSON Configuration Syntax](#json-configuration-syntax) section.
+
+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.
+
+
+Basic Configuration Syntax
+--------------------------
+
+The basic configuration format is parsed using `parseLogConfig()`.
+
+The basic format string is separated with semicolons.  Everything up to the
+first semicolon specifies LogCategory configurations.  Each remaining
+semicolon-separated section defines a LogHandler configuration.
+
+To keep the basic format simple, it does not support any form of character
+escape sequences.  If you need to define a log category whose name includes a
+special character like a comma or semicolon use the JSON format instead.
+
+### Grammar Overview
+
+```
+<config> ::= <category_configs> <handler_configs>
+<category_configs> ::= <category_config>
+                     | <category_config> "," <category_configs>
+                     | <empty_string>
+<handler_configs> ::= ";" <handler_config>
+                    | ";" <handler_config> <handler_configs>
+                    | <empty_string>
+
+<category_config> ::= <cat_level_config> <handler_list>
+<cat_level_config> ::= <level>
+                     | <catgory_name> "=" <level>
+                     | <catgory_name> ":=" <level>
+<handler_list> ::= ":" <handler_name> <handler_list>
+                 | <empty_string>
+
+<handler_config> ::= <handler_name> "=" <handler_type> <handler_options>
+<handler_options> ::= "," <option_name> "=" <option_value> <handler_options>
+                    | <empty_string>
+
+<catgory_name> ::= <atom>
+<handler_name> ::= <atom>
+<handler_type> ::= <atom>
+<option_name> ::= <atom>
+<option_value> ::= <atom>
+<atom> ::= any sequence of characters except ";", ",", "=", or ":",
+           with leading and trailing whitespace ignored
+
+<level> ::= <log_level_string>
+          | <positive_integer>
+<log_level_string> ::= any one of the strings accepted by logLevelToString()
+```
+
+### Log Category Configuration
+
+The log category configurations are a comma-separated list.  Each element in
+this list has the form
+
+  NAME=LEVEL:HANDLER1:HANDLER2
+
+The log category name and '=' sign can be omitted, in which case the setting
+applies to the root log category.  The root log category can also be
+explicitly named either using the empty string or the name ".".
+
+The NAME and LEVEL can also be separated with ":=" instead of "=",
+which disables log level inheritance for this category.  This forces
+category's effective log level to be the exact level specified, even if its
+parent category has a more verbose level setting.
+
+The log handler settings for a log category can be omitted, in which case
+the existing log handlers for this category will be left unchanged when
+updating the LoggerDB settings.  Specifying an empty log handler list (a
+trailing ':' with no log handlers following) will cause the log handler list
+for this category to be cleared instead.
+
+### Log Handler Configuration
+
+Each log handler configuration section takes the form
+
+  NAME=TYPE,OPTION1=VALUE1,OPTION2=VALUE2
+
+NAME specifies the log handler name, and TYPE specifies the log handler
+type.  A comma separated list of name=value options may follow the log
+handler name and type.  The option list will be passed to the
+LogHandlerFactory for the specified handler type.
+
+
+### Examples
+
+Example log configuration strings:
+
+* `ERROR`
+
+  Sets the root log category level to ERR.  (Note that `ERROR` is allowed in
+  configuration strings as an alias for the `LogLevel::ERR` value.)
+
+* `folly=INFO,folly.io=DBG2`
+
+  Sets the "folly" log category level to INFO, and the "folly.io" log
+  category level to DBG2.
+
+* `folly=DBG2,folly.io:=INFO`
+
+  Sets the "folly" log category level to DBG2, and the "folly.io" log
+  category level to INFO, and prevent it from inheriting its effective log
+  level from its parent category.  DBG2 log messages sent to "folly.io" will
+  therefore be discarded, even though they are enabled for one of its parent
+  categories.
+
+* `ERROR:stderr, folly=INFO; stderr=file,stream=stderr`
+
+  Sets the root log category level to ERROR, and sets its handler list to
+  use the "stderr" handler.  Sets the folly log level to INFO.  Defines
+  a log handler named "stderr" which writes to stderr.
+
+* `ERROR:default,folly=INFO:x;default=file,stream=stderr;x=file,path=/tmp/x.log`
+
+  Defines two log handlers: "default" which writes to stderr and "x" which
+  writes to the file /tmp/x.log
+  Sets the root log catgory level to ERROR, and configures it to use the
+  "default" handler.  Sets the log level for the "folly" category to INFO and
+  configures it to use the "x" handler.
+
+* `ERROR:default:x; default=file,stream=stderr; x=file,path=/tmp/x.log`
+
+  Defines two log handlers: "default" which writes to stderr and "x" which
+  writes to the file /tmp/x.log
+  Sets the root log catgory level to ERROR, and configures it to use both
+  the "default" and "x" handlers.
+
+* `ERROR:`
+
+  Sets the root log category level to ERR, and removes any log handlers
+  configured for it.  Explicitly specifying an empty list of handlers (with
+  a ':' followed by no handlers) will update the handlers for this category
+  to the empty list.  Not specifying handler information at all (no ':')
+  will leave any pre-existing handlers as-is.
+
+* `;default=file,stream=stdout`
+
+  Does not change any log category settings, and defines a "default" handler
+  that writes to stdout.  This format is useful to update log handler settings
+  if the "default" handler already exists and is attached to existing log
+  categories.
+
+
+JSON Configuration Syntax
+-------------------------
+
+The `parseLogConfig()` function, which parses the basic configuration string
+syntax, will also accept a JSON object string as input.  However, you can also
+use `parseLogConfigJson()` to explicitly parse the input as JSON, and not
+accept the basic configuration string syntax.
+
+The input string is parsed using relaxed JSON parsing, allowing C and C++ style
+comments, as well as trailing commas.
+
+The JSON configuration string must be a JSON object data type, with two
+optional members: `categories` and `handlers`.  Any additional members besides
+these two are ignored.
+
+### Log Category Configuration
+
+If present, the `categories` member of the top-level object should be a JSON
+object mapping log category names to configuration settings for that log
+category.
+
+The value of each element in `categories` should also be a JSON object with the
+following fields:
+
+* `level`
+
+  This field is required.  It should be a string or positive integer value
+  specifying the log level for this category.
+
+* `inherit`
+
+  This should be a boolean value indicating if this category should inherit its
+  effective log level from its parent category if its parent has a more verbose
+  log level setting.
+
+  This field is optional, and defaults to true 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.
+
+### Log Handler Configuration
+
+If present, the `handlers` member of the top-level object should be a JSON
+object mapping log handler names to configuration settings for that log
+handler.
+
+The value of each element in `handlers` should also be a JSON object with the
+following fields:
+
+* `type`
+
+  This field is required.  It should be a string containing the name of the log
+  handler type.  This type name must correspond to `LogHandlerFactory` type
+  registered with the `LoggerDB`.
+
+* `options`
+
+  This field is optional.  If present, it should be a JSON object containing
+  string-to-string mappings to be passed to the `LogHandlerFactory` for
+  constructing this log handler.
+
+### Example
+
+```javascript
+{
+  "categories": {
+    "foo": { "level": "INFO", "handlers": ["stderr"] },
+    "foo.only_fatal": { "level": "FATAL", "inherit": false }
+  }
+  "handlers": {
+    "stderr": {
+      "type": "file",
+      "options": {
+        "stream": "stderr",
+        "async": true,
+        "max_buffer_size": 4096000
+      }
+    }
+  }
+}
+```
+
+
+Custom Configuration Mechanisms
+-------------------------------
+
+Internally the the `LogConfig` class represents configuration settings for the
+folly logging library.  Users of the logging library can also programmatically
+construct their own `LogConfig` objects and use the `LoggerDB::updateConfig()`
+and `LoggerDB::resetConfig()` APIs to apply the configuration changes.
+
+You can also directly manipulate the log level and other settings on
+`LogCategory` objects.
+
+While it is possible to also manually create new `LogHandler` objects, it is
+generally preferred to do this using the `LoggerDB::updateConfig()` and
+`LoggerDB::resetConfig()` APIs.  If you manually create a new `LogHandler` and
+directly attach it to some categories the `LoggerDB::getConfig()` call will not
+be able to return complete information for your manually created log handler,
+since it does not have a name or handler type that can be included in the
+configuration.
diff --git a/folly/experimental/logging/test/ConfigParserTest.cpp b/folly/experimental/logging/test/ConfigParserTest.cpp
new file mode 100644 (file)
index 0000000..ac95884
--- /dev/null
@@ -0,0 +1,574 @@
+/*
+ * 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/String.h>
+#include <folly/dynamic.h>
+#include <folly/experimental/logging/LogCategory.h>
+#include <folly/experimental/logging/LogConfig.h>
+#include <folly/experimental/logging/LogConfigParser.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 folly {
+std::ostream& operator<<(std::ostream& os, const LogCategoryConfig& config) {
+  os << logLevelToString(config.level);
+  if (!config.inheritParentLevel) {
+    os << "!";
+  }
+  if (config.handlers.hasValue()) {
+    os << ":" << join(",", config.handlers.value());
+  }
+  return os;
+}
+
+std::ostream& operator<<(std::ostream& os, const LogHandlerConfig& config) {
+  os << config.type;
+  bool first = true;
+  for (const auto& opt : config.options) {
+    if (!first) {
+      os << ",";
+    } else {
+      os << ":";
+      first = false;
+    }
+    os << opt.first << "=" << opt.second;
+  }
+  return os;
+}
+} // namespace folly
+
+TEST(LogConfig, parseBasic) {
+  auto config = parseLogConfig("");
+  EXPECT_THAT(config.getCategoryConfigs(), UnorderedElementsAre());
+  EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
+
+  config = parseLogConfig("   ");
+  EXPECT_THAT(config.getCategoryConfigs(), UnorderedElementsAre());
+  EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
+
+  config = parseLogConfig(".=ERROR,folly=DBG2");
+  EXPECT_THAT(
+      config.getCategoryConfigs(),
+      UnorderedElementsAre(
+          Pair("", LogCategoryConfig{LogLevel::ERR, true}),
+          Pair("folly", LogCategoryConfig{LogLevel::DBG2, true})));
+  EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
+
+  config = parseLogConfig(" INFO , folly  := FATAL   ");
+  EXPECT_THAT(
+      config.getCategoryConfigs(),
+      UnorderedElementsAre(
+          Pair("", LogCategoryConfig{LogLevel::INFO, true}),
+          Pair("folly", LogCategoryConfig{LogLevel::FATAL, false})));
+  EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
+
+  config =
+      parseLogConfig("my.category:=INFO , my.other.stuff  := 19,foo.bar=DBG7");
+  EXPECT_THAT(
+      config.getCategoryConfigs(),
+      UnorderedElementsAre(
+          Pair("my.category", LogCategoryConfig{LogLevel::INFO, false}),
+          Pair(
+              "my.other.stuff",
+              LogCategoryConfig{static_cast<LogLevel>(19), false}),
+          Pair("foo.bar", LogCategoryConfig{LogLevel::DBG7, true})));
+  EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
+
+  config = parseLogConfig(" ERR ");
+  EXPECT_THAT(
+      config.getCategoryConfigs(),
+      UnorderedElementsAre(Pair("", LogCategoryConfig{LogLevel::ERR, true})));
+  EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
+
+  config = parseLogConfig(" ERR: ");
+  EXPECT_THAT(
+      config.getCategoryConfigs(),
+      UnorderedElementsAre(
+          Pair("", LogCategoryConfig{LogLevel::ERR, true, {}})));
+  EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
+
+  config = parseLogConfig(" ERR:stderr; stderr=file,stream=stderr ");
+  EXPECT_THAT(
+      config.getCategoryConfigs(),
+      UnorderedElementsAre(
+          Pair("", LogCategoryConfig{LogLevel::ERR, true, {"stderr"}})));
+  EXPECT_THAT(
+      config.getHandlerConfigs(),
+      UnorderedElementsAre(
+          Pair("stderr", LogHandlerConfig{"file", {{"stream", "stderr"}}})));
+
+  config = parseLogConfig(
+      "ERR:myfile:custom, folly=DBG2, folly.io:=WARN:other;"
+      "myfile=file,path=/tmp/x.log; "
+      "custom=custom,foo=bar,hello=world,a = b = c; "
+      "other=custom2");
+  EXPECT_THAT(
+      config.getCategoryConfigs(),
+      UnorderedElementsAre(
+          Pair(
+              "", LogCategoryConfig{LogLevel::ERR, true, {"myfile", "custom"}}),
+          Pair("folly", LogCategoryConfig{LogLevel::DBG2, true}),
+          Pair(
+              "folly.io",
+              LogCategoryConfig{LogLevel::WARN, false, {"other"}})));
+  EXPECT_THAT(
+      config.getHandlerConfigs(),
+      UnorderedElementsAre(
+          Pair("myfile", LogHandlerConfig{"file", {{"path", "/tmp/x.log"}}}),
+          Pair(
+              "custom",
+              LogHandlerConfig{
+                  "custom",
+                  {{"foo", "bar"}, {"hello", "world"}, {"a", "b = c"}}}),
+          Pair("other", LogHandlerConfig{"custom2"})));
+
+  // Log handler changes with no category changes
+  config = parseLogConfig("; myhandler=custom,foo=bar");
+  EXPECT_THAT(config.getCategoryConfigs(), UnorderedElementsAre());
+  EXPECT_THAT(
+      config.getHandlerConfigs(),
+      UnorderedElementsAre(
+          Pair("myhandler", LogHandlerConfig{"custom", {{"foo", "bar"}}})));
+}
+
+TEST(LogConfig, parseBasicErrors) {
+  // Errors in the log category settings
+  EXPECT_THROW_RE(
+      parseLogConfig("=="),
+      LogConfigParseError,
+      "invalid log level \"=\" for category \"\"");
+  EXPECT_THROW_RE(
+      parseLogConfig("bogus_level"),
+      LogConfigParseError,
+      "invalid log level \"bogus_level\" for category \".\"");
+  EXPECT_THROW_RE(
+      parseLogConfig("foo=bogus_level"),
+      LogConfigParseError,
+      "invalid log level \"bogus_level\" for category \"foo\"");
+  EXPECT_THROW_RE(
+      parseLogConfig("foo=WARN,bar=invalid"),
+      LogConfigParseError,
+      "invalid log level \"invalid\" for category \"bar\"");
+  EXPECT_THROW_RE(
+      parseLogConfig("foo=WARN,bar="),
+      LogConfigParseError,
+      "invalid log level \"\" for category \"bar\"");
+  EXPECT_THROW_RE(
+      parseLogConfig("foo=WARN,bar:="),
+      LogConfigParseError,
+      "invalid log level \"\" for category \"bar\"");
+  EXPECT_THROW_RE(
+      parseLogConfig("foo:=,bar:=WARN"),
+      LogConfigParseError,
+      "invalid log level \"\" for category \"foo\"");
+  EXPECT_THROW_RE(
+      parseLogConfig("x"),
+      LogConfigParseError,
+      "invalid log level \"x\" for category \".\"");
+  EXPECT_THROW_RE(
+      parseLogConfig("x,y,z"),
+      LogConfigParseError,
+      "invalid log level \"x\" for category \".\"");
+  EXPECT_THROW_RE(
+      parseLogConfig("foo=WARN,"),
+      LogConfigParseError,
+      "invalid log level \"\" for category \".\"");
+  EXPECT_THROW_RE(
+      parseLogConfig("="),
+      LogConfigParseError,
+      "invalid log level \"\" for category \"\"");
+  EXPECT_THROW_RE(
+      parseLogConfig(":="),
+      LogConfigParseError,
+      "invalid log level \"\" for category \"\"");
+  EXPECT_THROW_RE(
+      parseLogConfig("foo=bar=ERR"),
+      LogConfigParseError,
+      "invalid log level \"bar=ERR\" for category \"foo\"");
+  EXPECT_THROW_RE(
+      parseLogConfig("foo.bar=ERR,foo..bar=INFO"),
+      LogConfigParseError,
+      "category \"foo\\.bar\" listed multiple times under different names: "
+      "\"foo\\.+bar\" and \"foo\\.+bar\"");
+  EXPECT_THROW_RE(
+      parseLogConfig("=ERR,.=INFO"),
+      LogConfigParseError,
+      "category \"\" listed multiple times under different names: "
+      "\"\\.?\" and \"\\.?\"");
+
+  // Errors in the log handler settings
+  EXPECT_THROW_RE(
+      parseLogConfig("ERR;"),
+      LogConfigParseError,
+      "error parsing log handler configuration \"\": "
+      "expected data in the form NAME=TYPE");
+  EXPECT_THROW_RE(
+      parseLogConfig("ERR;foo"),
+      LogConfigParseError,
+      "error parsing log handler configuration \"foo\": "
+      "expected data in the form NAME=TYPE");
+  EXPECT_THROW_RE(
+      parseLogConfig("ERR;foo="),
+      LogConfigParseError,
+      "error parsing configuration for log handler \"foo\": "
+      "empty log handler type");
+  EXPECT_THROW_RE(
+      parseLogConfig("ERR;=file"),
+      LogConfigParseError,
+      "error parsing log handler configuration: empty log handler name");
+  EXPECT_THROW_RE(
+      parseLogConfig("ERR;handler1=file;"),
+      LogConfigParseError,
+      "error parsing log handler configuration \"\": "
+      "expected data in the form NAME=TYPE");
+}
+
+TEST(LogConfig, parseJson) {
+  auto config = parseLogConfig("{}");
+  EXPECT_THAT(config.getCategoryConfigs(), UnorderedElementsAre());
+  config = parseLogConfig("  {}   ");
+  EXPECT_THAT(config.getCategoryConfigs(), UnorderedElementsAre());
+
+  config = parseLogConfig(R"JSON({
+    "categories": {
+      ".": "ERROR",
+      "folly": "DBG2",
+    }
+  })JSON");
+  EXPECT_THAT(
+      config.getCategoryConfigs(),
+      UnorderedElementsAre(
+          Pair("", LogCategoryConfig{LogLevel::ERR, true}),
+          Pair("folly", LogCategoryConfig{LogLevel::DBG2, true})));
+  EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
+
+  config = parseLogConfig(R"JSON({
+    "categories": {
+      "": "ERROR",
+      "folly": "DBG2",
+    }
+  })JSON");
+  EXPECT_THAT(
+      config.getCategoryConfigs(),
+      UnorderedElementsAre(
+          Pair("", LogCategoryConfig{LogLevel::ERR, true}),
+          Pair("folly", LogCategoryConfig{LogLevel::DBG2, true})));
+  EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
+
+  config = parseLogConfig(R"JSON({
+    "categories": {
+      ".": { "level": "INFO" },
+      "folly": { "level": "FATAL", "inherit": false },
+    }
+  })JSON");
+  EXPECT_THAT(
+      config.getCategoryConfigs(),
+      UnorderedElementsAre(
+          Pair("", LogCategoryConfig{LogLevel::INFO, true}),
+          Pair("folly", LogCategoryConfig{LogLevel::FATAL, false})));
+  EXPECT_THAT(config.getHandlerConfigs(), UnorderedElementsAre());
+
+  config = parseLogConfig(R"JSON({
+    "categories": {
+      "my.category": { "level": "INFO", "inherit": true },
+      // comments are allowed
+      "my.other.stuff": { "level": 19, "inherit": false },
+      "foo.bar": { "level": "DBG7" },
+    },
+    "handlers": {
+      "h1": { "type": "custom", "options": {"foo": "bar", "a": "z"} }
+    }
+  })JSON");
+  EXPECT_THAT(
+      config.getCategoryConfigs(),
+      UnorderedElementsAre(
+          Pair("my.category", LogCategoryConfig{LogLevel::INFO, true}),
+          Pair(
+              "my.other.stuff",
+              LogCategoryConfig{static_cast<LogLevel>(19), false}),
+          Pair("foo.bar", LogCategoryConfig{LogLevel::DBG7, true})));
+  EXPECT_THAT(
+      config.getHandlerConfigs(),
+      UnorderedElementsAre(Pair(
+          "h1", LogHandlerConfig{"custom", {{"foo", "bar"}, {"a", "z"}}})));
+
+  // The JSON config parsing should allow unusual log category names
+  // containing whitespace, equal signs, and other characters not allowed in
+  // the basic config style.
+  config = parseLogConfig(R"JSON({
+    "categories": {
+      "  my.category  ": { "level": "INFO" },
+      " foo; bar=asdf, test": { "level": "DBG1" },
+    },
+    "handlers": {
+      "h1;h2,h3= ": { "type": " x;y " }
+    }
+  })JSON");
+  EXPECT_THAT(
+      config.getCategoryConfigs(),
+      UnorderedElementsAre(
+          Pair("  my.category  ", LogCategoryConfig{LogLevel::INFO, true}),
+          Pair(
+              " foo; bar=asdf, test",
+              LogCategoryConfig{LogLevel::DBG1, true})));
+  EXPECT_THAT(
+      config.getHandlerConfigs(),
+      UnorderedElementsAre(Pair("h1;h2,h3= ", LogHandlerConfig{" x;y "})));
+}
+
+TEST(LogConfig, parseJsonErrors) {
+  EXPECT_THROW_RE(
+      parseLogConfigJson("5"),
+      LogConfigParseError,
+      "JSON config input must be an object");
+  EXPECT_THROW_RE(
+      parseLogConfigJson("true"),
+      LogConfigParseError,
+      "JSON config input must be an object");
+  EXPECT_THROW_RE(
+      parseLogConfigJson("\"hello\""),
+      LogConfigParseError,
+      "JSON config input must be an object");
+  EXPECT_THROW_RE(
+      parseLogConfigJson("[1, 2, 3]"),
+      LogConfigParseError,
+      "JSON config input must be an object");
+  EXPECT_THROW_RE(
+      parseLogConfigJson(""), std::runtime_error, "json parse error");
+  EXPECT_THROW_RE(
+      parseLogConfigJson("{"), std::runtime_error, "json parse error");
+  EXPECT_THROW_RE(parseLogConfig("{"), std::runtime_error, "json parse error");
+  EXPECT_THROW_RE(
+      parseLogConfig("{}}"), std::runtime_error, "json parse error");
+
+  StringPiece input = R"JSON({
+    "categories": 5
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "unexpected data type for log categories config: "
+      "got integer, expected an object");
+  input = R"JSON({
+    "categories": {
+      "foo": true,
+    }
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "unexpected data type for configuration of category \"foo\": "
+      "got boolean, expected an object, string, or integer");
+
+  input = R"JSON({
+    "categories": {
+      "foo": [1, 2, 3],
+    }
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "unexpected data type for configuration of category \"foo\": "
+      "got array, expected an object, string, or integer");
+
+  input = R"JSON({
+    "categories": {
+      ".": { "level": "INFO" },
+      "folly": { "level": "FATAL", "inherit": 19 },
+    }
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "unexpected data type for inherit field of category \"folly\": "
+      "got integer, expected a boolean");
+  input = R"JSON({
+    "categories": {
+      "folly": { "level": [], },
+    }
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "unexpected data type for level field of category \"folly\": "
+      "got array, expected a string or integer");
+  input = R"JSON({
+    "categories": {
+      5: {}
+    }
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input), std::runtime_error, "json parse error");
+
+  input = R"JSON({
+    "categories": {
+      "foo...bar": { "level": "INFO", },
+      "foo..bar": { "level": "INFO", },
+    }
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "category \"foo\\.bar\" listed multiple times under different names: "
+      "\"foo\\.\\.+bar\" and \"foo\\.+bar\"");
+  input = R"JSON({
+    "categories": {
+      "...": { "level": "ERR", },
+      "": { "level": "INFO", },
+    }
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "category \"\" listed multiple times under different names: "
+      "\"(\\.\\.\\.|)\" and \"(\\.\\.\\.|)\"");
+
+  input = R"JSON({
+    "categories": { "folly": { "level": "ERR" } },
+    "handlers": 9.8
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "unexpected data type for log handlers config: "
+      "got double, expected an object");
+
+  input = R"JSON({
+    "categories": { "folly": { "level": "ERR" } },
+    "handlers": {
+      "foo": "test"
+    }
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "unexpected data type for configuration of handler \"foo\": "
+      "got string, expected an object");
+
+  input = R"JSON({
+    "categories": { "folly": { "level": "ERR" } },
+    "handlers": {
+      "foo": {}
+    }
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "no handler type specified for log handler \"foo\"");
+
+  input = R"JSON({
+    "categories": { "folly": { "level": "ERR" } },
+    "handlers": {
+      "foo": {
+        "type": 19
+      }
+    }
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "unexpected data type for \"type\" field of handler \"foo\": "
+      "got integer, expected a string");
+
+  input = R"JSON({
+    "categories": { "folly": { "level": "ERR" } },
+    "handlers": {
+      "foo": {
+        "type": "custom",
+        "options": true
+      }
+    }
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "unexpected data type for \"options\" field of handler \"foo\": "
+      "got boolean, expected an object");
+
+  input = R"JSON({
+    "categories": { "folly": { "level": "ERR" } },
+    "handlers": {
+      "foo": {
+        "type": "custom",
+        "options": ["foo", "bar"]
+      }
+    }
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "unexpected data type for \"options\" field of handler \"foo\": "
+      "got array, expected an object");
+
+  input = R"JSON({
+    "categories": { "folly": { "level": "ERR" } },
+    "handlers": {
+      "foo": {
+        "type": "custom",
+        "options": {"bar": 5}
+      }
+    }
+  })JSON";
+  EXPECT_THROW_RE(
+      parseLogConfig(input),
+      LogConfigParseError,
+      "unexpected data type for option \"bar\" of handler \"foo\": "
+      "got integer, expected a string");
+}
+
+TEST(LogConfig, toJson) {
+  auto config = parseLogConfig("");
+  auto expectedJson = folly::parseJson(R"JSON({
+  "categories": {},
+  "handlers": {}
+})JSON");
+  EXPECT_EQ(expectedJson, logConfigToDynamic(config));
+
+  config = parseLogConfig(
+      "ERROR:h1,foo.bar:=FATAL,folly=INFO:; "
+      "h1=custom,foo=bar");
+  expectedJson = folly::parseJson(R"JSON({
+  "categories" : {
+    "" : {
+      "inherit" : true,
+      "level" : "ERR",
+      "handlers" : ["h1"]
+    },
+    "folly" : {
+      "inherit" : true,
+      "level" : "INFO",
+      "handlers" : []
+    },
+    "foo.bar" : {
+      "inherit" : false,
+      "level" : "FATAL"
+    }
+  },
+  "handlers" : {
+    "h1": {
+      "type": "custom",
+      "options": { "foo": "bar" }
+    }
+  }
+})JSON");
+  EXPECT_EQ(expectedJson, logConfigToDynamic(config));
+}