7e51472ebf1f4b99179a8357afa56202bc579600
[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 <cassert>
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::split() always returns a list of length 1
280     assert(handlerPieces.size() >= 1);
281     auto levelString = trimWhitespace(handlerPieces[0]);
282
283     bool hasHandlerConfig = handlerPieces.size() > 1;
284     if (handlerPieces.size() == 2 && trimWhitespace(handlerPieces[1]).empty()) {
285       // This is an explicitly empty handler list.
286       // This requests LoggerDB::updateConfig() to clear all existing log
287       // handlers from this category.
288       categoryConfig.handlers = std::vector<std::string>{};
289     } else if (hasHandlerConfig) {
290       categoryConfig.handlers = std::vector<std::string>{};
291       for (size_t n = 1; n < handlerPieces.size(); ++n) {
292         auto handlerName = trimWhitespace(handlerPieces[n]);
293         if (handlerName.empty()) {
294           throw LogConfigParseError{to<string>(
295               "error parsing configuration for log category \"",
296               categoryName,
297               "\": log handler name cannot be empty")};
298         }
299         categoryConfig.handlers->push_back(handlerName.str());
300       }
301     }
302
303     // Parse the levelString into a LogLevel
304     levelString = trimWhitespace(levelString);
305     try {
306       categoryConfig.level = stringToLogLevel(levelString);
307     } catch (const std::exception&) {
308       throw LogConfigParseError{to<string>(
309           "invalid log level \"",
310           levelString,
311           "\" for category \"",
312           categoryName,
313           "\"")};
314     }
315
316     // Check for multiple entries for the same category with different but
317     // equivalent names.
318     auto canonicalName = LogName::canonicalize(categoryName);
319     auto ret = seenCategories.emplace(canonicalName, categoryName.str());
320     if (!ret.second) {
321       throw LogConfigParseError{to<string>(
322           "category \"",
323           canonicalName,
324           "\" listed multiple times under different names: \"",
325           ret.first->second,
326           "\" and \"",
327           categoryName,
328           "\"")};
329     }
330
331     auto emplaceResult =
332         categoryConfigs.emplace(canonicalName, std::move(categoryConfig));
333     assert(emplaceResult.second);
334   }
335
336   return categoryConfigs;
337 }
338
339 bool splitNameValue(
340     StringPiece input,
341     StringPiece* outName,
342     StringPiece* outValue) {
343   size_t equalIndex = input.find('=');
344   if (equalIndex == StringPiece::npos) {
345     return false;
346   }
347
348   StringPiece name{input.begin(), input.begin() + equalIndex};
349   StringPiece value{input.begin() + equalIndex + 1, input.end()};
350
351   *outName = trimWhitespace(name);
352   *outValue = trimWhitespace(value);
353   return true;
354 }
355
356 std::pair<std::string, LogHandlerConfig> parseHandlerConfig(StringPiece value) {
357   std::vector<StringPiece> pieces;
358   folly::split(",", value, pieces);
359   // "folly::split() always returns a list of length 1";
360   assert(pieces.size() >= 1);
361
362   StringPiece handlerName;
363   StringPiece handlerType;
364   if (!splitNameValue(pieces[0], &handlerName, &handlerType)) {
365     throw LogConfigParseError{to<string>(
366         "error parsing log handler configuration \"",
367         pieces[0],
368         "\": expected data in the form NAME=TYPE")};
369   }
370   if (handlerName.empty()) {
371     throw LogConfigParseError{
372         "error parsing log handler configuration: empty log handler name"};
373   }
374   if (handlerType.empty()) {
375     throw LogConfigParseError{to<string>(
376         "error parsing configuration for log handler \"",
377         handlerName,
378         "\": empty log handler type")};
379   }
380
381   LogHandlerConfig config{handlerType};
382   for (size_t n = 1; n < pieces.size(); ++n) {
383     StringPiece optionName;
384     StringPiece optionValue;
385     if (!splitNameValue(pieces[n], &optionName, &optionValue)) {
386       throw LogConfigParseError{to<string>(
387           "error parsing configuration for log handler \"",
388           handlerName,
389           "\": options must be of the form NAME=VALUE")};
390     }
391
392     auto ret = config.options.emplace(optionName.str(), optionValue.str());
393     if (!ret.second) {
394       throw LogConfigParseError{to<string>(
395           "error parsing configuration for log handler \"",
396           handlerName,
397           "\": duplicate configuration for option \"",
398           optionName,
399           "\"")};
400     }
401   }
402
403   return std::make_pair(handlerName.str(), std::move(config));
404 }
405
406 } // namespace
407
408 LogConfig parseLogConfig(StringPiece value) {
409   value = trimWhitespace(value);
410   if (value.startsWith('{')) {
411     return parseLogConfigJson(value);
412   }
413
414   // Split the input string on semicolons.
415   // Everything up to the first semicolon specifies log category configs.
416   // From then on each section specifies a single LogHandler config.
417   std::vector<StringPiece> pieces;
418   folly::split(";", value, pieces);
419   // "folly::split() always returns a list of length 1";
420   assert(pieces.size() >= 1);
421
422   auto categoryConfigs = parseCategoryConfigs(pieces[0]);
423   LogConfig::HandlerConfigMap handlerConfigs;
424   for (size_t n = 1; n < pieces.size(); ++n) {
425     auto handlerInfo = parseHandlerConfig(pieces[n]);
426     auto ret = handlerConfigs.emplace(
427         handlerInfo.first, std::move(handlerInfo.second));
428     if (!ret.second) {
429       throw LogConfigParseError{to<string>(
430           "configuration for log category \"",
431           handlerInfo.first,
432           "\" specified multiple times")};
433     }
434   }
435
436   return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
437 }
438
439 LogConfig parseLogConfigJson(StringPiece value) {
440   json::serialization_opts opts;
441   opts.allow_trailing_comma = true;
442   auto jsonData = folly::parseJson(json::stripComments(value), opts);
443   return parseLogConfigDynamic(jsonData);
444 }
445
446 LogConfig parseLogConfigDynamic(const dynamic& value) {
447   if (!value.isObject()) {
448     throw LogConfigParseError{"JSON config input must be an object"};
449   }
450
451   std::unordered_map<string, string> seenCategories;
452   LogConfig::CategoryConfigMap categoryConfigs;
453   auto* categories = value.get_ptr("categories");
454   if (categories) {
455     if (!categories->isObject()) {
456       throw LogConfigParseError{to<string>(
457           "unexpected data type for log categories config: got ",
458           dynamicTypename(*categories),
459           ", expected an object")};
460     }
461
462     for (const auto& entry : categories->items()) {
463       if (!entry.first.isString()) {
464         // This shouldn't really ever happen.
465         // We deserialize the json with allow_non_string_keys set to False.
466         throw LogConfigParseError{"category name must be a string"};
467       }
468       auto categoryName = entry.first.asString();
469       auto categoryConfig = parseJsonCategoryConfig(entry.second, categoryName);
470
471       // Check for multiple entries for the same category with different but
472       // equivalent names.
473       auto canonicalName = LogName::canonicalize(categoryName);
474       auto ret = seenCategories.emplace(canonicalName, categoryName);
475       if (!ret.second) {
476         throw LogConfigParseError{to<string>(
477             "category \"",
478             canonicalName,
479             "\" listed multiple times under different names: \"",
480             ret.first->second,
481             "\" and \"",
482             categoryName,
483             "\"")};
484       }
485
486       categoryConfigs[canonicalName] = std::move(categoryConfig);
487     }
488   }
489
490   LogConfig::HandlerConfigMap handlerConfigs;
491   auto* handlers = value.get_ptr("handlers");
492   if (handlers) {
493     if (!handlers->isObject()) {
494       throw LogConfigParseError{to<string>(
495           "unexpected data type for log handlers config: got ",
496           dynamicTypename(*handlers),
497           ", expected an object")};
498     }
499
500     for (const auto& entry : handlers->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{"handler name must be a string"};
505       }
506       auto handlerName = entry.first.asString();
507       handlerConfigs.emplace(
508           handlerName, parseJsonHandlerConfig(entry.second, handlerName));
509     }
510   }
511
512   return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
513 }
514
515 dynamic logConfigToDynamic(const LogConfig& config) {
516   dynamic categories = dynamic::object;
517   for (const auto& entry : config.getCategoryConfigs()) {
518     categories.insert(entry.first, logConfigToDynamic(entry.second));
519   }
520
521   dynamic handlers = dynamic::object;
522   for (const auto& entry : config.getHandlerConfigs()) {
523     handlers.insert(entry.first, logConfigToDynamic(entry.second));
524   }
525
526   return dynamic::object("categories", std::move(categories))(
527       "handlers", std::move(handlers));
528 }
529
530 dynamic logConfigToDynamic(const LogHandlerConfig& config) {
531   dynamic options = dynamic::object;
532   for (const auto& opt : config.options) {
533     options.insert(opt.first, opt.second);
534   }
535   return dynamic::object("type", config.type)("options", options);
536 }
537
538 dynamic logConfigToDynamic(const LogCategoryConfig& config) {
539   auto value = dynamic::object("level", logLevelToString(config.level))(
540       "inherit", config.inheritParentLevel);
541   if (config.handlers.hasValue()) {
542     auto handlers = dynamic::array();
543     for (const auto& handlerName : config.handlers.value()) {
544       handlers.push_back(handlerName);
545     }
546     value("handlers", std::move(handlers));
547   }
548   return value;
549 }
550
551 } // namespace folly