| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 "paimon/logging.h" |
| |
| #include <atomic> |
| #include <cstdarg> |
| #include <future> |
| #include <iostream> |
| #include <memory> |
| #include <random> |
| #include <string> |
| #include <thread> |
| #include <vector> |
| |
| #include "glog/log_severity.h" |
| #include "glog/logging.h" |
| #include "glog/raw_logging.h" |
| #include "paimon/common/executor/future.h" |
| #include "paimon/executor.h" |
| #include "paimon/fs/file_system.h" |
| #include "paimon/testing/utils/testharness.h" |
| namespace paimon::test { |
| namespace { |
| |
| std::atomic<int> g_creator_calls{0}; |
| |
| // A Logger that forwards to glog exactly like the built-in adaptor. It is used to |
| // restore behavior-preserving logging after exercising the custom-creator path, |
| // because the registry cannot be reset to "unset" through the public API. |
| class GlogForwardingLogger : public Logger { |
| public: |
| void LogV(PaimonLogLevel level, const char* fname, int lineno, const char* /*function*/, |
| const char* fmt, ...) override { |
| va_list args; |
| va_start(args, fmt); |
| google::RawLog__(ToGlog(level), fname, lineno, fmt, args); |
| va_end(args); |
| } |
| |
| bool IsLevelEnabled(PaimonLogLevel /*level*/) const override { |
| return true; |
| } |
| |
| private: |
| static google::LogSeverity ToGlog(PaimonLogLevel level) { |
| switch (level) { |
| case PAIMON_LOG_LEVEL_WARN: |
| return google::GLOG_WARNING; |
| case PAIMON_LOG_LEVEL_ERROR: |
| return google::GLOG_ERROR; |
| default: |
| return google::GLOG_INFO; |
| } |
| } |
| }; |
| |
| } // namespace |
| TEST(LoggerTest, TestMultiThreadGetLogger) { |
| ASSERT_OK_AND_ASSIGN(auto executor, CreateDefaultExecutor(/*thread_count=*/4)); |
| auto get_logger = []() { |
| auto logger = Logger::GetLogger("my_log"); |
| ASSERT_TRUE(logger); |
| }; |
| |
| std::vector<std::future<void>> futures; |
| for (int32_t i = 0; i < 1000; ++i) { |
| futures.push_back(Via(executor.get(), get_logger)); |
| } |
| Wait(futures); |
| } |
| |
| TEST(LoggerTest, TestLogAllSeverities) { |
| // The default logger routes every PaimonLogLevel through the severity mapping. |
| auto logger = Logger::GetLogger("severity_test"); |
| ASSERT_TRUE(logger); |
| |
| logger->LogV(PAIMON_LOG_LEVEL_DEBUG, __FILE__, __LINE__, __FUNCTION__, "debug severity"); |
| logger->LogV(PAIMON_LOG_LEVEL_INFO, __FILE__, __LINE__, __FUNCTION__, "info severity"); |
| logger->LogV(PAIMON_LOG_LEVEL_WARN, __FILE__, __LINE__, __FUNCTION__, "warn severity"); |
| logger->LogV(PAIMON_LOG_LEVEL_ERROR, __FILE__, __LINE__, __FUNCTION__, "error severity"); |
| // NONE and MAX fall into the default branch of the mapping. |
| logger->LogV(PAIMON_LOG_LEVEL_NONE, __FILE__, __LINE__, __FUNCTION__, "none severity"); |
| logger->LogV(PAIMON_LOG_LEVEL_MAX, __FILE__, __LINE__, __FUNCTION__, "max severity"); |
| } |
| |
| // Demonstrates that glog's normal LOG() path actually writes a log file to disk. |
| // Note: Paimon's Logger/GlogAdaptor uses google::RawLog__, which only goes to |
| // stderr and never touches disk, so this test drives glog's file sink directly. |
| TEST(LoggerTest, TestGlogWritesLogFileToDisk) { |
| // Save the global glog flags we are about to change so other tests are unaffected. |
| const bool prev_logtostderr = FLAGS_logtostderr; |
| const bool prev_timestamp_in_name = FLAGS_timestamp_in_logfile_name; |
| const int32_t prev_minloglevel = FLAGS_minloglevel; |
| |
| // A unique, empty directory so the only file inside is the one glog creates for us. |
| // The directory (and everything glog wrote into it) is removed on destruction. |
| auto tmp_dir = UniqueTestDirectory::Create(); |
| ASSERT_TRUE(tmp_dir); |
| std::shared_ptr<FileSystem> fs = tmp_dir->GetFileSystem(); |
| const std::string base = tmp_dir->Str() + "/paimon_demo"; |
| |
| FLAGS_logtostderr = false; // must be false, otherwise glog skips the file sink |
| FLAGS_timestamp_in_logfile_name = false; // deterministic file name (no time/pid suffix) |
| FLAGS_minloglevel = google::GLOG_INFO; // do not filter out INFO |
| |
| if (!google::IsGoogleLoggingInitialized()) { |
| google::InitGoogleLogging("paimon-log-disk-test"); |
| } |
| // Force INFO logs to a file under our unique directory. Disable the |
| // "<program>.INFO" symlink glog normally creates next to the log file: it would |
| // dangle during recursive deletion and break UniqueTestDirectory cleanup. |
| google::SetLogDestination(google::GLOG_INFO, base.c_str()); |
| google::SetLogSymlink(google::GLOG_INFO, ""); |
| |
| const std::string token = "PAIMON_DISK_LOG_DEMO_" + std::to_string(RandomNumber(0, 1'000'000)); |
| LOG(INFO) << "hello from disk logging, token=" << token; |
| google::FlushLogFiles(google::GLOG_INFO); |
| |
| // Collect the content of whatever file glog created in our directory. |
| std::string on_disk_path; |
| std::string content; |
| std::vector<std::unique_ptr<BasicFileStatus>> entries; |
| ASSERT_OK(fs->ListDir(tmp_dir->Str(), &entries)); |
| for (const auto& entry : entries) { |
| if (entry->IsDir()) { |
| continue; |
| } |
| std::string file_content; |
| ASSERT_OK(fs->ReadFile(entry->GetPath(), &file_content)); |
| if (file_content.find(token) != std::string::npos) { |
| on_disk_path = entry->GetPath(); |
| content = std::move(file_content); |
| break; |
| } |
| } |
| |
| // Show the real on-disk file and its raw content so it can be eyeballed in test output. |
| std::cout << "\n===== on-disk glog file: " << on_disk_path << " =====\n" |
| << content << "===== end of file =====\n"; |
| |
| ASSERT_FALSE(on_disk_path.empty()) |
| << "no glog file containing the token was written to " << tmp_dir->Str(); |
| ASSERT_NE(content.find(token), std::string::npos); |
| |
| // Stop writing INFO logs to the directory that is deleted when tmp_dir goes out of |
| // scope, then restore flags. |
| google::SetLogDestination(google::GLOG_INFO, ""); |
| FLAGS_logtostderr = prev_logtostderr; |
| FLAGS_timestamp_in_logfile_name = prev_timestamp_in_name; |
| FLAGS_minloglevel = prev_minloglevel; |
| } |
| |
| // Keep this test last: it installs a process-wide logger creator that cannot be |
| // unset, so it must not run before tests that rely on the default logger. |
| TEST(LoggerTest, TestRegisterCustomLoggerCreator) { |
| g_creator_calls.store(0); |
| Logger::RegisterLogger([](const std::string& /*path*/) -> std::unique_ptr<Logger> { |
| g_creator_calls.fetch_add(1); |
| return std::make_unique<GlogForwardingLogger>(); |
| }); |
| |
| auto logger = Logger::GetLogger("custom_path"); |
| ASSERT_TRUE(logger); |
| // GetLogger must have gone through the registered creator branch. |
| ASSERT_EQ(1, g_creator_calls.load()); |
| ASSERT_TRUE(logger->IsLevelEnabled(PAIMON_LOG_LEVEL_INFO)); |
| } |
| } // namespace paimon::test |