diff --git a/include/seastar/util/log.hh b/include/seastar/util/log.hh index 7599def12bc..bebad1a2476 100644 --- a/include/seastar/util/log.hh +++ b/include/seastar/util/log.hh @@ -71,6 +71,14 @@ class logger { #endif public: + /// Tag used to request that a log call bypass the configured level + /// gate. Passed as the first argument to any force-suffixed overload + /// or to the per-level helpers. A distinct type (not bool) avoids + /// ambiguity with the format_info-first overloads: a string literal + /// converts to format_info but not to force_tag. + struct force_tag {}; + static constexpr force_tag force{}; + class log_writer { public: virtual ~log_writer() = default; @@ -251,20 +259,13 @@ public: /// template void log(log_level level, format_info_t fmt, Args&&... args) noexcept { - if (is_enabled(level)) { - try { - lambda_log_writer writer([&] (internal::log_buf::inserter_iterator it) { -#ifdef SEASTAR_LOGGER_COMPILE_TIME_FMT - return fmt::format_to(it, fmt.format, std::forward(args)...); -#else - return fmt::format_to(it, fmt::runtime(fmt.format), std::forward(args)...); -#endif - }); - do_log(level, writer); - } catch (...) { - failed_to_log(std::current_exception(), fmt::string_view(fmt.format), fmt.loc); - } - } + do_log_checked(level, false, std::move(fmt), std::forward(args)...); + } + + /// Force-emit variant: bypass the level gate. See \ref force_tag. + template + void log(log_level level, force_tag, format_info_t fmt, Args&&... args) noexcept { + do_log_checked(level, true, std::move(fmt), std::forward(args)...); } /// logs with a rate limit to desired level if enabled, otherwise we ignore the log line @@ -283,19 +284,14 @@ public: /// template void log(log_level level, rate_limit& rl, format_info_t fmt, Args&&... args) noexcept { - if (is_enabled(level) && rl.check()) { - try { - lambda_log_writer writer([&] (internal::log_buf::inserter_iterator it) { - if (rl.has_dropped_messages()) { - it = fmt::format_to(it, "(rate limiting dropped {} similar messages) ", rl.get_and_reset_dropped_messages()); - } - return fmt::format_to(it, fmt::runtime(fmt.format), std::forward(args)...); - }); - do_log(level, writer); - } catch (...) { - failed_to_log(std::current_exception(), fmt::string_view(fmt.format), fmt.loc); - } - } + do_log_checked_rl(level, false, rl, std::move(fmt), std::forward(args)...); + } + + /// Force-emit rate-limited variant: bypass the level gate (the rate + /// limit itself still applies). + template + void log(log_level level, force_tag, rate_limit& rl, format_info_t fmt, Args&&... args) noexcept { + do_log_checked_rl(level, true, rl, std::move(fmt), std::forward(args)...); } /// \cond internal @@ -355,6 +351,11 @@ public: void error(format_info_t fmt, Args&&... args) noexcept { log(log_level::error, std::move(fmt), std::forward(args)...); } + /// Force-emit variant: bypass the level gate. See \ref force_tag. + template + void error(force_tag, format_info_t fmt, Args&&... args) noexcept { + log(log_level::error, force, std::move(fmt), std::forward(args)...); + } /// Log with warning tag: /// WARN %Y-%m-%d %T,%03d [shard 0] - "your msg" \n /// @@ -366,6 +367,11 @@ public: void warn(format_info_t fmt, Args&&... args) noexcept { log(log_level::warn, std::move(fmt), std::forward(args)...); } + /// Force-emit variant: bypass the level gate. See \ref force_tag. + template + void warn(force_tag, format_info_t fmt, Args&&... args) noexcept { + log(log_level::warn, force, std::move(fmt), std::forward(args)...); + } /// Log with info tag: /// INFO %Y-%m-%d %T,%03d [shard 0] - "your msg" \n /// @@ -377,6 +383,11 @@ public: void info(format_info_t fmt, Args&&... args) noexcept { log(log_level::info, std::move(fmt), std::forward(args)...); } + /// Force-emit variant: bypass the level gate. See \ref force_tag. + template + void info(force_tag, format_info_t fmt, Args&&... args) noexcept { + log(log_level::info, force, std::move(fmt), std::forward(args)...); + } /// Log with info tag on shard zero only: /// INFO %Y-%m-%d %T,%03d [shard 0] - "your msg" \n /// @@ -390,6 +401,13 @@ public: log(log_level::info, std::move(fmt), std::forward(args)...); } } + /// Force-emit variant: bypass the level gate. See \ref force_tag. + template + void info0(force_tag, format_info_t fmt, Args&&... args) noexcept { + if (is_shard_zero()) { + log(log_level::info, force, std::move(fmt), std::forward(args)...); + } + } /// Log with debug tag: /// DEBUG %Y-%m-%d %T,%03d [shard 0] - "your msg" \n /// @@ -401,6 +419,11 @@ public: void debug(format_info_t fmt, Args&&... args) noexcept { log(log_level::debug, std::move(fmt), std::forward(args)...); } + /// Force-emit variant: bypass the level gate. See \ref force_tag. + template + void debug(force_tag, format_info_t fmt, Args&&... args) noexcept { + log(log_level::debug, force, std::move(fmt), std::forward(args)...); + } /// Log with trace tag: /// TRACE %Y-%m-%d %T,%03d [shard 0] - "your msg" \n /// @@ -412,6 +435,11 @@ public: void trace(format_info_t fmt, Args&&... args) noexcept { log(log_level::trace, std::move(fmt), std::forward(args)...); } + /// Force-emit variant: bypass the level gate. See \ref force_tag. + template + void trace(force_tag, format_info_t fmt, Args&&... args) noexcept { + log(log_level::trace, force, std::move(fmt), std::forward(args)...); + } /// \return name of the logger. Usually one logger per module /// @@ -457,6 +485,42 @@ public: /// /// \note this is a noop if fmtlib's version is less than 6.0 static void set_with_color(bool enabled) noexcept; + +private: + template + void do_log_checked(log_level level, bool bypass, format_info_t fmt, Args&&... args) noexcept { + if (bypass || is_enabled(level)) { + try { + lambda_log_writer writer([&] (internal::log_buf::inserter_iterator it) { +#ifdef SEASTAR_LOGGER_COMPILE_TIME_FMT + return fmt::format_to(it, fmt.format, std::forward(args)...); +#else + return fmt::format_to(it, fmt::runtime(fmt.format), std::forward(args)...); +#endif + }); + do_log(level, writer); + } catch (...) { + failed_to_log(std::current_exception(), fmt::string_view(fmt.format), fmt.loc); + } + } + } + + template + void do_log_checked_rl(log_level level, bool bypass, rate_limit& rl, format_info_t fmt, Args&&... args) noexcept { + if ((bypass || is_enabled(level)) && rl.check()) { + try { + lambda_log_writer writer([&] (internal::log_buf::inserter_iterator it) { + if (rl.has_dropped_messages()) { + it = fmt::format_to(it, "(rate limiting dropped {} similar messages) ", rl.get_and_reset_dropped_messages()); + } + return fmt::format_to(it, fmt::runtime(fmt.format), std::forward(args)...); + }); + do_log(level, writer); + } catch (...) { + failed_to_log(std::current_exception(), fmt::string_view(fmt.format), fmt.loc); + } + } + } }; /// \brief used to keep a static registry of loggers diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 413ca0b0c5d..28db9238158 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -769,6 +769,9 @@ seastar_add_test (weak_ptr seastar_add_test (log_buf SOURCES log_buf_test.cc) +seastar_add_test (logger_force + SOURCES logger_force_test.cc) + seastar_add_test (exception_logging KIND BOOST SOURCES exception_logging_test.cc) diff --git a/tests/unit/logger_force_test.cc b/tests/unit/logger_force_test.cc new file mode 100644 index 00000000000..980daba67e2 --- /dev/null +++ b/tests/unit/logger_force_test.cc @@ -0,0 +1,68 @@ +/* + * This file is open source software, licensed to you under the terms + * of the Apache License, Version 2.0 (the "License"). See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. 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 + +namespace { +seastar::logger test_log("test"); +} + +SEASTAR_THREAD_TEST_CASE(force_bypasses_level_gate_log_overload) { + std::ostringstream captured; + seastar::logger::set_ostream(captured); + seastar::logger::set_ostream_enabled(true); + + test_log.set_level(seastar::log_level::warn); + + // Without force: dropped. + test_log.log(seastar::log_level::info, "dropped_line"); + BOOST_REQUIRE(captured.str().find("dropped_line") == std::string::npos); + + // With force: emitted. + test_log.log(seastar::log_level::info, seastar::logger::force, "forced_line"); + BOOST_REQUIRE(captured.str().find("forced_line") != std::string::npos); +} + +SEASTAR_THREAD_TEST_CASE(force_bypasses_level_gate_convenience_methods) { + std::ostringstream captured; + seastar::logger::set_ostream(captured); + seastar::logger::set_ostream_enabled(true); + + test_log.set_level(seastar::log_level::warn); + + test_log.info(seastar::logger::force, "info_forced"); + test_log.debug(seastar::logger::force, "debug_forced"); + test_log.trace(seastar::logger::force, "trace_forced"); + test_log.warn(seastar::logger::force, "warn_forced"); + test_log.error(seastar::logger::force, "error_forced"); + + const auto out = captured.str(); + BOOST_REQUIRE(out.find("info_forced") != std::string::npos); + BOOST_REQUIRE(out.find("debug_forced") != std::string::npos); + BOOST_REQUIRE(out.find("trace_forced") != std::string::npos); + BOOST_REQUIRE(out.find("warn_forced") != std::string::npos); + BOOST_REQUIRE(out.find("error_forced") != std::string::npos); + + // Without force, info/debug/trace dropped under warn level. + captured.str(""); + test_log.info("info_dropped"); + BOOST_REQUIRE(captured.str().find("info_dropped") == std::string::npos); +}