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
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/LogHandlerConfig.h \
experimental/logging/LogLevel.h \
experimental/logging/LogMessage.h \
experimental/logging/LogName.h \
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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
ImmediateFileWriter.cpp \
Init.cpp \
LogCategory.cpp \
+ LogCategoryConfig.cpp \
+ LogConfig.cpp \
+ LogConfigParser.cpp \
Logger.cpp \
LoggerDB.cpp \
+ LogHandlerConfig.cpp \
LogLevel.cpp \
LogMessage.cpp \
LogName.cpp \
--- /dev/null
+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.
--- /dev/null
+/*
+ * 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));
+}