folly/folly/logging/test/LoggerTest.cpp

/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <folly/logging/Logger.h>

#include <fmt/format.h>
#include <folly/logging/LogCategory.h>
#include <folly/logging/LogHandler.h>
#include <folly/logging/LogMessage.h>
#include <folly/logging/LoggerDB.h>
#include <folly/logging/test/TestLogHandler.h>
#include <folly/portability/GMock.h>
#include <folly/portability/GTest.h>
#include <folly/test/TestUtils.h>

using namespace folly;
using std::make_shared;
using testing::MatchesRegex;

class LoggerTest : public ::testing::Test {
 protected:
  void SetUp() override {
    auto* category = logger_.getCategory();

    handler_ = make_shared<TestLogHandler>();
    category->addHandler(handler_);
    category->setLevel(LogLevel::DBG, true);
  }

  static StringPiece pathBasename(StringPiece path) {
    auto idx = path.rfind('/');
    if (idx == StringPiece::npos) {
      return path.str();
    }
    return path.subpiece(idx + 1);
  }

  LoggerDB db_{LoggerDB::TESTING};
  Logger logger_{&db_, "test"};
  std::shared_ptr<TestLogHandler> handler_;
};

TEST_F(LoggerTest, basic) {
  // Simple log message
  auto expectedLine = __LINE__ + 1;
  FB_LOG(logger_, WARN, "hello world");

  auto& messages = handler_->getMessages();
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ("hello world", messages[0].first.getMessage());
  EXPECT_EQ("LoggerTest.cpp", pathBasename(messages[0].first.getFileName()));
  EXPECT_EQ(expectedLine, messages[0].first.getLineNumber());
  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, subCategory) {
  // Log from a sub-category.
  Logger subLogger{&db_, "test.foo.bar"};
  auto expectedLine = __LINE__ + 1;
  FB_LOG(subLogger, ERR, "sub-category\nlog message");

  auto& messages = handler_->getMessages();
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ("sub-category\nlog message", messages[0].first.getMessage());
  EXPECT_EQ("LoggerTest.cpp", pathBasename(messages[0].first.getFileName()));
  EXPECT_EQ(expectedLine, messages[0].first.getLineNumber());
  EXPECT_EQ(LogLevel::ERR, messages[0].first.getLevel());
  EXPECT_TRUE(messages[0].first.containsNewlines());
  EXPECT_EQ(subLogger.getCategory(), messages[0].first.getCategory());
  EXPECT_EQ(logger_.getCategory(), messages[0].second);
}

TEST_F(LoggerTest, formatMessage) {
  auto expectedLine = __LINE__ + 1;
  FB_LOGF(logger_, WARN, "num events: {:06d}, duration: {:6.3f}", 1234, 5.6789);

  auto& messages = handler_->getMessages();
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ(
      "num events: 001234, duration:  5.679", messages[0].first.getMessage());
  EXPECT_EQ("LoggerTest.cpp", pathBasename(messages[0].first.getFileName()));
  EXPECT_EQ(expectedLine, messages[0].first.getLineNumber());
  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, formatError) {
  // If we pass in a bogus format string, logf() should not throw.
  // It should instead log a message, just complaining about the format error.
  FB_LOGF(
      logger_, WARN, "param1: {:06d}, param2: {:6.3f}", 1234, "hello world!");

  auto& messages = handler_->getMessages();
  ASSERT_EQ(1, messages.size());
  // Use a regex match here, since the type IDs are reported slightly
  // differently on different platforms.
  EXPECT_THAT(
      messages[0].first.getMessage(),
      MatchesRegex(R"(error formatting log message: )"
                   R"(.*invalid .* specifier; )"
                   R"(format string: "param1: \{:06d\}, param2: \{:6.3f\}", )"
                   R"(arguments: 1234, hello world!)"));
  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, toString) {
  // Use the log API that calls folly::to<string>
  auto expectedLine = __LINE__ + 1;
  FB_LOG(logger_, DBG5, "status=", 5, " name=", "foobar");

  auto& messages = handler_->getMessages();
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ("status=5 name=foobar", messages[0].first.getMessage());
  EXPECT_EQ("LoggerTest.cpp", pathBasename(messages[0].first.getFileName()));
  EXPECT_EQ(expectedLine, messages[0].first.getLineNumber());
  EXPECT_EQ(LogLevel::DBG5, 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);
}

