2 * Copyright 2004-present Facebook, Inc.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 #include <folly/experimental/logging/LogConfigParser.h>
18 #include <folly/Conv.h>
19 #include <folly/String.h>
20 #include <folly/dynamic.h>
21 #include <folly/experimental/logging/LogName.h>
22 #include <folly/json.h>
23 #include <folly/lang/SafeAssert.h>
25 using std::shared_ptr;
33 * Get the type of a folly::dynamic object as a string, for inclusion in
36 std::string dynamicTypename(const dynamic& value) {
37 switch (value.type()) {
53 return "unknown type";
57 * Parse a LogLevel from a JSON value.
59 * This accepts the log level either as an integer value or a string that can
60 * be parsed with stringToLogLevel()
62 * On success updates the result parameter and returns true.
63 * Returns false if the input is not a string or integer.
64 * Throws a LogConfigParseError on other errors.
68 StringPiece categoryName,
70 if (value.isString()) {
71 auto levelString = value.asString();
73 result = stringToLogLevel(levelString);
75 } catch (const std::exception& ex) {
76 throw LogConfigParseError{to<string>(
77 "invalid log level \"",
83 } else if (value.isInt()) {
84 auto level = static_cast<LogLevel>(value.asInt());
85 if (level < LogLevel::MIN_LEVEL || level > LogLevel::MAX_LEVEL) {
86 throw LogConfigParseError{to<string>(
91 "\": outside of valid range")};
100 LogCategoryConfig parseJsonCategoryConfig(
101 const dynamic& value,
102 StringPiece categoryName) {
103 LogCategoryConfig config;
105 // If the input is not an object, allow it to be
106 // just a plain level specification
107 if (!value.isObject()) {
108 if (!parseJsonLevel(value, categoryName, config.level)) {
109 throw LogConfigParseError{to<string>(
110 "unexpected data type for configuration of category \"",
113 dynamicTypename(value),
114 ", expected an object, string, or integer")};
119 auto* level = value.get_ptr("level");
121 // Require that level information be present for each log category.
122 throw LogConfigParseError{to<string>(
123 "no log level specified for category \"", categoryName, "\"")};
125 if (!parseJsonLevel(*level, categoryName, config.level)) {
126 throw LogConfigParseError{to<string>(
127 "unexpected data type for level field of category \"",
130 dynamicTypename(*level),
131 ", expected a string or integer")};
134 auto* inherit = value.get_ptr("inherit");
136 if (!inherit->isBool()) {
137 throw LogConfigParseError{to<string>(
138 "unexpected data type for inherit field of category \"",
141 dynamicTypename(*inherit),
142 ", expected a boolean")};
144 config.inheritParentLevel = inherit->asBool();
147 auto* handlers = value.get_ptr("handlers");
149 if (!handlers->isArray()) {
150 throw LogConfigParseError{to<string>(
151 "the \"handlers\" field for category ",
155 config.handlers = std::vector<std::string>{};
156 for (const auto& item : *handlers) {
157 if (!item.isString()) {
158 throw LogConfigParseError{to<string>(
159 "the \"handlers\" list for category ",
161 " must be contain only strings")};
163 config.handlers->push_back(item.asString());
170 LogHandlerConfig parseJsonHandlerConfig(
171 const dynamic& value,
172 StringPiece handlerName) {
173 if (!value.isObject()) {
174 throw LogConfigParseError{to<string>(
175 "unexpected data type for configuration of handler \"",
178 dynamicTypename(value),
179 ", expected an object")};
182 // Parse the handler type
183 auto* type = value.get_ptr("type");
185 throw LogConfigParseError{to<string>(
186 "no handler type specified for log handler \"", handlerName, "\"")};
188 if (!type->isString()) {
189 throw LogConfigParseError{to<string>(
190 "unexpected data type for \"type\" field of handler \"",
193 dynamicTypename(*type),
194 ", expected a string")};
196 LogHandlerConfig config{type->asString()};
198 // Parse the handler options
199 auto* options = value.get_ptr("options");
201 if (!options->isObject()) {
202 throw LogConfigParseError{to<string>(
203 "unexpected data type for \"options\" field of handler \"",
206 dynamicTypename(*options),
207 ", expected an object")};
210 for (const auto& item : options->items()) {
211 if (!item.first.isString()) {
212 // This shouldn't really ever happen.
213 // We deserialize the json with allow_non_string_keys set to False.
214 throw LogConfigParseError{to<string>(
215 "unexpected data type for option of handler \"",
218 dynamicTypename(item.first),
219 ", expected string")};
221 if (!item.second.isString()) {
222 throw LogConfigParseError{to<string>(
223 "unexpected data type for option \"",
224 item.first.asString(),
228 dynamicTypename(item.second),
229 ", expected a string")};
231 config.options[item.first.asString()] = item.second.asString();
238 LogConfig::CategoryConfigMap parseCategoryConfigs(StringPiece value) {
239 LogConfig::CategoryConfigMap categoryConfigs;
241 // Allow empty (or all whitespace) input
242 value = trimWhitespace(value);
244 return categoryConfigs;
247 std::unordered_map<string, string> seenCategories;
248 std::vector<StringPiece> pieces;
249 folly::split(",", value, pieces);
250 for (const auto& piece : pieces) {
251 LogCategoryConfig categoryConfig;
252 StringPiece categoryName;
253 StringPiece configString;
255 auto equalIndex = piece.find('=');
256 if (equalIndex == StringPiece::npos) {
257 // If level information is supplied without a category name,
258 // apply it to the root log category.
259 categoryName = StringPiece{"."};
260 configString = trimWhitespace(piece);
262 categoryName = piece.subpiece(0, equalIndex);
263 configString = piece.subpiece(equalIndex + 1);
265 // If ":=" is used instead of just "=", disable inheriting the parent's
266 // effective level if it is lower than this category's level.
267 if (categoryName.endsWith(':')) {
268 categoryConfig.inheritParentLevel = false;
269 categoryName.subtract(1);
272 // Remove whitespace from the category name
273 categoryName = trimWhitespace(categoryName);
276 // Split the configString into level and handler information.
277 std::vector<StringPiece> handlerPieces;
278 folly::split(":", configString, handlerPieces);
280 handlerPieces.size() >= 1,
281 "folly::split() always returns a list of length 1");
282 auto levelString = trimWhitespace(handlerPieces[0]);
284 bool hasHandlerConfig = handlerPieces.size() > 1;
285 if (handlerPieces.size() == 2 && trimWhitespace(handlerPieces[1]).empty()) {
286 // This is an explicitly empty handler list.
287 // This requests LoggerDB::updateConfig() to clear all existing log
288 // handlers from this category.
289 categoryConfig.handlers = std::vector<std::string>{};
290 } else if (hasHandlerConfig) {
291 categoryConfig.handlers = std::vector<std::string>{};
292 for (size_t n = 1; n < handlerPieces.size(); ++n) {
293 auto handlerName = trimWhitespace(handlerPieces[n]);
294 if (handlerName.empty()) {
295 throw LogConfigParseError{to<string>(
296 "error parsing configuration for log category \"",
298 "\": log handler name cannot be empty")};
300 categoryConfig.handlers->push_back(handlerName.str());
304 // Parse the levelString into a LogLevel
305 levelString = trimWhitespace(levelString);
307 categoryConfig.level = stringToLogLevel(levelString);
308 } catch (const std::exception&) {
309 throw LogConfigParseError{to<string>(
310 "invalid log level \"",
312 "\" for category \"",
317 // Check for multiple entries for the same category with different but
319 auto canonicalName = LogName::canonicalize(categoryName);
320 auto ret = seenCategories.emplace(canonicalName, categoryName.str());
322 throw LogConfigParseError{to<string>(
325 "\" listed multiple times under different names: \"",
333 categoryConfigs.emplace(canonicalName, std::move(categoryConfig));
335 emplaceResult.second,
336 "category name must be new since it was not in seenCategories");
339 return categoryConfigs;
344 StringPiece* outName,
345 StringPiece* outValue) {
346 size_t equalIndex = input.find('=');
347 if (equalIndex == StringPiece::npos) {
351 StringPiece name{input.begin(), input.begin() + equalIndex};
352 StringPiece value{input.begin() + equalIndex + 1, input.end()};
354 *outName = trimWhitespace(name);
355 *outValue = trimWhitespace(value);
359 std::pair<std::string, LogHandlerConfig> parseHandlerConfig(StringPiece value) {
360 // Parse the handler name and optional type
361 auto colonIndex = value.find(':');
362 StringPiece namePortion;
363 StringPiece optionsStr;
364 if (colonIndex == StringPiece::npos) {
367 namePortion = StringPiece{value.begin(), value.begin() + colonIndex};
368 optionsStr = StringPiece{value.begin() + colonIndex + 1, value.end()};
371 StringPiece handlerName;
372 Optional<StringPiece> handlerType(in_place);
373 if (!splitNameValue(namePortion, &handlerName, &handlerType.value())) {
374 handlerName = trimWhitespace(namePortion);
375 handlerType = folly::none;
378 // Make sure the handler name and type are not empty.
379 // Also disallow commas in the name: this helps catch accidental errors where
380 // the user left out the ':' and intended to be specifying options instead of
381 // part of the name or type.
382 if (handlerName.empty()) {
383 throw LogConfigParseError{
384 "error parsing log handler configuration: empty log handler name"};
386 if (handlerName.contains(',')) {
387 throw LogConfigParseError{to<string>(
388 "error parsing configuration for log handler \"",
390 "\": name cannot contain a comma when using the basic config format")};
392 if (handlerType.hasValue()) {
393 if (handlerType->empty()) {
394 throw LogConfigParseError{to<string>(
395 "error parsing configuration for log handler \"",
397 "\": empty log handler type")};
399 if (handlerType->contains(',')) {
400 throw LogConfigParseError{to<string>(
401 "error parsing configuration for log handler \"",
403 "\": invalid type \"",
405 "\": type name cannot contain a comma when using "
406 "the basic config format")};
411 LogHandlerConfig config{handlerType};
412 optionsStr = trimWhitespace(optionsStr);
413 if (!optionsStr.empty()) {
414 std::vector<StringPiece> pieces;
415 folly::split(",", optionsStr, pieces);
417 pieces.size() >= 1, "folly::split() always returns a list of length 1");
419 for (const auto& piece : pieces) {
420 StringPiece optionName;
421 StringPiece optionValue;
422 if (!splitNameValue(piece, &optionName, &optionValue)) {
423 throw LogConfigParseError{to<string>(
424 "error parsing configuration for log handler \"",
426 "\": options must be of the form NAME=VALUE")};
429 auto ret = config.options.emplace(optionName.str(), optionValue.str());
431 throw LogConfigParseError{to<string>(
432 "error parsing configuration for log handler \"",
434 "\": duplicate configuration for option \"",
441 return std::make_pair(handlerName.str(), std::move(config));
446 LogConfig parseLogConfig(StringPiece value) {
447 value = trimWhitespace(value);
448 if (value.startsWith('{')) {
449 return parseLogConfigJson(value);
452 // Split the input string on semicolons.
453 // Everything up to the first semicolon specifies log category configs.
454 // From then on each section specifies a single LogHandler config.
455 std::vector<StringPiece> pieces;
456 folly::split(";", value, pieces);
458 pieces.size() >= 1, "folly::split() always returns a list of length 1");
460 auto categoryConfigs = parseCategoryConfigs(pieces[0]);
461 LogConfig::HandlerConfigMap handlerConfigs;
462 for (size_t n = 1; n < pieces.size(); ++n) {
463 auto handlerInfo = parseHandlerConfig(pieces[n]);
464 auto ret = handlerConfigs.emplace(
465 handlerInfo.first, std::move(handlerInfo.second));
467 throw LogConfigParseError{to<string>(
468 "configuration for log category \"",
470 "\" specified multiple times")};
474 return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
477 LogConfig parseLogConfigJson(StringPiece value) {
478 json::serialization_opts opts;
479 opts.allow_trailing_comma = true;
480 auto jsonData = folly::parseJson(json::stripComments(value), opts);
481 return parseLogConfigDynamic(jsonData);
484 LogConfig parseLogConfigDynamic(const dynamic& value) {
485 if (!value.isObject()) {
486 throw LogConfigParseError{"JSON config input must be an object"};
489 std::unordered_map<string, string> seenCategories;
490 LogConfig::CategoryConfigMap categoryConfigs;
491 auto* categories = value.get_ptr("categories");
493 if (!categories->isObject()) {
494 throw LogConfigParseError{to<string>(
495 "unexpected data type for log categories config: got ",
496 dynamicTypename(*categories),
497 ", expected an object")};
500 for (const auto& entry : categories->items()) {
501 if (!entry.first.isString()) {
502 // This shouldn't really ever happen.
503 // We deserialize the json with allow_non_string_keys set to False.
504 throw LogConfigParseError{"category name must be a string"};
506 auto categoryName = entry.first.asString();
507 auto categoryConfig = parseJsonCategoryConfig(entry.second, categoryName);
509 // Check for multiple entries for the same category with different but
511 auto canonicalName = LogName::canonicalize(categoryName);
512 auto ret = seenCategories.emplace(canonicalName, categoryName);
514 throw LogConfigParseError{to<string>(
517 "\" listed multiple times under different names: \"",
524 categoryConfigs[canonicalName] = std::move(categoryConfig);
528 LogConfig::HandlerConfigMap handlerConfigs;
529 auto* handlers = value.get_ptr("handlers");
531 if (!handlers->isObject()) {
532 throw LogConfigParseError{to<string>(
533 "unexpected data type for log handlers config: got ",
534 dynamicTypename(*handlers),
535 ", expected an object")};
538 for (const auto& entry : handlers->items()) {
539 if (!entry.first.isString()) {
540 // This shouldn't really ever happen.
541 // We deserialize the json with allow_non_string_keys set to False.
542 throw LogConfigParseError{"handler name must be a string"};
544 auto handlerName = entry.first.asString();
545 handlerConfigs.emplace(
546 handlerName, parseJsonHandlerConfig(entry.second, handlerName));
550 return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
553 dynamic logConfigToDynamic(const LogConfig& config) {
554 dynamic categories = dynamic::object;
555 for (const auto& entry : config.getCategoryConfigs()) {
556 categories.insert(entry.first, logConfigToDynamic(entry.second));
559 dynamic handlers = dynamic::object;
560 for (const auto& entry : config.getHandlerConfigs()) {
561 handlers.insert(entry.first, logConfigToDynamic(entry.second));
564 return dynamic::object("categories", std::move(categories))(
565 "handlers", std::move(handlers));
568 dynamic logConfigToDynamic(const LogHandlerConfig& config) {
569 dynamic options = dynamic::object;
570 for (const auto& opt : config.options) {
571 options.insert(opt.first, opt.second);
573 auto result = dynamic::object("options", options);
574 if (config.type.hasValue()) {
575 result("type", config.type.value());
577 return std::move(result);
580 dynamic logConfigToDynamic(const LogCategoryConfig& config) {
581 auto value = dynamic::object("level", logLevelToString(config.level))(
582 "inherit", config.inheritParentLevel);
583 if (config.handlers.hasValue()) {
584 auto handlers = dynamic::array();
585 for (const auto& handlerName : config.handlers.value()) {
586 handlers.push_back(handlerName);
588 value("handlers", std::move(handlers));
590 return std::move(value);