diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt index 747f8a10f5dfd..ac37cb9c91d22 100644 --- a/cpp/src/arrow/CMakeLists.txt +++ b/cpp/src/arrow/CMakeLists.txt @@ -509,6 +509,7 @@ set(ARROW_UTIL_SRCS util/io_util.cc util/list_util.cc util/logging.cc + util/logging_v2.cc util/key_value_metadata.cc util/memory.cc util/mutex.cc diff --git a/cpp/src/arrow/telemetry/logging.cc b/cpp/src/arrow/telemetry/logging.cc index f8b9b8a4ee3bc..79114a187939b 100644 --- a/cpp/src/arrow/telemetry/logging.cc +++ b/cpp/src/arrow/telemetry/logging.cc @@ -226,13 +226,6 @@ otel_shared_ptr MakeLoggerProvider( new otel::logs::NoopLoggerProvider{}); } -class NoopLogger : public Logger { - public: - void Log(const LogDescriptor&) override {} - bool Flush(std::chrono::microseconds) override { return true; } - std::string_view name() const override { return "noop"; } -}; - class OtelLogger : public Logger { public: OtelLogger(LoggingOptions options, otel_shared_ptr ot_logger) @@ -276,7 +269,7 @@ class OtelLogger : public Logger { logger_->EmitLogRecord(std::move(log)); if (desc.severity >= options_.flush_severity) { - Logger::Flush(); + util::Logger::Flush(); } } @@ -284,6 +277,12 @@ class OtelLogger : public Logger { return GlobalLoggerProvider::Flush(timeout); } + bool is_enabled() const override { return true; } + + util::ArrowLogLevel severity_threshold() const override { + return options_.severity_threshold; + } + std::string_view name() const override { otel_string_view s = logger_->GetName(); return std::string_view(s.data(), s.length()); @@ -319,47 +318,19 @@ bool GlobalLoggerProvider::Flush(std::chrono::microseconds timeout) { return false; } -Result> GlobalLoggerProvider::MakeLogger( +Result> GlobalLoggerProvider::MakeLogger( std::string_view name, const LoggingOptions& options, const AttributeHolder& attributes) { auto ot_logger = otel::logs::Provider::GetLoggerProvider()->GetLogger( ToOtel(name), /*library_name=*/"", /*library_version=*/"", /*schema_url=*/"", OtelAttributeHolder(attributes)); - return std::make_unique(options, std::move(ot_logger)); + return std::make_shared(options, std::move(ot_logger)); } -Result> GlobalLoggerProvider::MakeLogger( +Result> GlobalLoggerProvider::MakeLogger( std::string_view name, const LoggingOptions& options) { return MakeLogger(name, options, EmptyAttributeHolder{}); } -std::unique_ptr GlobalLogger::logger_ = nullptr; - -std::unique_ptr MakeNoopLogger() { return std::make_unique(); } - -class LogMessage::Impl { - public: - Impl(LogLevel severity, Logger* logger) : logger(logger), severity(severity) {} - - ~Impl() { - if (logger) { - auto body = stream.str(); - LogDescriptor desc; - desc.body = body; - desc.severity = severity; - logger->Log(desc); - } - } - - Logger* logger; - LogLevel severity; - std::stringstream stream; -}; - -LogMessage::LogMessage(LogLevel severity, Logger* logger) - : impl_(std::make_shared(severity, logger)) {} - -std::ostream& LogMessage::Stream() { return impl_->stream; } - } // namespace telemetry } // namespace arrow diff --git a/cpp/src/arrow/telemetry/logging.h b/cpp/src/arrow/telemetry/logging.h index 30f91215e8762..1a8e40dfaf726 100644 --- a/cpp/src/arrow/telemetry/logging.h +++ b/cpp/src/arrow/telemetry/logging.h @@ -28,6 +28,7 @@ #include "arrow/status.h" #include "arrow/util/config.h" #include "arrow/util/logging.h" +#include "arrow/util/logging_v2.h" #include "arrow/util/macros.h" #include "arrow/util/visibility.h" @@ -106,12 +107,20 @@ struct LogDescriptor { const AttributeHolder* attributes = NULLPTR; }; -class ARROW_EXPORT Logger { +class ARROW_EXPORT Logger : public util::Logger { public: virtual ~Logger() = default; virtual void Log(const LogDescriptor&) = 0; + void Log(const util::LogDetails& details) override { + LogDescriptor desc; + desc.body = details.message; + desc.severity = details.severity; + desc.timestamp = details.timestamp; + this->Log(desc); + } + void Log(LogLevel severity, std::string_view body, const AttributeHolder& attributes, EventId event_id = EventId::Invalid()) { LogDescriptor desc; @@ -147,14 +156,9 @@ class ARROW_EXPORT Logger { this->Log(desc); } - virtual bool Flush(std::chrono::microseconds timeout) = 0; - bool Flush() { return this->Flush(std::chrono::microseconds::max()); } - virtual std::string_view name() const = 0; }; -ARROW_EXPORT std::unique_ptr MakeNoopLogger(); - class ARROW_EXPORT GlobalLoggerProvider { public: static Status Initialize( @@ -164,50 +168,12 @@ class ARROW_EXPORT GlobalLoggerProvider { static bool Flush(std::chrono::microseconds timeout = std::chrono::microseconds::max()); - static Result> MakeLogger( + static Result> MakeLogger( std::string_view name, const LoggingOptions& options = LoggingOptions::Defaults()); - static Result> MakeLogger(std::string_view name, + static Result> MakeLogger(std::string_view name, const LoggingOptions& options, const AttributeHolder& attributes); }; -class ARROW_EXPORT GlobalLogger { - public: - static Logger* Get() { - if (!logger_) { - logger_ = MakeNoopLogger(); - } - return logger_.get(); - } - - static void Set(std::unique_ptr logger) { - if (logger) { - logger_ = std::move(logger); - } else { - logger_ = MakeNoopLogger(); - } - } - - private: - static std::unique_ptr logger_; -}; - -class ARROW_EXPORT LogMessage { - public: - explicit LogMessage(LogLevel, Logger*); - - std::ostream& Stream(); - - template - LogMessage& operator<<(const T& t) { - Stream() << t; - return *this; - } - - private: - class Impl; - std::shared_ptr impl_; -}; - } // namespace telemetry } // namespace arrow diff --git a/cpp/src/arrow/telemetry/telemetry_test.cc b/cpp/src/arrow/telemetry/telemetry_test.cc index 5e494aa268afd..96546663ab5c8 100644 --- a/cpp/src/arrow/telemetry/telemetry_test.cc +++ b/cpp/src/arrow/telemetry/telemetry_test.cc @@ -37,6 +37,8 @@ namespace telemetry { class OtelEnvironment : public ::testing::Environment { public: + static constexpr std::string_view kLoggerName = "arrow-telemetry-test"; + void SetUp() override { std::vector> processors; auto tracer_provider = otel::nostd::shared_ptr( @@ -55,9 +57,9 @@ class OtelEnvironment : public ::testing::Environment { ASSERT_OK_AND_ASSIGN( auto logger, GlobalLoggerProvider::MakeLogger( - "arrow-telemetry-test", logging_options, + kLoggerName, logging_options, AttributeList{Attribute{"fooInt", 42}, Attribute{"barStr", "fourty two"}})); - GlobalLogger::Set(std::move(logger)); + ASSERT_OK(util::LoggerRegistry::RegisterLogger(logger->name(), logger)); } void TearDown() override { EXPECT_TRUE(GlobalLoggerProvider::ShutDown()); } @@ -68,7 +70,10 @@ static ::testing::Environment* kOtelEnvironment = template void Log(Args&&... args) { - GlobalLogger::Get()->Log(std::forward(args)...); + auto logger = std::dynamic_pointer_cast( + util::LoggerRegistry::GetLogger(OtelEnvironment::kLoggerName)); + ASSERT_NE(logger, nullptr); + logger->Log(std::forward(args)...); } class TestLogging : public ::testing::Test { @@ -91,8 +96,6 @@ TEST_F(TestLogging, Basics) { Log(LogLevel::ARROW_WARNING, "baz bal", AttributeList{Attribute{"intAttr", 24}, Attribute{"boolAttr", true}, Attribute{"strAttr", std::string("ab") + "c"}}); - LogMessage(LogLevel::ARROW_INFO, GlobalLogger::Get()) << "This is a " - << "log message"; } } // namespace telemetry diff --git a/cpp/src/arrow/util/CMakeLists.txt b/cpp/src/arrow/util/CMakeLists.txt index e26efba28594b..55bbecb547789 100644 --- a/cpp/src/arrow/util/CMakeLists.txt +++ b/cpp/src/arrow/util/CMakeLists.txt @@ -64,6 +64,7 @@ add_arrow_test(utility-test iterator_test.cc list_util_test.cc logging_test.cc + logging_v2_test.cc queue_test.cc range_test.cc ree_util_test.cc diff --git a/cpp/src/arrow/util/logging_v2.cc b/cpp/src/arrow/util/logging_v2.cc new file mode 100644 index 0000000000000..e66255476dc71 --- /dev/null +++ b/cpp/src/arrow/util/logging_v2.cc @@ -0,0 +1,198 @@ +// 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 +#include +#include +#include + +#include "arrow/util/logging_v2.h" + +namespace arrow { +namespace util { + +namespace { + +class NoopLogger : public Logger { + public: + void Log(const LogDetails&) override {} + bool is_enabled() const override { return false; } +}; + +class SimpleLogger : public Logger { + public: + SimpleLogger(ArrowLogLevel severity_threshold, std::ostream* sink) + : severity_threshold_(severity_threshold), sink_(sink) {} + + void Log(const LogDetails& details) override { + *sink_ << details.source_location.file << ":" << details.source_location.line << ": " + << std::string(details.message) << std::endl; + } + + bool Flush(std::chrono::microseconds) override { + sink_->flush(); + return true; + } + + bool is_enabled() const override { return true; } + + ArrowLogLevel severity_threshold() const override { return severity_threshold_; } + + private: + ArrowLogLevel severity_threshold_; + std::ostream* sink_; +}; + +using LoggerMap = std::unordered_map>; + +struct RegistryImpl { + LoggerMap loggers; + std::shared_ptr default_logger = MakeOStreamLogger(ArrowLogLevel::ARROW_INFO); + std::mutex mtx; +}; + +RegistryImpl* GetRegistry() { + static auto instance = std::make_unique(); + return instance.get(); +} + +} // namespace + +std::shared_ptr MakeOStreamLogger(ArrowLogLevel severity_threshold, + std::ostream& sink) { + return std::make_shared(severity_threshold, &sink); +} +std::shared_ptr MakeOStreamLogger(ArrowLogLevel severity_threshold) { + return MakeOStreamLogger(severity_threshold, std::cerr); +} + +Status LoggerRegistry::RegisterLogger(std::string_view name, + std::shared_ptr logger) { + DCHECK_NE(logger, nullptr); + auto registry = GetRegistry(); + + std::lock_guard lk(registry->mtx); + auto ret = registry->loggers.emplace(name, std::move(logger)); + ARROW_RETURN_IF(!ret.second, Status::Invalid("Logger with name \"", std::string(name), + "\" is already registered")); + return Status::OK(); +} + +void LoggerRegistry::UnregisterLogger(std::string_view name) { + auto registry = GetRegistry(); + std::lock_guard lk(registry->mtx); + registry->loggers.erase(std::string(name)); +} + +std::shared_ptr LoggerRegistry::GetLogger(std::string_view name) { + if (name.empty()) { + return GetDefaultLogger(); + } + + auto registry = GetRegistry(); + { + std::lock_guard lk(registry->mtx); + const auto& loggers = registry->loggers; + if (auto it = loggers.find(std::string(name)); it != loggers.end()) { + return it->second; + } + } + return std::make_shared(); +} + +std::shared_ptr LoggerRegistry::GetDefaultLogger() { + auto registry = GetRegistry(); + std::lock_guard lk(registry->mtx); + return registry->default_logger; +} + +void LoggerRegistry::SetDefaultLogger(std::shared_ptr logger) { + DCHECK_NE(logger, nullptr); + auto registry = GetRegistry(); + std::lock_guard lk(registry->mtx); + registry->default_logger = std::move(logger); +} + +class LogMessage::Impl { + public: + Impl(ArrowLogLevel severity, SourceLocation source_location) { + details.severity = severity; + details.source_location = source_location; + } + Impl(ArrowLogLevel severity, std::shared_ptr logger, + SourceLocation source_location) + : Impl(severity, source_location) { + logger_ = std::move(logger); + } + Impl(ArrowLogLevel severity, std::string logger_name, SourceLocation source_location) + : Impl(severity, source_location) { + this->logger_name = std::move(logger_name); + } + + ~Impl() { + auto* logger = GetResolvedLogger(); + + if (logger && logger->is_enabled() && + details.severity >= logger->severity_threshold()) { + auto str = stream.str(); + details.message = str; + logger->Log(details); + } + + // It's debatable whether this should be the underlying logger's responsibility + if (details.severity == ArrowLogLevel::ARROW_FATAL) { + if (logger) { + logger->Flush(); + } + std::abort(); + } + } + + Logger* GetResolvedLogger() { + if (!logger_) { + logger_ = LoggerRegistry::GetLogger(logger_name); + } + return logger_.get(); + } + + LogDetails details{}; + std::string logger_name; + std::stringstream stream; + + private: + std::shared_ptr logger_; +}; + +LogMessage::LogMessage(ArrowLogLevel severity, std::shared_ptr logger, + SourceLocation source_location) + : impl_(std::make_shared(severity, std::move(logger), source_location)) {} + +LogMessage::LogMessage(ArrowLogLevel severity, std::string_view logger_name, + SourceLocation source_location) + : impl_(std::make_shared(severity, std::string(logger_name), source_location)) { +} + +std::ostream& LogMessage::Stream() { return impl_->stream; } + +bool LogMessage::CheckIsEnabled() { + auto* logger = impl_->GetResolvedLogger(); + return (logger && logger->is_enabled() && + impl_->details.severity >= logger->severity_threshold()); +} + +} // namespace util +} // namespace arrow diff --git a/cpp/src/arrow/util/logging_v2.h b/cpp/src/arrow/util/logging_v2.h new file mode 100644 index 0000000000000..60030e935335d --- /dev/null +++ b/cpp/src/arrow/util/logging_v2.h @@ -0,0 +1,203 @@ +// 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. + +#pragma once + +#include +#include +#include +#include + +#include "arrow/result.h" +#include "arrow/status.h" +#include "arrow/util/logging.h" +#include "arrow/util/macros.h" +#include "arrow/util/visibility.h" + +namespace arrow { +namespace util { + +struct SourceLocation { + const char* file = ""; + int line = 0; +}; + +struct LogDetails { + ArrowLogLevel severity = ArrowLogLevel::ARROW_INFO; + std::chrono::system_clock::time_point timestamp = std::chrono::system_clock::now(); + SourceLocation source_location{}; + std::string_view message = ""; +}; + +/// \brief A base interface for custom loggers. +/// +/// Loggers can be added to the LoggerRegistry for global access or directly provided to +/// certain logging utilities. +class Logger { + public: + virtual ~Logger() = default; + + virtual void Log(const LogDetails& details) = 0; + + virtual bool Flush(std::chrono::microseconds timeout) { return true; } + bool Flush() { return this->Flush(std::chrono::microseconds::max()); } + + virtual bool is_enabled() const { return true; } + + virtual ArrowLogLevel severity_threshold() const { return ArrowLogLevel::ARROW_TRACE; } +}; + +/// \brief Creates a simple logger that redirects output to std::cerr +ARROW_EXPORT std::shared_ptr MakeOStreamLogger(ArrowLogLevel severity_threshold); +/// \brief Creates a simple logger that redirects output to the provided ostream +ARROW_EXPORT std::shared_ptr MakeOStreamLogger(ArrowLogLevel severity_threshold, + std::ostream& sink); + +class ARROW_EXPORT LoggerRegistry { + public: + /// \brief Add a logger to the registry with the associated name + /// + /// Returns Invalid if a logger with the provided name already exists. Users should call + /// `UnregisterLogger` first if they wish to overwrite it. + static Status RegisterLogger(std::string_view name, std::shared_ptr logger); + + /// \brief Remove a logger from the registry + static void UnregisterLogger(std::string_view name); + + /// \brief Return the logger associated with the provided name + /// + /// If `name` is empty, the default logger is returned. If `name` doesn't match any of + /// the registered loggers then a non-null noop logger is returned + static std::shared_ptr GetLogger(std::string_view name = ""); + + /// \brief Return the default logger + static std::shared_ptr GetDefaultLogger(); + /// \brief Set the default logger + static void SetDefaultLogger(std::shared_ptr logger); +}; + +/// \brief Represents a single log record to be emitted by an underlying logger +class LogMessage { + public: + /// \brief Construct a LogMessage with the provided underlying logger + LogMessage(ArrowLogLevel severity, std::shared_ptr logger, + SourceLocation source_location = {}); + /// \brief Construct a LogMessage with the provided logger name, which will be used to + /// find an underlying logger in the registry + LogMessage(ArrowLogLevel severity, std::string_view logger_name, + SourceLocation source_location = {}); + + std::ostream& Stream(); + + template + LogMessage& operator<<(const T& t) { + if (CheckIsEnabled()) { + Stream() << t; + } + return *this; + } + + // Convenience method - mainly for use in ARROW_LOG_* macros. This prevents unnecessary + // argument evaluation when log statements are stripped in certain builds + template + LogMessage& Append(Args&&... args) { + if constexpr (sizeof...(Args) > 0) { + if (CheckIsEnabled()) { + (Stream() << ... << args); + } + } + return *this; + } + + private: + bool CheckIsEnabled(); + + class Impl; + std::shared_ptr impl_; +}; + +} // namespace util +} // namespace arrow + +// For the following macros, log statements with a lower severity than +// `ARROW_MINIMUM_LOG_LEVEL` will be stripped from the build +#ifndef ARROW_MINIMUM_LOG_LEVEL +#define ARROW_MINIMUM_LOG_LEVEL -1000 +#endif + +#define ARROW_LOG_WITH_INTERNAL(LOGGER, LEVEL) \ + (::arrow::util::LogMessage(::arrow::util::ArrowLogLevel::ARROW_##LEVEL, LOGGER, \ + ::arrow::util::SourceLocation{__FILE__, __LINE__})) + +static_assert(static_cast(::arrow::util::ArrowLogLevel::ARROW_TRACE) == -2); +#if ARROW_MINIMUM_LOG_LEVEL <= -2 +#define ARROW_LOG_TRACE_WITH(LOGGER, ...) \ + (ARROW_LOG_WITH_INTERNAL(LOGGER, TRACE).Append(__VA_ARGS__)) +#else +#define ARROW_LOG_TRACE_WITH(...) (::arrow::util::detail::NullLog{}) +#endif + +static_assert(static_cast(::arrow::util::ArrowLogLevel::ARROW_DEBUG) == -1); +#if ARROW_MINIMUM_LOG_LEVEL <= -1 +#define ARROW_LOG_DEBUG_WITH(LOGGER, ...) \ + (ARROW_LOG_WITH_INTERNAL(LOGGER, DEBUG).Append(__VA_ARGS__)) +#else +#define ARROW_LOG_DEBUG_WITH(...) (::arrow::util::detail::NullLog{}) +#endif + +static_assert(static_cast(::arrow::util::ArrowLogLevel::ARROW_INFO) == 0); +#if ARROW_MINIMUM_LOG_LEVEL <= 0 +#define ARROW_LOG_INFO_WITH(LOGGER, ...) \ + (ARROW_LOG_WITH_INTERNAL(LOGGER, INFO).Append(__VA_ARGS__)) +#else +#define ARROW_LOG_INFO_WITH(...) (::arrow::util::detail::NullLog{}) +#endif + +static_assert(static_cast(::arrow::util::ArrowLogLevel::ARROW_WARNING) == 1); +#if ARROW_MINIMUM_LOG_LEVEL <= 1 +#define ARROW_LOG_WARNING_WITH(LOGGER, ...) \ + (ARROW_LOG_WITH_INTERNAL(LOGGER, WARNING).Append(__VA_ARGS__)) +#else +#define ARROW_LOG_WARNING_WITH (::arrow::util::detail::NullLog{}) +#endif + +static_assert(static_cast(::arrow::util::ArrowLogLevel::ARROW_ERROR) == 2); +#if ARROW_MINIMUM_LOG_LEVEL <= 2 +#define ARROW_LOG_ERROR_WITH(LOGGER, ...) \ + (ARROW_LOG_WITH_INTERNAL(LOGGER, ERROR).Append(__VA_ARGS__)) +#else +#define ARROW_LOG_ERROR_WITH(...) (::arrow::util::detail::NullLog{}) +#endif + +static_assert(static_cast(::arrow::util::ArrowLogLevel::ARROW_FATAL) == 3); +#if ARROW_MINIMUM_LOG_LEVEL <= 3 +#define ARROW_LOG_FATAL_WITH(LOGGER, ...) \ + (ARROW_LOG_WITH_INTERNAL(LOGGER, FATAL).Append(__VA_ARGS__)) +#else +#define ARROW_LOG_FATAL_WITH(...) (::arrow::util::detail::NullLog{}) +#endif + +#define ARROW_LOG_WITH(LOGGER, LEVEL, ...) ARROW_LOG_##LEVEL##_WITH(LOGGER, __VA_ARGS__) + +#define ARROW_LOG_TRACE(...) ARROW_LOG_TRACE_WITH("", __VA_ARGS__) +#define ARROW_LOG_DEBUG(...) ARROW_LOG_DEBUG_WITH("", __VA_ARGS__) +#define ARROW_LOG_INFO(...) ARROW_LOG_INFO_WITH("", __VA_ARGS__) +#define ARROW_LOG_WARNING(...) ARROW_LOG_WARNING_WITH("", __VA_ARGS__) +#define ARROW_LOG_ERROR(...) ARROW_LOG_ERROR_WITH("", __VA_ARGS__) +#define ARROW_LOG_FATAL(...) ARROW_LOG_FATAL_WITH("", __VA_ARGS__) + +#define ARROW_LOG_V2(LEVEL, ...) ARROW_LOG_WITH("", LEVEL, __VA_ARGS__) diff --git a/cpp/src/arrow/util/logging_v2_test.cc b/cpp/src/arrow/util/logging_v2_test.cc new file mode 100644 index 0000000000000..df0ea2a924a89 --- /dev/null +++ b/cpp/src/arrow/util/logging_v2_test.cc @@ -0,0 +1,135 @@ +// 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 +#include + +#include + +#include "arrow/testing/gtest_util.h" +#include "arrow/util/logging_v2.h" + +namespace arrow { +namespace util { + +namespace { + +class MockLogger : public Logger { + public: + explicit MockLogger(ArrowLogLevel threshold = ArrowLogLevel::ARROW_TRACE) + : threshold(threshold) {} + void Log(const LogDetails& details) override { + if (callback) { + callback(details); + } + } + ArrowLogLevel severity_threshold() const override { return threshold; } + bool is_enabled() const override { return enabled; } + + ArrowLogLevel threshold; + bool enabled = true; + std::function callback; +}; + +struct LoggingTracer { + mutable bool was_evaluated = false; + friend std::ostream& operator<<(std::ostream& os, const LoggingTracer& tracer) { + tracer.was_evaluated = true; + return os; + } +}; + +} // namespace + +TEST(LoggingV2Test, Basics) { + // Basic tests using the default logger + ARROW_LOG_V2(ERROR); + ARROW_LOG_V2(ERROR, "foo") << "bar"; + auto to_int = [](ArrowLogLevel lvl) { return static_cast(lvl); }; + ARROW_LOG_V2(TRACE, "sev: ", to_int(ArrowLogLevel::ARROW_TRACE), ":TRACE"); + ARROW_LOG_V2(DEBUG, "sev: ", to_int(ArrowLogLevel::ARROW_DEBUG), ":DEBUG"); + ARROW_LOG_V2(INFO, "sev: ", to_int(ArrowLogLevel::ARROW_INFO), ":INFO"); + ARROW_LOG_V2(WARNING, "sev: ", to_int(ArrowLogLevel::ARROW_WARNING), ":WARNING"); + ARROW_LOG_V2(ERROR, "sev: ", to_int(ArrowLogLevel::ARROW_ERROR), ":ERROR"); + + { + auto logger = std::make_shared(ArrowLogLevel::ARROW_WARNING); + LoggingTracer tracers[5]{}; + ARROW_LOG_WITH(logger, TRACE, "foo", tracers[0], "bar"); + ARROW_LOG_WITH(logger, DEBUG, "foo", tracers[1], "bar"); + ARROW_LOG_WITH(logger, INFO, "foo", tracers[2], "bar"); + ARROW_LOG_WITH(logger, WARNING, "foo", tracers[3], "bar"); + ARROW_LOG_WITH(logger, ERROR, "foo", tracers[4], "bar"); + + // If the message severity doesn't meet the logger's minimum severity, the LogMessage + // stream shouldn't be appended to + ASSERT_FALSE(tracers[0].was_evaluated); + ASSERT_FALSE(tracers[1].was_evaluated); + ASSERT_FALSE(tracers[2].was_evaluated); + ASSERT_TRUE(tracers[3].was_evaluated); + ASSERT_TRUE(tracers[4].was_evaluated); + } + + { + auto logger = std::make_shared(ArrowLogLevel::ARROW_WARNING); + logger->enabled = false; + LoggingTracer tracer; + // If the underlying logger is disabled, the LogMessage stream shouldn't be appended + // to (regardless of severity) + ARROW_LOG_WITH(logger, WARNING, tracer); + ARROW_LOG_WITH(logger, ERROR, tracer); + ASSERT_FALSE(tracer.was_evaluated); + } +} + +TEST(LoggingV2Test, EmittedMessages) { + std::vector messages; + auto callback = [&messages](const LogDetails& details) { + messages.push_back(std::string(details.message)); + }; + + auto logger = std::make_shared(ArrowLogLevel::ARROW_TRACE); + logger->callback = callback; + for (int i = 0; i < 3; ++i) { + ARROW_LOG_WITH(logger, TRACE, "i=", i); + } + + ASSERT_EQ(messages.size(), 3); + EXPECT_EQ(messages[0], "i=0"); + EXPECT_EQ(messages[1], "i=1"); + EXPECT_EQ(messages[2], "i=2"); +} + +TEST(LoggingV2Test, Registry) { + std::string name = "test-logger"; + std::shared_ptr logger = std::make_shared(); + + ASSERT_OK(LoggerRegistry::RegisterLogger(name, logger)); + ASSERT_EQ(logger, LoggerRegistry::GetLogger(name)); + + // Error if the logger is already registered + ASSERT_RAISES(Invalid, LoggerRegistry::RegisterLogger(name, logger)); + + LoggerRegistry::UnregisterLogger(name); + ASSERT_OK(LoggerRegistry::RegisterLogger(name, logger)); + ASSERT_EQ(logger, LoggerRegistry::GetLogger(name)); + + LoggerRegistry::UnregisterLogger(name); +} + +} // namespace util +} // namespace arrow