class ToStringFailure {};
class FormattableButNoToString {
 public:
  explicit FormattableButNoToString(uint32_t v) : value(v) {}
  uint32_t value = 0;
};

namespace fmt {
template <>
struct formatter<ToStringFailure> : formatter<std::string> {
  auto format(ToStringFailure, format_context& ctx) const { return ctx.out(); }
};

template <>
struct formatter<FormattableButNoToString> : formatter<std::string> {
  auto format(FormattableButNoToString, format_context& ctx) const {
    throw std::runtime_error("test");
    return ctx.out();
  }
};
} // namespace fmt

// clang-format off
[[noreturn]] void toAppend(
    const ToStringFailure& /* arg */,
    std::string* /* result */) {
  throw std::runtime_error(
      "error converting ToStringFailure object to a string");
}
// clang-format on

TEST_F(LoggerTest, toStringError) {
  // Use the folly::to<string> log API, with an object that will throw
  // an exception when we try to convert it to a string.
  //
  // The logging code should not throw, but should instead log a message
  // with some detail about the failure.
  ToStringFailure obj;
  auto expectedLine = __LINE__ + 1;
  FB_LOG(logger_, DBG1, "status=", obj, " name=", "foobar");

  auto& messages = handler_->getMessages();
  ASSERT_EQ(1, messages.size());
  EXPECT_THAT(
      messages[0].first.getMessage(),
      MatchesRegex("error constructing log message: .*"
                   "error converting ToStringFailure object to a string"));
  EXPECT_EQ("LoggerTest.cpp", pathBasename(messages[0].first.getFileName()));
  EXPECT_EQ(expectedLine, messages[0].first.getLineNumber());
  EXPECT_EQ(LogLevel::DBG1, 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, 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_THAT(
      messages[0].first.getMessage(),
      MatchesRegex(R"(error formatting log message: .*format_error.*; )"
                   R"(format string: "param1: \{\}, param2: \{\}, \{\}", )"
                   R"(arguments: 1234, )"
                   R"(\[(.*ToStringFailure.*|object) of size (.*):.*\])"));
  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(0x1234cdef);
  FB_LOGF(logger_, WARN, "param1: {}, param2: {}", 1234, obj);

  std::string objectHex = kIsLittleEndian ? "ef cd 34 12" : "12 34 cd ef";
  auto expectedRegex =
      R"(error formatting log message: .*test; )"
      R"(format string: "param1: \{\}, param2: \{\}", )"
      R"(arguments: 1234, )"
      R"(\[(.*FormattableButNoToString.*|object) of size 4: )" +
      objectHex + R"(\])";

  auto& messages = handler_->getMessages();
  ASSERT_EQ(1, messages.size());
  EXPECT_THAT(messages[0].first.getMessage(), MatchesRegex(expectedRegex));
  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) {
  auto& messages = handler_->getMessages();

  // Test with only streaming arguments
  std::string foo = "bar";
  FB_LOG(logger_, WARN) << "foo=" << foo << ", test=0x" << std::hex << 35;
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ("foo=bar, test=0x23", 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);
  messages.clear();

  // Test with both function-style and streaming arguments
  FB_LOG(logger_, WARN, "foo=", foo) << " hello, " << "world: " << 34;
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ("foo=bar hello, world: 34", 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);
  messages.clear();

  // Test with format-style and streaming arguments
  FB_LOGF(logger_, WARN, "foo={}, x={}", foo, 34) << ", also " << 12;
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ("foo=bar, x=34, also 12", 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);
  messages.clear();
}

TEST_F(LoggerTest, escapeSequences) {
  // Escape characters (and any other unprintable characters) in the log
  // message should be escaped when logged.
  auto expectedLine = __LINE__ + 1;
  FB_LOG(logger_, WARN, "hello \033[34mworld\033[0m!");

  auto& messages = handler_->getMessages();
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ("hello \\x1b[34mworld\\x1b[0m!", messages[0].first.getMessage());
  EXPECT_EQ("LoggerTest.cpp", pathBasename(messages[0].first.getFileName()));
  EXPECT_EQ(expectedLine, messages[0].first.getLineNumber());
  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, logMacros) {
  Logger foo{&db_, "test.foo.bar"};
  Logger foobar{&db_, "test.foo.bar"};
  Logger footest{&db_, "test.foo.test"};
  Logger footest1234{&db_, "test.foo.test.1234"};
  Logger other{&db_, "test.other"};
  db_.setLevel("test", LogLevel::ERR);
  db_.setLevel("test.foo", LogLevel::DBG2);
  db_.setLevel("test.foo.test", LogLevel::DBG7);

  auto& messages = handler_->getMessages();

  // test.other's effective level should be INFO, so a DBG0
  // message to it should be discarded
  FB_LOG(other, DBG0, "this should be discarded");
  ASSERT_EQ(0, messages.size());

  // Disabled log messages should not evaluate their arguments
  bool argumentEvaluated = false;
  auto getValue = [&] {
    argumentEvaluated = true;
    return 5;
  };
  FB_LOG(foobar, DBG3, "discarded message: ", getValue());
  EXPECT_FALSE(argumentEvaluated);

  FB_LOG(foobar, DBG1, "this message should pass: ", getValue());
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ("this message should pass: 5", messages[0].first.getMessage());
  EXPECT_TRUE(argumentEvaluated);
  messages.clear();

  // Similar checks with FB_LOGF()
  argumentEvaluated = false;
  FB_LOGF(footest1234, DBG9, "failing log check: {}", getValue());
  EXPECT_FALSE(argumentEvaluated);

  FB_LOGF(footest1234, DBG5, "passing log: {:03}", getValue());
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ("passing log: 005", messages[0].first.getMessage());
  EXPECT_TRUE(argumentEvaluated);
  messages.clear();

  // Bad format arguments should not throw
  FB_LOGF(footest1234, ERR, "whoops: {}, {}", getValue());
  ASSERT_EQ(1, messages.size());
  EXPECT_THAT(
      messages[0].first.getMessage(),
      MatchesRegex(R"(error formatting log message: .*format_error.*; )"
                   R"(format string: "whoops: \{\}, \{\}", arguments: 5)"));
  messages.clear();
}

TEST_F(LoggerTest, logRawMacros) {
  Logger foobar{&db_, "test.foo.bar"};
  db_.setLevel("test.foo", LogLevel::DBG2);

  auto& messages = handler_->getMessages();

  FB_LOG_RAW(
      foobar,
      LogLevel::DBG1,
      "src/some/file.c",
      1234,
      "testFunction",
      "hello",
      ' ',
      1)
      << " world";
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ("hello 1 world", messages[0].first.getMessage());
  EXPECT_EQ("src/some/file.c", messages[0].first.getFileName());
  EXPECT_EQ("file.c", messages[0].first.getFileBaseName());
  EXPECT_EQ(1234, messages[0].first.getLineNumber());
  messages.clear();

  auto level = LogLevel::DBG1;
  FB_LOGF_RAW(
      foobar,
      level,
      "test/mytest.c",
      99,
      "testFunction",
      "{}: num={}",
      "test",
      42)
      << " plus extra stuff";
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ("test: num=42 plus extra stuff", messages[0].first.getMessage());
  EXPECT_EQ("test/mytest.c", messages[0].first.getFileName());
  EXPECT_EQ("mytest.c", messages[0].first.getFileBaseName());
  EXPECT_EQ(99, messages[0].first.getLineNumber());
  messages.clear();
}