d0948709d6fd24bbde59ba44499b8fd461476c7e
[folly.git] / folly / experimental / logging / LogConfigParser.cpp
1 /*
2  * Copyright 2004-present Facebook, Inc.
3  *
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
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 #include <folly/experimental/logging/LogConfigParser.h>
17
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>
24
25 using std::shared_ptr;
26 using std::string;
27
28 namespace folly {
29
30 namespace {
31
32 /**
33  * Get the type of a folly::dynamic object as a string, for inclusion in
34  * exception messages.
35  */
36 std::string dynamicTypename(const dynamic& value) {
37   switch (value.type()) {
38     case dynamic::NULLT:
39       return "null";
40     case dynamic::ARRAY:
41       return "array";
42     case dynamic::BOOL:
43       return "boolean";
44     case dynamic::DOUBLE:
45       return "double";
46     case dynamic::INT64:
47       return "integer";
48     case dynamic::OBJECT:
49       return "object";
50     case dynamic::STRING:
51       return "string";
52   }
53   return "unknown type";
54 }
55
56 /**
57  * Parse a LogLevel from a JSON value.
58  *
59  * This accepts the log level either as an integer value or a string that can
60  * be parsed with stringToLogLevel()
61  *
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.
65  */
66 bool parseJsonLevel(
67     const dynamic& value,
68     StringPiece categoryName,
69     LogLevel& result) {
70   if (value.isString()) {
71     auto levelString = value.asString();
72     try {
73       result = stringToLogLevel(levelString);
74       return true;
75     } catch (const std::exception& ex) {
76       throw LogConfigParseError{to<string>(
77           "invalid log level \"",
78           levelString,
79           "\" for category \"",
80           categoryName,
81           "\"")};
82     }
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>(
87           "invalid log level ",
88           value.asInt(),
89           " for category \"",
90           categoryName,
91           "\": outside of valid range")};
92     }
93     result = level;
94     return true;
95   }
96
97   return false;
98 }
99
100 LogCategoryConfig parseJsonCategoryConfig(
101     const dynamic& value,
102     StringPiece categoryName) {
103   LogCategoryConfig config;
104
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 \"",
111           categoryName,
112           "\": got ",
113           dynamicTypename(value),
114           ", expected an object, string, or integer")};
115     }
116     return config;
117   }
118
119   auto* level = value.get_ptr("level");
120   if (!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, "\"")};
124   }
125   if (!parseJsonLevel(*level, categoryName, config.level)) {
126     throw LogConfigParseError{to<string>(
127         "unexpected data type for level field of category \"",
128         categoryName,
129         "\": got ",
130         dynamicTypename(*level),
131         ", expected a string or integer")};
132   }
133
134   auto* inherit = value.get_ptr("inherit");
135   if (inherit) {
136     if (!inherit->isBool()) {
137       throw LogConfigParseError{to<string>(
138           "unexpected data type for inherit field of category \"",
139           categoryName,
140           "\": got ",
141           dynamicTypename(*inherit),
142           ", expected a boolean")};
143     }
144     config.inheritParentLevel = inherit->asBool();
145   }
146
147   auto* handlers = value.get_ptr("handlers");
148   if (handlers) {
149     if (!handlers->isArray()) {
150       throw LogConfigParseError{to<string>(
151           "the \"handlers\" field for category ",
152           categoryName,
153           " must be a list")};
154     }
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 ",
160             categoryName,
161             " must be contain only strings")};
162       }
163       config.handlers->push_back(item.asString());
164     }
165   }
166
167   return config;
168 }
169
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 \"",
176         handlerName,
177         "\": got ",
178         dynamicTypename(value),
179         ", expected an object")};
180   }
181
182   // Parse the handler type
183   auto* type = value.get_ptr("type");
184   if (!type) {
185     throw LogConfigParseError{to<string>(
186         "no handler type specified for log handler \"", handlerName, "\"")};
187   }
188   if (!type->isString()) {
189     throw LogConfigParseError{to<string>(
190         "unexpected data type for \"type\" field of handler \"",
191         handlerName,
192         "\": got ",
193         dynamicTypename(*type),
194         ", expected a string")};
195   }
196   LogHandlerConfig config{type->asString()};
197
198   // Parse the handler options
199   auto* options = value.get_ptr("options");
200   if (options) {
201     if (!options->isObject()) {
202       throw LogConfigParseError{to<string>(
203           "unexpected data type for \"options\" field of handler \"",
204           handlerName,
205           "\": got ",
206           dynamicTypename(*options),
207           ", expected an object")};
208     }
209
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 \"",
216             handlerName,
217             "\": got ",
218             dynamicTypename(item.first),
219             ", expected string")};
220       }
221       if (!item.second.isString()) {
222         throw LogConfigParseError{to<string>(
223             "unexpected data type for option \"",
224             item.first.asString(),
225             "\" of handler \"",
226             handlerName,
227             "\": got ",
228             dynamicTypename(item.second),
229             ", expected a string")};
230       }
231       config.options[item.first.asString()] = item.second.asString();
232     }
233   }
234
235   return config;
236 }
237
238 LogConfig::CategoryConfigMap parseCategoryConfigs(StringPiece value) {
239   LogConfig::CategoryConfigMap categoryConfigs;
240
241   // Allow empty (or all whitespace) input
242   value = trimWhitespace(value);
243   if (value.empty()) {
244     return categoryConfigs;
245   }
246
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;
254
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);
261     } else {
262       categoryName = piece.subpiece(0, equalIndex);
263       configString = piece.subpiece(equalIndex + 1);
264
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);
270       }
271
272       // Remove whitespace from the category name
273       categoryName = trimWhitespace(categoryName);
274     }
275
276     // Split the configString into level and handler information.
277     std::vector<StringPiece> handlerPieces;
278     folly::split(":", configString, handlerPieces);
279     FOLLY_SAFE_DCHECK(
280         handlerPieces.size() >= 1,
281         "folly::split() always returns a list of length 1");
282     auto levelString = trimWhitespace(handlerPieces[0]);
283
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 \"",
297               categoryName,
298               "\": log handler name cannot be empty")};
299         }
300         categoryConfig.handlers->push_back(handlerName.str());
301       }
302     }
303
304     // Parse the levelString into a LogLevel
305     levelString = trimWhitespace(levelString);
306     try {
307       categoryConfig.level = stringToLogLevel(levelString);
308     } catch (const std::exception&) {
309       throw LogConfigParseError{to<string>(
310           "invalid log level \"",
311           levelString,
312           "\" for category \"",
313           categoryName,
314           "\"")};
315     }
316
317     // Check for multiple entries for the same category with different but
318     // equivalent names.
319     auto canonicalName = LogName::canonicalize(categoryName);
320     auto ret = seenCategories.emplace(canonicalName, categoryName.str());
321     if (!ret.second) {
322       throw LogConfigParseError{to<string>(
323           "category \"",
324           canonicalName,
325           "\" listed multiple times under different names: \"",
326           ret.first->second,
327           "\" and \"",
328           categoryName,
329           "\"")};
330     }
331
332     auto emplaceResult =
333         categoryConfigs.emplace(canonicalName, std::move(categoryConfig));
334     FOLLY_SAFE_DCHECK(
335         emplaceResult.second,
336         "category name must be new since it was not in seenCategories");
337   }
338
339   return categoryConfigs;
340 }
341
342 bool splitNameValue(
343     StringPiece input,
344     StringPiece* outName,
345     StringPiece* outValue) {
346   size_t equalIndex = input.find('=');
347   if (equalIndex == StringPiece::npos) {
348     return false;
349   }
350
351   StringPiece name{input.begin(), input.begin() + equalIndex};
352   StringPiece value{input.begin() + equalIndex + 1, input.end()};
353
354   *outName = trimWhitespace(name);
355   *outValue = trimWhitespace(value);
356   return true;
357 }
358
359 std::pair<std::string, LogHandlerConfig> parseHandlerConfig(StringPiece value) {
360   std::vector<StringPiece> pieces;
361   folly::split(",", value, pieces);
362   FOLLY_SAFE_DCHECK(
363       pieces.size() >= 1, "folly::split() always returns a list of length 1");
364
365   StringPiece handlerName;
366   StringPiece handlerType;
367   if (!splitNameValue(pieces[0], &handlerName, &handlerType)) {
368     throw LogConfigParseError{to<string>(
369         "error parsing log handler configuration \"",
370         pieces[0],
371         "\": expected data in the form NAME=TYPE")};
372   }
373   if (handlerName.empty()) {
374     throw LogConfigParseError{
375         "error parsing log handler configuration: empty log handler name"};
376   }
377   if (handlerType.empty()) {
378     throw LogConfigParseError{to<string>(
379         "error parsing configuration for log handler \"",
380         handlerName,
381         "\": empty log handler type")};
382   }
383
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 \"",
391           handlerName,
392           "\": options must be of the form NAME=VALUE")};
393     }
394
395     auto ret = config.options.emplace(optionName.str(), optionValue.str());
396     if (!ret.second) {
397       throw LogConfigParseError{to<string>(
398           "error parsing configuration for log handler \"",
399           handlerName,
400           "\": duplicate configuration for option \"",
401           optionName,
402           "\"")};
403     }
404   }
405
406   return std::make_pair(handlerName.str(), std::move(config));
407 }
408
409 } // namespace
410
411 LogConfig parseLogConfig(StringPiece value) {
412   value = trimWhitespace(value);
413   if (value.startsWith('{')) {
414     return parseLogConfigJson(value);
415   }
416
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);
422   FOLLY_SAFE_DCHECK(
423       pieces.size() >= 1, "folly::split() always returns a list of length 1");
424
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));
431     if (!ret.second) {
432       throw LogConfigParseError{to<string>(
433           "configuration for log category \"",
434           handlerInfo.first,
435           "\" specified multiple times")};
436     }
437   }
438
439   return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
440 }
441
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);
447 }
448
449 LogConfig parseLogConfigDynamic(const dynamic& value) {
450   if (!value.isObject()) {
451     throw LogConfigParseError{"JSON config input must be an object"};
452   }
453
454   std::unordered_map<string, string> seenCategories;
455   LogConfig::CategoryConfigMap categoryConfigs;
456   auto* categories = value.get_ptr("categories");
457   if (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")};
463     }
464
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"};
470       }
471       auto categoryName = entry.first.asString();
472       auto categoryConfig = parseJsonCategoryConfig(entry.second, categoryName);
473
474       // Check for multiple entries for the same category with different but
475       // equivalent names.
476       auto canonicalName = LogName::canonicalize(categoryName);
477       auto ret = seenCategories.emplace(canonicalName, categoryName);
478       if (!ret.second) {
479         throw LogConfigParseError{to<string>(
480             "category \"",
481             canonicalName,
482             "\" listed multiple times under different names: \"",
483             ret.first->second,
484             "\" and \"",
485             categoryName,
486             "\"")};
487       }
488
489       categoryConfigs[canonicalName] = std::move(categoryConfig);
490     }
491   }
492
493   LogConfig::HandlerConfigMap handlerConfigs;
494   auto* handlers = value.get_ptr("handlers");
495   if (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")};
501     }
502
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"};
508       }
509       auto handlerName = entry.first.asString();
510       handlerConfigs.emplace(
511           handlerName, parseJsonHandlerConfig(entry.second, handlerName));
512     }
513   }
514
515   return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
516 }
517
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));
522   }
523
524   dynamic handlers = dynamic::object;
525   for (const auto& entry : config.getHandlerConfigs()) {
526     handlers.insert(entry.first, logConfigToDynamic(entry.second));
527   }
528
529   return dynamic::object("categories", std::move(categories))(
530       "handlers", std::move(handlers));
531 }
532
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);
537   }
538   return dynamic::object("type", config.type)("options", options);
539 }
540
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);
548     }
549     value("handlers", std::move(handlers));
550   }
551   return value;
552 }
553
554 } // namespace folly