Commit ba74a196 authored by Adam Simpkins's avatar Adam Simpkins Committed by Facebook Github Bot

logging: if folly::format() fails, also log the arguments

Summary:
If the folly::sformat() call fails in an FB_LOGF() statement, make a
best-effort attempt to log the format arguments as well, in addition to the
format string.  For each argument, folly::to<std::string>() is use if it is
supported for this argument.

This will help ensure that the arguments that were being logged aren't lost
even if the format string was incorrect.

Reviewed By: WillerZ

Differential Revision: D5082978

fbshipit-source-id: 0d56030e639cd7e8f2242bb43646ab4248c6a877
parent dc2c2837
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
#pragma once #pragma once
#include <folly/Conv.h> #include <folly/Conv.h>
#include <folly/Demangle.h>
#include <folly/Format.h> #include <folly/Format.h>
#include <folly/experimental/logging/LogCategory.h> #include <folly/experimental/logging/LogCategory.h>
#include <folly/experimental/logging/LogMessage.h> #include <folly/experimental/logging/LogMessage.h>
...@@ -24,6 +25,49 @@ namespace folly { ...@@ -24,6 +25,49 @@ namespace folly {
class LogStream; class LogStream;
/*
* Helper functions for fallback-formatting of arguments if folly::format()
* throws an exception.
*
* These are in a detail namespace so that we can include a using directive in
* order to do proper argument-dependent lookup of the correct toAppend()
* function to use.
*/
namespace detail {
/* using override */
using folly::toAppend;
template <typename Arg>
auto fallbackFormatOneArg(std::string* str, const Arg* arg, int) -> decltype(
toAppend(std::declval<Arg>(), std::declval<std::string*>()),
std::declval<void>()) {
str->push_back('(');
try {
#ifdef FOLLY_HAS_RTTI
toAppend(folly::demangle(typeid(*arg)), str);
str->append(": ");
#endif
toAppend(*arg, str);
} catch (const std::exception&) {
str->append("<error_converting_to_string>");
}
str->push_back(')');
}
template <typename Arg>
inline void fallbackFormatOneArg(std::string* str, const Arg* arg, long) {
str->push_back('(');
#ifdef FOLLY_HAS_RTTI
try {
toAppend(folly::demangle(typeid(*arg)), str);
str->append(": ");
} catch (const std::exception&) {
// Ignore the error
}
#endif
str->append("<no_string_conversion>)");
}
}
/** /**
* LogStreamProcessor receives a LogStream and logs it. * LogStreamProcessor receives a LogStream and logs it.
* *
...@@ -159,26 +203,50 @@ class LogStreamProcessor { ...@@ -159,26 +203,50 @@ class LogStreamProcessor {
* exceptions, but instead just log an error string when something goes wrong. * exceptions, but instead just log an error string when something goes wrong.
*/ */
template <typename... Args> template <typename... Args>
std::string formatLogString(folly::StringPiece fmt, Args&&... args) noexcept { std::string formatLogString(
folly::StringPiece fmt,
const Args&... args) noexcept {
try { try {
return folly::sformat(fmt, std::forward<Args>(args)...); return folly::sformat(fmt, args...);
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
// This most likely means that the caller had a bug in their format // This most likely means that the caller had a bug in their format
// string/arguments. Handle the exception here, rather than letting it // string/arguments. Handle the exception here, rather than letting it
// propagate up, since callers generally do not expect log statements to // propagate up, since callers generally do not expect log statements to
// throw. // throw.
// //
// Log the format string by itself, to help the developer at least // Log the format string and as much of the arguments as we can convert,
// identify the buggy format string in their code. // to aid debugging.
return folly::to<std::string>( std::string result;
"error formatting log message: ", result.append("error formatting log message: ");
ex.what(), result.append(ex.what());
"; format string: \"", result.append("; format string: \"");
fmt, result.append(fmt.data(), fmt.size());
"\""); result.append("\", arguments: ");
fallbackFormat(&result, args...);
return result;
} }
} }
/**
* Helper function generate a fallback version of the arguments in case
* folly::sformat() throws an exception.
*
* This attempts to convert each argument to a string using a similar
* mechanism to folly::to<std::string>(), if supported.
*/
template <typename Arg1, typename... Args>
void
fallbackFormat(std::string* str, const Arg1& arg1, const Args&... remainder) {
detail::fallbackFormatOneArg(str, &arg1, 0);
str->append(", ");
fallbackFormat(str, remainder...);
}
template <typename Arg>
void fallbackFormat(std::string* str, const Arg& arg) {
detail::fallbackFormatOneArg(str, &arg, 0);
}
const LogCategory* const category_; const LogCategory* const category_;
LogLevel const level_; LogLevel const level_;
folly::StringPiece filename_; folly::StringPiece filename_;
......
...@@ -107,7 +107,8 @@ TEST_F(LoggerTest, follyFormatError) { ...@@ -107,7 +107,8 @@ TEST_F(LoggerTest, follyFormatError) {
EXPECT_EQ( EXPECT_EQ(
"error formatting log message: " "error formatting log message: "
"invalid format argument {:6.3f}: invalid specifier 'f'; " "invalid format argument {:6.3f}: invalid specifier 'f'; "
"format string: \"param1: {:06d}, param2: {:6.3f}\"", "format string: \"param1: {:06d}, param2: {:6.3f}\", "
"arguments: (int: 1234), (char [13]: hello world!)",
messages[0].first.getMessage()); messages[0].first.getMessage());
EXPECT_EQ("LoggerTest.cpp", pathBasename(messages[0].first.getFileName())); EXPECT_EQ("LoggerTest.cpp", pathBasename(messages[0].first.getFileName()));
EXPECT_EQ(LogLevel::WARN, messages[0].first.getLevel()); EXPECT_EQ(LogLevel::WARN, messages[0].first.getLevel());
...@@ -133,6 +134,7 @@ TEST_F(LoggerTest, toString) { ...@@ -133,6 +134,7 @@ TEST_F(LoggerTest, toString) {
} }
class ToStringFailure {}; class ToStringFailure {};
class FormattableButNoToString {};
[[noreturn]] void toAppend( [[noreturn]] void toAppend(
const ToStringFailure& /* arg */, const ToStringFailure& /* arg */,
...@@ -141,6 +143,30 @@ class ToStringFailure {}; ...@@ -141,6 +143,30 @@ class ToStringFailure {};
"error converting ToStringFailure object to a string"); "error converting ToStringFailure object to a string");
} }
namespace folly {
template <>
class FormatValue<ToStringFailure> {
public:
explicit FormatValue(ToStringFailure) {}
template <class FormatCallback>
void format(FormatArg& arg, FormatCallback& cb) const {
FormatValue<std::string>("ToStringFailure").format(arg, cb);
}
};
template <>
class FormatValue<FormattableButNoToString> {
public:
explicit FormatValue(FormattableButNoToString) {}
template <class FormatCallback>
void format(FormatArg&, FormatCallback&) const {
throw std::runtime_error("test");
}
};
}
TEST_F(LoggerTest, toStringError) { TEST_F(LoggerTest, toStringError) {
// Use the folly::to<string> log API, with an object that will throw // Use the folly::to<string> log API, with an object that will throw
// an exception when we try to convert it to a string. // an exception when we try to convert it to a string.
...@@ -165,6 +191,47 @@ TEST_F(LoggerTest, toStringError) { ...@@ -165,6 +191,47 @@ TEST_F(LoggerTest, toStringError) {
EXPECT_EQ(logger_.getCategory(), messages[0].second); EXPECT_EQ(logger_.getCategory(), messages[0].second);
} }
TEST_F(LoggerTest, formatFallbackError) {
// Check the behavior if logf() fails, and toAppend() also fails.
ToStringFailure obj;
FB_LOGF(logger_, WARN, "param1: {}, param2: {}, {}", 1234, obj);
auto& messages = handler_->getMessages();
ASSERT_EQ(1, messages.size());
EXPECT_EQ(
"error formatting log message: "
"invalid format argument {}: argument index out of range, max=2; "
"format string: \"param1: {}, param2: {}, {}\", "
"arguments: (int: 1234), (ToStringFailure: <error_converting_to_string>)",
messages[0].first.getMessage());
EXPECT_EQ("LoggerTest.cpp", pathBasename(messages[0].first.getFileName()));
EXPECT_EQ(LogLevel::WARN, messages[0].first.getLevel());
EXPECT_FALSE(messages[0].first.containsNewlines());
EXPECT_EQ(logger_.getCategory(), messages[0].first.getCategory());
EXPECT_EQ(logger_.getCategory(), messages[0].second);
}
TEST_F(LoggerTest, formatFallbackUnsupported) {
// Check the behavior if logf() fails, and toAppend() also fails.
FormattableButNoToString obj;
FB_LOGF(logger_, WARN, "param1: {}, param2: {}", 1234, obj);
auto& messages = handler_->getMessages();
ASSERT_EQ(1, messages.size());
EXPECT_EQ(
"error formatting log message: "
"test; "
"format string: \"param1: {}, param2: {}\", "
"arguments: (int: 1234), "
"(FormattableButNoToString: <no_string_conversion>)",
messages[0].first.getMessage());
EXPECT_EQ("LoggerTest.cpp", pathBasename(messages[0].first.getFileName()));
EXPECT_EQ(LogLevel::WARN, messages[0].first.getLevel());
EXPECT_FALSE(messages[0].first.containsNewlines());
EXPECT_EQ(logger_.getCategory(), messages[0].first.getCategory());
EXPECT_EQ(logger_.getCategory(), messages[0].second);
}
TEST_F(LoggerTest, streamingArgs) { TEST_F(LoggerTest, streamingArgs) {
auto& messages = handler_->getMessages(); auto& messages = handler_->getMessages();
...@@ -270,7 +337,8 @@ TEST_F(LoggerTest, logMacros) { ...@@ -270,7 +337,8 @@ TEST_F(LoggerTest, logMacros) {
EXPECT_EQ( EXPECT_EQ(
"error formatting log message: " "error formatting log message: "
"invalid format argument {}: argument index out of range, max=1; " "invalid format argument {}: argument index out of range, max=1; "
"format string: \"whoops: {}, {}\"", "format string: \"whoops: {}, {}\", "
"arguments: (int: 5)",
messages[0].first.getMessage()); messages[0].first.getMessage());
messages.clear(); messages.clear();
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment