folly/folly/logging/test/FileHandlerFactoryTest.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/FileHandlerFactory.h>

#include <folly/Exception.h>
#include <folly/logging/AsyncFileWriter.h>
#include <folly/logging/GlogStyleFormatter.h>
#include <folly/logging/ImmediateFileWriter.h>
#include <folly/logging/StandardLogHandler.h>
#include <folly/logging/StreamHandlerFactory.h>
#include <folly/portability/GTest.h>
#include <folly/test/TestUtils.h>
#include <folly/testing/TestUtil.h>

using namespace folly;
using folly::test::TemporaryFile;
using std::make_pair;

void checkAsyncWriter(
    const LogWriter* writer,
    const char* expectedPath,
    size_t expectedMaxBufferSize) {
  auto asyncWriter = dynamic_cast<const AsyncFileWriter*>(writer);
  ASSERT_TRUE(asyncWriter)
      << "handler factory should have created an AsyncFileWriter";
  EXPECT_EQ(expectedMaxBufferSize, asyncWriter->getMaxBufferSize());

  // Make sure this refers to the expected output file
  struct stat expectedStatInfo;
  checkUnixError(stat(expectedPath, &expectedStatInfo), "stat failed");
  struct stat actualStatInfo;
  checkUnixError(
      fstat(asyncWriter->getFile().fd(), &actualStatInfo), "fstat failed");
  EXPECT_EQ(expectedStatInfo.st_dev, actualStatInfo.st_dev);
  EXPECT_EQ(expectedStatInfo.st_ino, actualStatInfo.st_ino);
}

void checkAsyncWriter(
    const LogWriter* writer, int expectedFD, size_t expectedMaxBufferSize) {
  auto asyncWriter = dynamic_cast<const AsyncFileWriter*>(writer);
  ASSERT_TRUE(asyncWriter)
      << "handler factory should have created an AsyncFileWriter";
  EXPECT_EQ(expectedMaxBufferSize, asyncWriter->getMaxBufferSize());
  EXPECT_EQ(expectedFD, asyncWriter->getFile().fd());
}

TEST(FileHandlerFactory, pathOnly) {
  FileHandlerFactory factory;

  TemporaryFile tmpFile{"logging_test"};
  auto options = LogHandlerFactory::Options{
      make_pair("path", tmpFile.path().string()),
  };
  auto handler = factory.createHandler(options);

  auto stdHandler = std::dynamic_pointer_cast<StandardLogHandler>(handler);
  ASSERT_TRUE(stdHandler);

  auto formatter =
      std::dynamic_pointer_cast<GlogStyleFormatter>(stdHandler->getFormatter());
  EXPECT_TRUE(formatter)
      << "handler factory should have created a GlogStyleFormatter";

  checkAsyncWriter(
      stdHandler->getWriter().get(),
      tmpFile.path().string().c_str(),
      AsyncFileWriter::kDefaultMaxBufferSize);
}

TEST(StreamHandlerFactory, stderrStream) {
  StreamHandlerFactory factory;

  TemporaryFile tmpFile{"logging_test"};
  auto options = StreamHandlerFactory::Options{
      make_pair("stream", "stderr"),
  };
  auto handler = factory.createHandler(options);

  auto stdHandler = std::dynamic_pointer_cast<StandardLogHandler>(handler);
  ASSERT_TRUE(stdHandler);

  auto formatter =
      std::dynamic_pointer_cast<GlogStyleFormatter>(stdHandler->getFormatter());
  EXPECT_TRUE(formatter)
      << "handler factory should have created a GlogStyleFormatter";

  checkAsyncWriter(
      stdHandler->getWriter().get(),
      STDERR_FILENO,
      AsyncFileWriter::kDefaultMaxBufferSize);
}

TEST(StreamHandlerFactory, stdoutWithMaxBuffer) {
  StreamHandlerFactory factory;

  TemporaryFile tmpFile{"logging_test"};
  auto options = StreamHandlerFactory::Options{
      make_pair("stream", "stdout"),
      make_pair("max_buffer_size", "4096"),
  };
  auto handler = factory.createHandler(options);

  auto stdHandler = std::dynamic_pointer_cast<StandardLogHandler>(handler);
  ASSERT_TRUE(stdHandler);

  auto formatter =
      std::dynamic_pointer_cast<GlogStyleFormatter>(stdHandler->getFormatter());
  EXPECT_TRUE(formatter)
      << "handler factory should have created a GlogStyleFormatter";

  checkAsyncWriter(stdHandler->getWriter().get(), STDOUT_FILENO, 4096);
}

TEST(FileHandlerFactory, pathWithMaxBufferSize) {
  FileHandlerFactory factory;

  TemporaryFile tmpFile{"logging_test"};
  auto options = LogHandlerFactory::Options{
      make_pair("path", tmpFile.path().string()),
      make_pair("max_buffer_size", "4096000"),
  };
  auto handler = factory.createHandler(options);

  auto stdHandler = std::dynamic_pointer_cast<StandardLogHandler>(handler);
  ASSERT_TRUE(stdHandler);

  auto formatter =
      std::dynamic_pointer_cast<GlogStyleFormatter>(stdHandler->getFormatter());
  EXPECT_TRUE(formatter)
      << "handler factory should have created a GlogStyleFormatter";

  checkAsyncWriter(
      stdHandler->getWriter().get(), tmpFile.path().string().c_str(), 4096000);
}

