• Adam Simpkins's avatar
    add a new logging library · e9c1c043
    Adam Simpkins authored
    Summary:
    This begins adding a new logging library for efficient, hierarchical logging.
    
    This includes the basic library framework, plus a README file with a brief
    overview and the motivation for creating a new logging library.
    
    Reviewed By: wez
    
    Differential Revision: D4911867
    
    fbshipit-source-id: 359623e11feeaa547f3ac2c369bf806ee6996554
    e9c1c043
LoggerTest.cpp 8.59 KB
/*
 * Copyright 2004-present Facebook, Inc.
 *
 * 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/experimental/logging/LogCategory.h>
#include <folly/experimental/logging/LogHandler.h>
#include <folly/experimental/logging/LogMessage.h>
#include <folly/experimental/logging/Logger.h>
#include <folly/experimental/logging/LoggerDB.h>
#include <folly/experimental/logging/test/TestLogHandler.h>
#include <folly/portability/GTest.h>

using namespace folly;
using std::make_shared;

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

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

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

TEST_F(LoggerTest, basic) {
  // Simple log message
  logger_.log(LogLevel::WARN, "src/myproject/myfile.cpp", 1234, "hello world");

  auto& messages = handler_->getMessages();
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ("hello world", messages[0].first.getMessage());
  EXPECT_EQ("src/myproject/myfile.cpp", messages[0].first.getFileName());
  EXPECT_EQ(1234, 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"};
  subLogger.log(LogLevel::ERROR, "myfile.cpp", 99, "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("myfile.cpp", messages[0].first.getFileName());
  EXPECT_EQ(99, messages[0].first.getLineNumber());
  EXPECT_EQ(LogLevel::ERROR, 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) {
  logger_.logf(
      LogLevel::WARN,
      "log.cpp",
      9,
      "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("log.cpp", messages[0].first.getFileName());
  EXPECT_EQ(9, 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, follyFormatError) {
  // If we pass in a bogus format string, logf() should not throw.
  // It should instead log a message, just complaining about the format error.
  logger_.logf(
      LogLevel::WARN,
      "log.cpp",
      9,
      "param1: {:06d}, param2: {:6.3f}",
      1234,
      "hello world!");

  auto& messages = handler_->getMessages();
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ(
      "error formatting log message: "
      "invalid format argument {:6.3f}: invalid specifier 'f'; "
      "format string: param1: {:06d}, param2: {:6.3f}",
      messages[0].first.getMessage());
  EXPECT_EQ("log.cpp", messages[0].first.getFileName());
  EXPECT_EQ(9, 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, toString) {
  // Use the log API that calls folly::to<string>
  logger_.log(LogLevel::DBG5, "log.cpp", 3, "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("log.cpp", messages[0].first.getFileName());
  EXPECT_EQ(3, 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 {};

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

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;
  logger_.log(LogLevel::DBG1, "log.cpp", 3, "status=", obj, " name=", "foobar");

  auto& messages = handler_->getMessages();
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ(
      "error constructing log message: "
      "error converting ToStringFailure object to a string",
      messages[0].first.getMessage());
  EXPECT_EQ("log.cpp", messages[0].first.getFileName());
  EXPECT_EQ(3, 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, escapeSequences) {
  // Escape characters (and any other unprintable characters) in the log
  // message should be escaped when logged.
  logger_.log(LogLevel::WARN, "termcap.cpp", 34, "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("termcap.cpp", messages[0].first.getFileName());
  EXPECT_EQ(34, 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::ERROR);
  db_.setLevel("test.foo", LogLevel::DBG2);
  db_.setLevel("test.foo.test", LogLevel::DBG7);

  auto& messages = handler_->getMessages();

  // test.other's effective level should be ERROR, so a warning
  // message to it should be discarded
  FB_LOG(other, WARN, "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, ERROR, "whoops: {}, {}", getValue());
  ASSERT_EQ(1, messages.size());
  EXPECT_EQ(
      "error formatting log message: "
      "invalid format argument {}: argument index out of range, max=1; "
      "format string: whoops: {}, {}",
      messages[0].first.getMessage());
  messages.clear();
}