Adds writer test case for RCU
[folly.git] / folly / experimental / logging / LogConfigParser.cpp
1 /*
2  * Copyright 2017-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   // Parse the handler name and optional type
361   auto colonIndex = value.find(':');
362   StringPiece namePortion;
363   StringPiece optionsStr;
364   if (colonIndex == StringPiece::npos) {
365     namePortion = value;
366   } else {
367     namePortion = StringPiece{value.begin(), value.begin() + colonIndex};
368     optionsStr = StringPiece{value.begin() + colonIndex + 1, value.end()};
369   }
370
371   StringPiece handlerName;
372   Optional<StringPiece> handlerType(in_place);
373   if (!splitNameValue(namePortion, &handlerName, &handlerType.value())) {
374     handlerName = trimWhitespace(namePortion);
375     handlerType = folly::none;
376   }
377
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"};
385   }
386   if (handlerName.contains(',')) {
387     throw LogConfigParseError{to<string>(
388         "error parsing configuration for log handler \"",
389         handlerName,
390         "\": name cannot contain a comma when using the basic config format")};
391   }
392   if (handlerType.hasValue()) {
393     if (handlerType->empty()) {
394       throw LogConfigParseError{to<string>(
395           "error parsing configuration for log handler \"",
396           handlerName,
397           "\": empty log handler type")};
398     }
399     if (handlerType->contains(',')) {
400       throw LogConfigParseError{to<string>(
401           "error parsing configuration for log handler \"",
402           handlerName,
403           "\": invalid type \"",
404           handlerType.value(),
405           "\": type name cannot contain a comma when using "
406           "the basic config format")};
407     }
408   }
409
410   // Parse the options
411   LogHandlerConfig config{handlerType};
412   optionsStr = trimWhitespace(optionsStr);
413   if (!optionsStr.empty()) {
414     std::vector<StringPiece> pieces;
415     folly::split(",", optionsStr, pieces);
416     FOLLY_SAFE_DCHECK(
417         pieces.size() >= 1, "folly::split() always returns a list of length 1");
418
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 \"",
425             handlerName,
426             "\": options must be of the form NAME=VALUE")};
427       }
428
429       auto ret = config.options.emplace(optionName.str(), optionValue.str());
430       if (!ret.second) {
431         throw LogConfigParseError{to<string>(
432             "error parsing configuration for log handler \"",
433             handlerName,
434             "\": duplicate configuration for option \"",
435             optionName,
436             "\"")};
437       }
438     }
439   }
440
441   return std::make_pair(handlerName.str(), std::move(config));
442 }
443
444 } // namespace
445
446 LogConfig parseLogConfig(StringPiece value) {
447   value = trimWhitespace(value);
448   if (value.startsWith('{')) {
449     return parseLogConfigJson(value);
450   }
451
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);
457   FOLLY_SAFE_DCHECK(
458       pieces.size() >= 1, "folly::split() always returns a list of length 1");
459
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));
466     if (!ret.second) {
467       throw LogConfigParseError{to<string>(
468           "configuration for log category \"",
469           handlerInfo.first,
470           "\" specified multiple times")};
471     }
472   }
473
474   return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
475 }
476
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);
482 }
483
484 LogConfig parseLogConfigDynamic(const dynamic& value) {
485   if (!value.isObject()) {
486     throw LogConfigParseError{"JSON config input must be an object"};
487   }
488
489   std::unordered_map<string, string> seenCategories;
490   LogConfig::CategoryConfigMap categoryConfigs;
491   auto* categories = value.get_ptr("categories");
492   if (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")};
498     }
499
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"};
505       }
506       auto categoryName = entry.first.asString();
507       auto categoryConfig = parseJsonCategoryConfig(entry.second, categoryName);
508
509       // Check for multiple entries for the same category with different but
510       // equivalent names.
511       auto canonicalName = LogName::canonicalize(categoryName);
512       auto ret = seenCategories.emplace(canonicalName, categoryName);
513       if (!ret.second) {
514         throw LogConfigParseError{to<string>(
515             "category \"",
516             canonicalName,
517             "\" listed multiple times under different names: \"",
518             ret.first->second,
519             "\" and \"",
520             categoryName,
521             "\"")};
522       }
523
524       categoryConfigs[canonicalName] = std::move(categoryConfig);
525     }
526   }
527
528   LogConfig::HandlerConfigMap handlerConfigs;
529   auto* handlers = value.get_ptr("handlers");
530   if (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")};
536     }
537
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"};
543       }
544       auto handlerName = entry.first.asString();
545       handlerConfigs.emplace(
546           handlerName, parseJsonHandlerConfig(entry.second, handlerName));
547     }
548   }
549
550   return LogConfig{std::move(handlerConfigs), std::move(categoryConfigs)};
551 }
552
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));
557   }
558
559   dynamic handlers = dynamic::object;
560   for (const auto& entry : config.getHandlerConfigs()) {
561     handlers.insert(entry.first, logConfigToDynamic(entry.second));
562   }
563
564   return dynamic::object("categories", std::move(categories))(
565       "handlers", std::move(handlers));
566 }
567
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);
572   }
573   auto result = dynamic::object("options", options);
574   if (config.type.hasValue()) {
575     result("type", config.type.value());
576   }
577   return std::move(result);
578 }
579
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);
587     }
588     value("handlers", std::move(handlers));
589   }
590   return std::move(value);
591 }
592
593 } // namespace folly