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 std::vector<StringPiece> pieces;
361 folly::split(",", value, pieces);
363 pieces.size() >= 1, "folly::split() always returns a list of length 1");
365 StringPiece handlerName;
366 StringPiece handlerType;
367 if (!splitNameValue(pieces[0], &handlerName, &handlerType)) {
368 throw LogConfigParseError{to<string>(
369 "error parsing log handler configuration \"",
371 "\": expected data in the form NAME=TYPE")};
373 if (handlerName.empty()) {
374 throw LogConfigParseError{
375 "error parsing log handler configuration: empty log handler name"};
377 if (handlerType.empty()) {
378 throw LogConfigParseError{to<string>(
379 "error parsing configuration for log handler \"",
381 "\": empty log handler type")};
384 LogHandlerConfig config{handlerType};
385 for (size_t n = 1; n < pieces.size(); ++n) {
386 StringPiece optionName;
387 StringPiece optionValue;
388 if (!splitNameValue(pieces[n], &optionName, &optionValue)) {
389 throw LogConfigParseError{to<string>(
390 "error parsing configuration for log handler \"",
392 "\": options must be of the form NAME=VALUE")};
395 auto ret = config.options.emplace(optionName.str(), optionValue.str());
397 throw LogConfigParseError{to<string>(
398 "error parsing configuration for log handler \"",
400 "\": duplicate configuration for option \"",
406 return std::make_pair(handlerName.str(), std::move(config));
411 LogConfig parseLogConfig(StringPiece value) {
412 value = trimWhitespace(value);
413 if (value.startsWith('{')) {
414 return parseLogConfigJson(value);
417 // Split the input string on semicolons.
418 // Everything up to the first semicolon specifies log category configs.
419 // From then on each section specifies a single LogHandler config.
420 std::vector<StringPiece> pieces;
421 folly::split(";", value, pieces);
423 pieces.size() >= 1, "folly::split() always returns a list of length 1");
425 auto categoryConfigs = parseCategoryConfigs(pieces[0]);
426 LogConfig::HandlerConfigMap handlerConfigs;
427 for (size_t n = 1; n < pieces.size(); ++n) {
428 auto handlerInfo = parseHandlerConfig(pieces[n]);
429 auto ret = handlerConfigs.emplace(
430 handlerInfo.first, std::move(handlerInfo.second));
432 throw LogConfigParseError{to<string>(
433 "configuration for log category \"",
435 "\" specified multiple times")};
439 return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
442 LogConfig parseLogConfigJson(StringPiece value) {
443 json::serialization_opts opts;
444 opts.allow_trailing_comma = true;
445 auto jsonData = folly::parseJson(json::stripComments(value), opts);
446 return parseLogConfigDynamic(jsonData);
449 LogConfig parseLogConfigDynamic(const dynamic& value) {
450 if (!value.isObject()) {
451 throw LogConfigParseError{"JSON config input must be an object"};
454 std::unordered_map<string, string> seenCategories;
455 LogConfig::CategoryConfigMap categoryConfigs;
456 auto* categories = value.get_ptr("categories");
458 if (!categories->isObject()) {
459 throw LogConfigParseError{to<string>(
460 "unexpected data type for log categories config: got ",
461 dynamicTypename(*categories),
462 ", expected an object")};
465 for (const auto& entry : categories->items()) {
466 if (!entry.first.isString()) {
467 // This shouldn't really ever happen.
468 // We deserialize the json with allow_non_string_keys set to False.
469 throw LogConfigParseError{"category name must be a string"};
471 auto categoryName = entry.first.asString();
472 auto categoryConfig = parseJsonCategoryConfig(entry.second, categoryName);
474 // Check for multiple entries for the same category with different but
476 auto canonicalName = LogName::canonicalize(categoryName);
477 auto ret = seenCategories.emplace(canonicalName, categoryName);
479 throw LogConfigParseError{to<string>(
482 "\" listed multiple times under different names: \"",
489 categoryConfigs[canonicalName] = std::move(categoryConfig);
493 LogConfig::HandlerConfigMap handlerConfigs;
494 auto* handlers = value.get_ptr("handlers");
496 if (!handlers->isObject()) {
497 throw LogConfigParseError{to<string>(
498 "unexpected data type for log handlers config: got ",
499 dynamicTypename(*handlers),
500 ", expected an object")};
503 for (const auto& entry : handlers->items()) {
504 if (!entry.first.isString()) {
505 // This shouldn't really ever happen.
506 // We deserialize the json with allow_non_string_keys set to False.
507 throw LogConfigParseError{"handler name must be a string"};
509 auto handlerName = entry.first.asString();
510 handlerConfigs.emplace(
511 handlerName, parseJsonHandlerConfig(entry.second, handlerName));
515 return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
518 dynamic logConfigToDynamic(const LogConfig& config) {
519 dynamic categories = dynamic::object;
520 for (const auto& entry : config.getCategoryConfigs()) {
521 categories.insert(entry.first, logConfigToDynamic(entry.second));
524 dynamic handlers = dynamic::object;
525 for (const auto& entry : config.getHandlerConfigs()) {
526 handlers.insert(entry.first, logConfigToDynamic(entry.second));
529 return dynamic::object("categories", std::move(categories))(
530 "handlers", std::move(handlers));
533 dynamic logConfigToDynamic(const LogHandlerConfig& config) {
534 dynamic options = dynamic::object;
535 for (const auto& opt : config.options) {
536 options.insert(opt.first, opt.second);
538 return dynamic::object("type", config.type)("options", options);
541 dynamic logConfigToDynamic(const LogCategoryConfig& config) {
542 auto value = dynamic::object("level", logLevelToString(config.level))(
543 "inherit", config.inheritParentLevel);
544 if (config.handlers.hasValue()) {
545 auto handlers = dynamic::array();
546 for (const auto& handlerName : config.handlers.value()) {
547 handlers.push_back(handlerName);
549 value("handlers", std::move(handlers));