TEST(StreamHandlerFactory, nonAsyncStderr) {
  StreamHandlerFactory factory;

  TemporaryFile tmpFile{"logging_test"};
  auto options = LogHandlerFactory::Options{
      make_pair("stream", "stderr"),
      make_pair("async", "no"),
  };
  auto handler = factory.createHandler(options);

  auto stdHandler = std::dynamic_pointer_cast<StandardLogHandler>(handler);
  ASSERT_TRUE(stdHandler);

  auto formatter =
      std::dynamic_pointer_cast<GlogStyleFormatter>(stdHandler->getFormatter());
  EXPECT_TRUE(formatter)
      << "handler factory should have created a GlogStyleFormatter";

  auto writer =
      std::dynamic_pointer_cast<ImmediateFileWriter>(stdHandler->getWriter());
  ASSERT_TRUE(writer);
  EXPECT_EQ(STDERR_FILENO, writer->getFile().fd());
}

TEST(FileHandlerFactory, errors) {
  FileHandlerFactory factory;
  TemporaryFile tmpFile{"logging_test"};
  using Options = LogHandlerFactory::Options;

  {
    auto options = Options{};
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "no path specified for file handler");
  }

  {
    auto options = Options{
        {"path", tmpFile.path().string()},
        {"stream", "stderr"},
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "unknown option \"stream\"");
  }

  {
    auto options = Options{
        {"stream", "nonstdout"},
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "unknown option \"stream\"");
  }

  {
    auto options = Options{
        {"path", tmpFile.path().string()},
        {"async", "xyz"},
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "^error processing option \"async\": Invalid value for bool: \"xyz\"$");
  }

  {
    auto options = Options{
        {"path", tmpFile.path().string()},
        {"async", "false"},
        {"max_buffer_size", "1234"},
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "the \"max_buffer_size\" option is only valid for async file handlers");
  }

  {
    auto options = Options{
        {"path", tmpFile.path().string()},
        {"max_buffer_size", "hello"},
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "^error processing option \"max_buffer_size\": "
        "Non-digit character found: \"hello\"$");
  }

  {
    auto options = Options{
        {"path", tmpFile.path().string()},
        {"max_buffer_size", "0"},
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "^error processing option \"max_buffer_size\": "
        "must be a positive integer$");
  }

  {
    auto options = Options{
        {"path", tmpFile.path().string()},
        {"foo", "bar"},
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "^unknown option \"foo\"$");
  }
}

TEST(StreamHandlerFactory, errors) {
  StreamHandlerFactory factory;
  using Options = LogHandlerFactory::Options;

  {
    auto options = Options{};
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "no stream name specified for stream handler");
  }

  {
    auto options = Options{
        {"path", "/tmp/log.txt"},
        {"stream", "stderr"},
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "unknown option \"path\"");
  }

  {
    auto options = Options{
        {"stream", "nonstdout"},
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "unknown stream \"nonstdout\": expected one of stdout or stderr");
  }

  {
    auto options = Options{
        {"stream", "stderr"},
        {"async", "xyz"},
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "^error processing option \"async\": Invalid value for bool: \"xyz\"$");
  }

  {
    auto options = Options{
        {"stream", "stderr"},
        {"async", "false"},
        {"max_buffer_size", "1234"},
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "^the \"max_buffer_size\" option is only valid for "
        "async file handlers$");
  }

  {
    auto options = Options{
        {"stream", "stderr"},
        {"max_buffer_size", "hello"},
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "^error processing option \"max_buffer_size\": "
        "Non-digit character found: \"hello\"$");
  }

  {
    auto options = Options{
        {"stream", "stderr"},
        {"max_buffer_size", "0"},
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "^error processing option \"max_buffer_size\": "
        "must be a positive integer$");
  }

  {
    auto options = Options{
        make_pair("stream", "stderr"),
        make_pair("foo", "bar"),
    };
    EXPECT_THROW_RE(
        factory.createHandler(options),
        std::invalid_argument,
        "unknown option \"foo\"");
  }
}

TEST(StreamHandlerFactory, writerFactory) {
  StreamHandlerFactory::WriterFactory factory;
  factory.processOption("stream", "stderr");
  {
    auto writer = factory.createWriter();
    checkAsyncWriter(
        writer.get(), STDERR_FILENO, AsyncFileWriter::kDefaultMaxBufferSize);
  }

  // For duplicate option, prefer the latter
  factory.processOption("stream", "stdout");
  {
    auto writer = factory.createWriter();
    checkAsyncWriter(
        writer.get(), STDOUT_FILENO, AsyncFileWriter::kDefaultMaxBufferSize);
  }
}

TEST(StreamHandlerFactory, writerFactoryError) {
  {
    StreamHandlerFactory::WriterFactory factory;
    factory.processOption("stream", "nope");
    EXPECT_THROW_RE(
        factory.createWriter(),
        std::invalid_argument,
        "^unknown stream \"nope\": expected one of stdout or stderr$");
  }
  {
    StreamHandlerFactory::WriterFactory factory;
    EXPECT_THROW_RE(
        factory.createWriter(),
        std::invalid_argument,
        "^no stream name specified for stream handler$");
  }
  {
    StreamHandlerFactory::WriterFactory factory;
    factory.processOption("stream", "");
    EXPECT_THROW_RE(
        factory.createWriter(),
        std::invalid_argument,
        "^no stream name specified for stream handler$");
  }
}