logging: streamline Logger state and drop redundant methods #35322

pull ryanofsky wants to merge 14 commits into bitcoin:master from ryanofsky:pr/levels changing 15 files +212 −198
  1. ryanofsky commented at 1:55 PM on May 19, 2026: contributor

    Problem: The Logger class currently tracks enabled log messages using three separate fields:

    • m_categories — bitmask of enabled log categories
    • m_log_level — global log level threshold
    • m_category_log_levels — hash map of per-category level overrides

    This representation is confusing and redundant. Bitcoin Core has only ~30 log categories and 2 conditional log levels, so a mutex-protected hash map, a bitset, and a separate global level are unnecessary. The added complexity is also harmful: it makes the code harder to follow, and basic operations like setting a category's level require coordinating multiple fields and method calls.

    Solution: This PR streamlines the Logger class without changing behavior, replacing the three fields with two atomics: one bitmask for each conditional log level (Debug and Trace). Each bitmask records which categories are enabled at that level.

    This allows the following methods to be dropped: EnableCategory, DisableCategory, WillLogCategory, LogLevel, SetLogLevel, GetCategoryMask, AddCategoryLogLevel. The remaining SetCategoryLogLevel and WillLogCategoryLevel methods are now self-contained.

    Behavior note: This is a behavior-preserving refactor with one exception: categories dynamically enabled at runtime through the logging RPC or the kernel btck_logging_* API are now always enabled at Debug level, even if -loglevel=trace or its kernel equivalent was set at startup. This is unlikely to matter in practice. Very little LogTrace logging currently exists, and both APIs are being reworked in bitcoin/bitcoin#34038 and bitcoin/bitcoin#34374. The -loglevel option otherwise continues to work as before when categories are not changed at runtime.

  2. DrahtBot commented at 1:55 PM on May 19, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35322.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process. A summary of reviews will appear here.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35387 (logging: make trace logging easily usable by ryanofsky)
    • #35355 (Use atomics for determining whether trace logging is enabled by ajtowns)
    • #35205 (kernel,node: clean up dbcache helpers and add kernel API by l0rinc)
    • #35182 (Replace libevent with our own HTTP and socket-handling implementation by pinheadmz)
    • #34778 (logging: rewrite macros to enforce restrictions at compile-time, improve efficiency and usability by ryanofsky)
    • #34775 (kernel: make logging callback global by stickies-v)
    • #34411 ([POC] Full Libevent removal by fanquake)
    • #34038 (logging: replace -loglevel with -trace, expose trace logging via RPC by ajtowns)
    • #33847 (kernel: Improve logging API by ryanofsky)
    • #31260 (scripted-diff: Type-safe settings retrieval by ryanofsky)
    • #30343 (wallet, logging: Replace WalletLogPrintf() with LogInfo() by ryanofsky)
    • #30342 (kernel, logging: Pass Logger instances to kernel objects by ryanofsky)
    • #29256 (log, refactor: Allow log macros to accept context arguments by ryanofsky)
    • #17783 (common: Disallow calling IsArgSet() on ALLOW_LIST options by ryanofsky)
    • #17581 (refactor: Remove settings merge reverse precedence code by ryanofsky)
    • #17580 (refactor: Add ALLOW_LIST flags and enforce usage in CheckArgFlags by ryanofsky)
    • #17493 (util: Forbid ambiguous multiple assignments in config file by ryanofsky)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  3. in src/test/logging_tests.cpp:209 in ccd90a7512 outdated
     206 |          ArgsManager args;
     207 | +        args.AddArg("-debug", "...", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
     208 | +        args.AddArg("-debugexclude", "...", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
     209 |          args.AddArg("-loglevel", "...", ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST);
     210 | -        const char* argv_test[] = {"bitcoind", "-loglevel=debug"};
     211 | +        std::array argv_test{"bitcoind", "-debug", "-debugexclude=net", "-loglevel=trace"};
    


    l0rinc commented at 8:58 AM on May 20, 2026:

    There's a single commit here with a lot of changes that don't look closely related. I don't have a strong opinion about logging, but it's obviously important to others so I don't mind reviewing it - though in this current state I can't do that meaningfully. If you think it's worth it, could you please split this into smaller, more focused commits (maybe multiple PRs, since the title already suggests it's hard to properly describe the goal of the changes)?


    ryanofsky commented at 10:24 AM on May 20, 2026:

    re: #35322 (review)

    I don't mind reviewing it - though in this current state I can't do that meaningfully

    Thanks for looking at this! This PR is a fairly small change: only +77/-124 lines excluding tests. But I appreciate it could be pretty difficult to understand if you are coming in cold and don't know where to begin.

    I think it should be possible to split the PR up and I'll experiment with this. (Here is a branch claude made which splits the PR into two commits. pr/levels-explain. It looks ok but I want to verify.)

    If I can give a review tip for the current PR, it would be to first look at the new SetCategoryLogLevel & WillLogCategoryLevel implementations and understand how they use the m_levels array to determine what to log and not log.

    After you understand those two methods, you basically understand the whole PR because it is just updating the rest of the code to call those methods.

    it's hard to properly describe the goal of the changes

    Let me know if I can help with this, I do hope the description is clear.


    l0rinc commented at 8:10 AM on May 21, 2026:

    I think it should be possible to split the PR up and I'll experiment with this.

    I would prefer peeling off each method separately to have some progress during review. For example, getLogCategories() is only used in a single place to decide whether the GUI should show debug information, and that can be split out before the category-mask refactor.

    Reviewing whether the GUI should check for any enabled category mask or for actual Debug-level logging is already non-trivial and worthy of separation.

    <details><summary>isAnyDebugLoggingEnabled</summary>

    diff --git a/src/bitcoind.cpp b/src/bitcoind.cpp
    index ac73158768..969ecb3910 100644
    --- a/src/bitcoind.cpp
    +++ b/src/bitcoind.cpp
    @@ -14,6 +14,7 @@
     #include <compat/compat.h>
     #include <init.h>
     #include <interfaces/chain.h>
    +#include <logging.h>
     #include <interfaces/init.h>
     #include <kernel/context.h>
     #include <node/context.h>
    diff --git a/src/interfaces/node.h b/src/interfaces/node.h
    index f6d8209f07..294b71e0eb 100644
    --- a/src/interfaces/node.h
    +++ b/src/interfaces/node.h
    @@ -7,7 +7,6 @@
     
     #include <common/settings.h>
     #include <consensus/amount.h>
    -#include <logging.h>
     #include <net.h>
     #include <net_types.h>
     #include <netaddress.h>
    @@ -84,8 +83,8 @@ public:
         //! Get exit status.
         virtual int getExitStatus() = 0;
     
    -    // Get log flags.
    -    virtual BCLog::CategoryMask getLogCategories() = 0;
    +    //! Returns true if any debug logging categories are enabled.
    +    virtual bool isAnyDebugLoggingEnabled() = 0;
     
         //! Initialize app dependencies.
         virtual bool baseInitialize() = 0;
    diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp
    index 16db8692a1..347a998faf 100644
    --- a/src/node/interfaces.cpp
    +++ b/src/node/interfaces.cpp
    @@ -112,7 +112,7 @@ public:
         void initParameterInteraction() override { InitParameterInteraction(args()); }
         bilingual_str getWarnings() override { return Join(Assert(m_context->warnings)->GetMessages(), Untranslated("<hr />")); }
         int getExitStatus() override { return Assert(m_context)->exit_status.load(); }
    -    BCLog::CategoryMask getLogCategories() override { return LogInstance().GetCategoryMask(); }
    +    bool isAnyDebugLoggingEnabled() override { return LogInstance().WillLogCategoryLevel(BCLog::ALL, BCLog::Level::Debug); }
         bool baseInitialize() override
         {
             if (!AppInitBasicSetup(args(), Assert(context())->exit_status)) return false;
    diff --git a/src/qt/transactiondesc.cpp b/src/qt/transactiondesc.cpp
    index d9c97d8529..3e59b01c49 100644
    --- a/src/qt/transactiondesc.cpp
    +++ b/src/qt/transactiondesc.cpp
    @@ -314,7 +314,7 @@ QString TransactionDesc::toHTML(interfaces::Node& node, interfaces::Wallet& wall
         //
         // Debug view
         //
    -    if (node.getLogCategories() != BCLog::NONE)
    +    if (node.isAnyDebugLoggingEnabled())
         {
             strHTML += "<hr><br>" + tr("Debug information") + "<br><br>";
             for (const CTxIn& txin : wtx.tx->vin)
    diff --git a/src/test/node_init_tests.cpp b/src/test/node_init_tests.cpp
    index 44635dd4aa..fb9bce6cff 100644
    --- a/src/test/node_init_tests.cpp
    +++ b/src/test/node_init_tests.cpp
    @@ -4,6 +4,7 @@
     
     #include <init.h>
     #include <interfaces/init.h>
    +#include <logging.h>
     #include <rpc/server.h>
     
     #include <boost/test/unit_test.hpp>
    

    </details>


    ryanofsky commented at 4:14 PM on May 27, 2026:

    re: #35322 (review)

    I would prefer peeling off each method separately to have some progress during review.

    Makes sense, and latest push removes each method in separate commits. Splitting this up does add some churn to the PR, because there is now temporary code added in the first commit that is removed in later commits. So some reviewers may prefer to just look at the overall diff, but probably splitting is an improvement.

  4. l0rinc changes_requested
  5. in src/test/logging_tests.cpp:178 in ccd90a7512 outdated
     173 | @@ -185,13 +174,12 @@ BOOST_FIXTURE_TEST_CASE(logging_LogPrintMacros_CategoryName, LogSetup)
     174 |  
     175 |  BOOST_FIXTURE_TEST_CASE(logging_SeverityLevels, LogSetup)
     176 |  {
     177 | -    LogInstance().SetLogLevel(BCLog::Level::Debug);
     178 | -    LogInstance().EnableCategory(BCLog::LogFlags::ALL);
     179 | +    LogInstance().SetCategoryLogLevel(BCLog::LogFlags::ALL, BCLog::Level::Debug);
     180 |      LogInstance().SetCategoryLogLevel(/*category_str=*/"net", /*level_str=*/"info");
    


    l0rinc commented at 8:21 AM on May 21, 2026:

    This string-parsing overload no longer has production callers after startup parsing moves out of Logger. Could we drop it and have the test use the typed setter directly?

    diff --git a/src/logging.cpp b/src/logging.cpp
    index 7c1777adc1..a0fae9badf 100644
    --- a/src/logging.cpp
    +++ b/src/logging.cpp
    @@ -567,18 +567,6 @@ bool BCLog::LogRateLimiter::Stats::Consume(uint64_t bytes)
         return true;
     }
     
    -bool BCLog::Logger::SetCategoryLogLevel(std::string_view category_str, std::string_view level_str)
    -{
    -    const auto flag{GetLogCategory(category_str)};
    -    if (!flag) return false;
    -
    -    const auto level = GetLogLevel(level_str);
    -    if (!level.has_value() || level.value() > BCLog::UNCONDITIONAL_LOG_LEVEL) return false;
    -
    -    SetCategoryLogLevel(*flag, level.value());
    -    return true;
    -}
    -
     bool util::log::ShouldLog(Category category, Level level)
     {
         return LogInstance().WillLogCategoryLevel(static_cast<BCLog::LogFlags>(category), level);
    diff --git a/src/logging.h b/src/logging.h
    index b44349cc55..a543562a1e 100644
    --- a/src/logging.h
    +++ b/src/logging.h
    @@ -230,7 +230,6 @@ namespace BCLog {
             //! Set the log level threshold for a specific category (or ALL categories if
             //! flag == BCLog::ALL). Levels >= UNCONDITIONAL_LOG_LEVEL disable the category.
             void SetCategoryLogLevel(CategoryMask category, Level level);
    -        bool SetCategoryLogLevel(std::string_view category_str, std::string_view level_str);
     
             //! Returns true if a message at the given category and level would be logged.
             bool WillLogCategoryLevel(LogFlags category, Level level) const;
    diff --git a/src/test/logging_tests.cpp b/src/test/logging_tests.cpp
    index 15e3b8ee4b..413df24af0 100644
    --- a/src/test/logging_tests.cpp
    +++ b/src/test/logging_tests.cpp
    @@ -175,7 +175,7 @@ BOOST_FIXTURE_TEST_CASE(logging_LogPrintMacros_CategoryName, LogSetup)
     BOOST_FIXTURE_TEST_CASE(logging_SeverityLevels, LogSetup)
     {
         LogInstance().SetCategoryLogLevel(BCLog::LogFlags::ALL, BCLog::Level::Debug);
    -    LogInstance().SetCategoryLogLevel(/*category_str=*/"net", /*level_str=*/"info");
    +    LogInstance().SetCategoryLogLevel(BCLog::NET, BCLog::Level::Info);
     
         // All categories at Debug level
         LogInfo("info_%s", 1);
    

    ryanofsky commented at 4:15 PM on May 27, 2026:

    re: #35322 (review)

    This string-parsing overload no longer has production callers after startup parsing moves out of Logger. Could we drop it and have the test use the typed setter directly?

    Nice find! This lets another logger method to be dropped. Implemented this change.

  6. in src/logging.h:66 in ccd90a7512 outdated
      61 | @@ -62,6 +62,8 @@ struct LogCategory {
      62 |  
      63 |  namespace BCLog {
      64 |      constexpr auto DEFAULT_LOG_LEVEL{Level::Debug};
      65 | +    //! Log messages at this severity level and above are always emitted regardless of category settings.
      66 | +    constexpr auto UNCONDITIONAL_LOG_LEVEL{Level::Info};
    


    l0rinc commented at 8:41 AM on May 21, 2026:

    Could this naming cleanup be split before the atomic-mask change? The current code already has one threshold that means "levels at or above this always log". Giving that threshold a shared name first would make the later mask change smaller, and the startup parsing checks could use the same name instead of hardcoding Level::Info.

    diff --git a/src/logging.cpp b/src/logging.cpp
    index 3373063c05..e0c2b2cef2 100644
    --- a/src/logging.cpp
    +++ b/src/logging.cpp
    @@ -21,7 +21,6 @@ using util::Join;
     using util::RemovePrefixView;
     
     const char * const DEFAULT_DEBUGLOGFILE = "debug.log";
    -constexpr auto MAX_USER_SETABLE_SEVERITY_LEVEL{BCLog::Level::Info};
     
     BCLog::Logger& LogInstance()
     {
    @@ -162,7 +161,7 @@ bool BCLog::Logger::WillLogCategoryLevel(BCLog::LogFlags category, BCLog::Level
     {
         // Log messages at Info, Warning and Error level unconditionally, so that
         // important troubleshooting information doesn't get lost.
    -    if (level >= BCLog::Level::Info) return true;
    +    if (level >= BCLog::UNCONDITIONAL_LOG_LEVEL) return true;
     
         if (!WillLogCategory(category)) return false;
     
    @@ -263,7 +262,7 @@ static std::string LogCategoryToStr(BCLog::LogFlags category)
         return it->second;
     }
     
    -static std::optional<BCLog::Level> GetLogLevel(std::string_view level_str)
    +std::optional<BCLog::Level> BCLog::Logger::GetLogLevel(std::string_view level_str)
     {
         if (level_str == "trace") {
             return BCLog::Level::Trace;
    @@ -586,7 +585,7 @@ bool BCLog::LogRateLimiter::Stats::Consume(uint64_t bytes)
     bool BCLog::Logger::SetLogLevel(std::string_view level_str)
     {
         const auto level = GetLogLevel(level_str);
    -    if (!level.has_value() || level.value() > MAX_USER_SETABLE_SEVERITY_LEVEL) return false;
    +    if (!level.has_value() || level.value() > BCLog::UNCONDITIONAL_LOG_LEVEL) return false;
         m_log_level = level.value();
         return true;
     }
    @@ -597,7 +596,7 @@ bool BCLog::Logger::SetCategoryLogLevel(std::string_view category_str, std::stri
         if (!flag) return false;
     
         const auto level = GetLogLevel(level_str);
    -    if (!level.has_value() || level.value() > MAX_USER_SETABLE_SEVERITY_LEVEL) return false;
    +    if (!level.has_value() || level.value() > BCLog::UNCONDITIONAL_LOG_LEVEL) return false;
     
         STDLOCK(m_cs);
         m_category_log_levels[*flag] = level.value();
    diff --git a/src/logging.h b/src/logging.h
    index 67f5d6ef00..041e341d3a 100644
    --- a/src/logging.h
    +++ b/src/logging.h
    @@ -62,6 +62,8 @@ struct LogCategory {
     
     namespace BCLog {
         constexpr auto DEFAULT_LOG_LEVEL{Level::Debug};
    +    //! Log messages at this severity level and above are always emitted regardless of category settings.
    +    constexpr auto UNCONDITIONAL_LOG_LEVEL{Level::Info};
         constexpr size_t DEFAULT_MAX_LOG_BUFFER{1'000'000}; // buffer up to 1MB of log data prior to StartLogging
         constexpr uint64_t RATELIMIT_MAX_BYTES{1_MiB}; // maximum number of bytes per source location that can be logged within the RATELIMIT_WINDOW
         constexpr auto RATELIMIT_WINDOW{1h}; // time window after which log ratelimit stats are reset
    @@ -271,6 +273,8 @@ namespace BCLog {
             //! Returns a string with all user-selectable log levels.
             std::string LogLevelsString() const;
     
    +        //! Returns the log level corresponding to a string, or nullopt if unrecognized.
    +        static std::optional<BCLog::Level> GetLogLevel(std::string_view str);
             //! Returns the string representation of a log level.
             static std::string LogLevelToStr(BCLog::Level level);
    

    And we could probably use this new constant in a few more places:

    diff --git a/src/init/common.cpp b/src/init/common.cpp
    --- a/src/init/common.cpp	(revision c9516e72f9c26f76aacc510e9d39448291fc2994)
    +++ b/src/init/common.cpp	(revision f1467268d1159abb523fd65062d7533100a15217)
    @@ -67,7 +67,7 @@
             if (level_str.find_first_of(':', 3) == std::string::npos) {
                 // user passed a global log level, i.e. -loglevel=<level>
                 const auto level{BCLog::Logger::GetLogLevel(level_str)};
    -            if (!level || *level > BCLog::Level::Info) {
    +            if (!level || *level > BCLog::UNCONDITIONAL_LOG_LEVEL) {
                     return util::Error{strprintf(_("Unsupported global logging level %s=%s. Valid values: %s."), "-loglevel", level_str, LogInstance().LogLevelsString())};
                 }
                 global_level = *level;
    @@ -76,7 +76,7 @@
                 const auto& toks = SplitString(level_str, ':');
                 const auto category{toks.size() == 2 ? BCLog::Logger::GetLogCategory(toks[0]) : std::nullopt};
                 const auto level{toks.size() == 2 ? BCLog::Logger::GetLogLevel(toks[1]) : std::nullopt};
    -            if (!category || !level || *level > BCLog::Level::Info) {
    +            if (!category || !level || *level > BCLog::UNCONDITIONAL_LOG_LEVEL) {
                     return util::Error{strprintf(_("Unsupported category-specific logging level %1$s=%2$s. Expected %1$s=<category>:<loglevel>. Valid categories: %3$s. Valid loglevels: %4$s."), "-loglevel", level_str, LogInstance().LogCategoriesString(), LogInstance().LogLevelsString())};
                 }
                 category_levels.emplace_back(*category, *level);
    

    ryanofsky commented at 4:23 PM on May 27, 2026:

    re: #35322 (review)

    Could this naming cleanup be split before the atomic-mask change?

    Following your other suggestion #35322 (review) this -loglevel parsing code is now dropped from the logger class.

    the startup parsing checks could use the same name instead of hardcoding Level::Info.

    This is probably worth improving still. I'm happy to follow any preferences expressed in this PR or #35387 which improves the -loglevel implementation specifically. For now the code is using Info just to be explicit about the behavior it is trying to preserve.

  7. in src/logging.cpp:147 in ccd90a7512 outdated
     177 | +}
     178 |  
     179 | -    STDLOCK(m_cs);
     180 | -    const auto it{m_category_log_levels.find(category)};
     181 | -    return level >= (it == m_category_log_levels.end() ? LogLevel() : it->second);
     182 | +BCLog::Logger::LogLevels BCLog::Logger::GetLogLevels() const
    


    l0rinc commented at 11:03 AM on May 21, 2026:

    This looks like save/restore plumbing for tests, not part of the core representation swap. Could it be split into the commit where the test fixtures first need to snapshot the level masks?

    diff --git a/src/logging.cpp b/src/logging.cpp
    index 6e24f2d8d9..3330ad9703 100644
    --- a/src/logging.cpp
    +++ b/src/logging.cpp
    @@ -144,6 +144,15 @@ bool BCLog::Logger::WillLogCategoryLevel(BCLog::LogFlags category, BCLog::Level
         return (m_levels[static_cast<size_t>(level)].load(std::memory_order_relaxed) & category) != 0;
     }
     
    +BCLog::Logger::LogLevels BCLog::Logger::GetLogLevels() const
    +{
    +    LogLevels snapshot;
    +    for (size_t i = 0; i < m_levels.size(); ++i) {
    +        snapshot[i] = m_levels[i].load(std::memory_order_relaxed);
    +    }
    +    return snapshot;
    +}
    +
     bool BCLog::Logger::SetLogLevel(std::string_view level_str)
     {
         const auto level{GetLogLevel(level_str)};
    diff --git a/src/logging.h b/src/logging.h
    index 94eaeeac95..07a20dd1b9 100644
    --- a/src/logging.h
    +++ b/src/logging.h
    @@ -244,10 +244,10 @@ namespace BCLog {
             void AddCategoryLogLevel(LogFlags category, Level level) { SetCategoryLogLevel(category, level); }
     
             Level LogLevel() const { return DEFAULT_LOG_LEVEL; }
    -        void SetLogLevel(Level level) { SetCategoryLogLevel(GetCategoryMask(), level); }
    +        void SetLogLevel(Level level) { SetCategoryLogLevel(GetLogLevels().back(), level); }
             bool SetLogLevel(std::string_view level);
     
    -        CategoryMask GetCategoryMask() const { return m_levels[static_cast<size_t>(Level::Debug)].load(std::memory_order_relaxed); }
    +        CategoryMask GetCategoryMask() const { return GetLogLevels().back(); }
     
             void EnableCategory(LogFlags flag) { SetCategoryLogLevel(flag, BCLog::Level::Debug); }
             bool EnableCategory(std::string_view str);
    @@ -275,6 +275,11 @@ namespace BCLog {
             //! Returns the string representation of a log level.
             static std::string LogLevelToStr(BCLog::Level level);
     
    +        //! Snapshot type for the per-level category bitmask array (non-atomic, for save/restore).
    +        using LogLevels = std::array<CategoryMask, static_cast<size_t>(UNCONDITIONAL_LOG_LEVEL)>;
    +        //! Returns a snapshot of the current per-level category bitmasks.
    +        LogLevels GetLogLevels() const;
    +
             bool DefaultShrinkDebugFile() const { return !WillLogCategoryLevel(BCLog::ALL, BCLog::Level::Debug); }
         };
    

    ryanofsky commented at 4:23 PM on May 27, 2026:

    re: #35322 (review)

    This looks like save/restore plumbing for tests, not part of the core representation swap. Could it be split into the commit where the test fixtures first need to snapshot the level masks?

    Yes, this should be done now.

  8. in src/test/logging_tests.cpp:230 in ccd90a7512 outdated
     230 |  
     231 | -        const auto& category_levels{LogInstance().CategoryLevels()};
     232 | -        const auto net_it{category_levels.find(BCLog::LogFlags::NET)};
     233 | -        BOOST_REQUIRE(net_it != category_levels.end());
     234 | -        BOOST_CHECK_EQUAL(net_it->second, BCLog::Level::Trace);
     235 | +        BOOST_CHECK(LogInstance().WillLogCategoryLevel(BCLog::NET, BCLog::Level::Trace));
    


    l0rinc commented at 12:55 PM on May 21, 2026:

    Isn’t this a behavior change?

    Previously, setting a category level at startup stored the override, but it had no visible effect unless the category was also enabled with -debug. Here, setting -loglevel=net:trace appears to enable net by itself.


    ryanofsky commented at 4:27 PM on May 27, 2026:

    re: #35322 (review)

    Isn’t this a behavior change?

    Yes, nice catch! I do think this behavior change is good, but it wasn't intended to be part of this PR. It's fixed by making -loglevel only update the levels of previously enabled categories.

    Edit: To follow up, I should add a test to make sure a standalone -loglevel option without -debug continues to be ignored.

  9. DrahtBot added the label Needs rebase on May 22, 2026
  10. in src/logging.h:151 in ccd90a7512 outdated
     155 | -        std::atomic<CategoryMask> m_categories{BCLog::NONE};
     156 | +        //! Per-level log category bitmasks. m_levels[i] holds a mask of log categories
     157 | +        //! whose verbosity threshold is <= Level(i), so that ShouldLog(cat, Level(i))
     158 | +        //! is true iff (m_levels[i] & cat) != 0. Invariant: if m_levels[i] has a
     159 | +        //! category bit set, all m_levels[j] with j > i also have that bit set.
     160 | +        std::array<std::atomic<CategoryMask>, static_cast<size_t>(UNCONDITIONAL_LOG_LEVEL)> m_levels{BCLog::NONE, BCLog::NONE};
    


    l0rinc commented at 11:26 AM on May 22, 2026:

    Could the comment say explicitly that m_levels only stores conditional levels (Trace and Debug), while Info and above bypass this array? I initially read the threshold wording as trying to represent all severities, but the array deliberately stops at UNCONDITIONAL_LOG_LEVEL.

    Seems we have two fundamentally different ways of logging that we're forcing together here. Do you anticipate category-based filtering for all levels in the future?

    I find this design confusing for multiple reasons:

    • Should we guard against setting log levels for unconditional logs, either through the API or internal checks? My understanding is that we're setting dummy values there and just ignoring them later.
    • debug/trace can also be turned on by BCLog::ALL, and they would take a different route than unconditional info/warn/error.

    ryanofsky commented at 4:48 PM on May 27, 2026:

    re: #35322 (review)

    Could the comment say explicitly that m_levels only stores conditional levels (Trace and Debug), while Info and above bypass this array?

    Yes, changed array declaration and added comment to make this clearer.

    Seems we have two fundamentally different ways of logging that we're forcing together here.

    I don't think that's right. Logger accepts a category and severity level for each log message and it provides a SetCategoryLogLevel(category, level) method to set a minimum severity level for each category and control what is logged.

    The fact that Logger always logs the highest severity messages seems like an internal implementation detail, not a reason to complicate its API. Or if the API should be more complicated, it would be good to know why, or what the goal would be.

    Do you anticipate category-based filtering for all levels in the future?

    No, I think it makes sense to have some levels that can't be turned off or filtered by category. (IMO it would best if to introduce a new critical level for this purpose, so lower-severity errors, warnings, and status messages could be distinguished from each other. But this is just a weak preference and should be unrelated to this PR.)

    Should we guard against setting log levels for unconditional logs, either through the API or internal checks?

    I don't think I have an opinion about this. Having something like separate enums for conditional and unconditional levels could be reasonable, but I'd want to know what the goal would be, or what types of bugs this could prevent.

    • debug/trace can also be turned on by BCLog::ALL, and they would take a different route than unconditional info/warn/error.

    I'm not sure I understand the concern. Inside the Logger class, all messages are taking the same route. It is intentional to provide a way to turn on debug and tracing for all categories. The decision to make it difficult to turn off info/warning/error logs doesn't seem like it should affect this.

  11. in src/logging.cpp:143 in ccd90a7512 outdated
     170 |      // Log messages at Info, Warning and Error level unconditionally, so that
     171 |      // important troubleshooting information doesn't get lost.
     172 | -    if (level >= BCLog::Level::Info) return true;
     173 | -
     174 | -    if (!WillLogCategory(category)) return false;
     175 | +    if (level >= BCLog::UNCONDITIONAL_LOG_LEVEL) return true;
    


    l0rinc commented at 11:51 AM on May 22, 2026:

    This seems like a code smell (even though it was similar before). Wouldn't the categories already return the same value here - Is this an optimization? So if info has BCLog::LogFlags::ALL it will take this branch, but if debug has the same it will take the next line?


    ryanofsky commented at 4:58 PM on May 27, 2026:

    re: #35322 (review)

    This seems like a code smell (even though it was similar before)

    Yeah agree this could be better, should be improved now but let me know.

  12. in src/logging.cpp:128 in ccd90a7512 outdated
     124 | @@ -125,55 +125,39 @@ void BCLog::Logger::DisableLogging()
     125 |      StartLogging();
     126 |  }
     127 |  
     128 | -void BCLog::Logger::EnableCategory(BCLog::LogFlags flag)
     129 | +void BCLog::Logger::SetCategoryLogLevel(CategoryMask category, BCLog::Level level)
    


    l0rinc commented at 1:44 PM on May 22, 2026:

    This loop has dual behavior that needed extra explanation in the header. Maybe we could make it more obvious via code instead:

    void BCLog::Logger::SetCategoryLogLevel(CategoryMask category, BCLog::Level level)
    {
        auto level_it{m_levels.begin()};
        for (size_t i{0}; level_it != m_levels.end() && i < size_t(level); ++i, ++level_it) {
            level_it->fetch_and(~category, std::memory_order_relaxed);
        }
        for (; level_it != m_levels.end(); ++level_it) {
            level_it->fetch_or(category, std::memory_order_relaxed);
        }
    }
    

    But even this seems excessive if indeed the only phase transitions are:

      Set Trace -> Trace mask on, Debug mask on
      Set Debug -> Trace mask off, Debug mask on
      Set Info  -> Trace mask off, Debug mask off
    

    ryanofsky commented at 5:05 PM on May 27, 2026:

    re: #35322 (review)

    This loop has dual behavior that needed extra explanation in the header. Maybe we could make it more obvious via code instead

    I added a comment here to be clearer, and I'm open to feedback on different ways to write this, but I feel like the current version is actually simpler than the suggestions (of using two for loops or hardcoding severity constants). The current code just loops through each level and setting category bits to 1 if the level is enabled, 0 if disabled.

  13. in src/rpc/node.cpp:208 in ccd90a7512 outdated
     212 | -        if (!success) {
     213 | +        const auto flag{GetLogCategory(cat)};
     214 | +        if (!flag) {
     215 |              throw JSONRPCError(RPC_INVALID_PARAMETER, "unknown logging category " + cat);
     216 |          }
     217 | +        LogInstance().SetCategoryLogLevel(*flag, enable ? BCLog::Level::Debug : BCLog::Level::Info);
    


    l0rinc commented at 2:20 PM on May 22, 2026:

    Could we add a test that goes through this runtime-enable path after startup configured the category at Trace?

    The PR description notes that runtime enables now use Debug even if startup used Trace. A test through the RPC/kernel entry point would make that intentional behavior change easy to verify.


    ryanofsky commented at 5:07 PM on May 27, 2026:

    re: #35322 (review)

    Could we add a test that goes through this runtime-enable path after startup configured the category at Trace?

    The PR description notes that runtime enables now use Debug even if startup used Trace. A test through the RPC/kernel entry point would make that intentional behavior change easy to verify.

    Yes this is a good idea. Current PR doesn't add new tests but it should and I will follow up with this.

  14. in src/rpc/node.cpp:256 in ccd90a7512 outdated
     256 |  
     257 |      // Update libevent logging if BCLog::LIBEVENT has changed.
     258 | -    if (changed_log_categories & BCLog::LIBEVENT) {
     259 | -        UpdateHTTPServerLogging(LogInstance().WillLogCategory(BCLog::LIBEVENT));
     260 | +    if (original_libevent != LogInstance().WillLogCategoryLevel(BCLog::LIBEVENT, BCLog::Level::Debug)) {
     261 | +        UpdateHTTPServerLogging(LogInstance().WillLogCategoryLevel(BCLog::LIBEVENT, BCLog::Level::Debug));
    


    l0rinc commented at 2:30 PM on May 22, 2026:

    We can probably deduplicate this:

        bool is_libevent_enabled{LogInstance().WillLogCategoryLevel(BCLog::LIBEVENT, BCLog::Level::Debug)};
        if (original_libevent != is_libevent_enabled) UpdateHTTPServerLogging(is_libevent_enabled);
    

    ryanofsky commented at 5:07 PM on May 27, 2026:

    re: #35322 (review)

    We can probably deduplicate this:

    Make sense, appied suggestion

  15. in src/kernel/bitcoinkernel.cpp:769 in ccd90a7512 outdated
     770 | -    if (category == btck_LogCategory_ALL) {
     771 | -        LogInstance().SetLogLevel(get_bclog_level(level));
     772 | -    }
     773 | -
     774 | -    LogInstance().AddCategoryLogLevel(get_bclog_flag(category), get_bclog_level(level));
     775 | +    LogInstance().SetCategoryLogLevel(get_bclog_flag(category), get_bclog_level(level));
    


    l0rinc commented at 2:36 PM on May 22, 2026:

    This looks similar to the earlier concern. Isn't this a behavior change as well? Previously, setting a category level did not enable the category. Now it looks like setting the level also enables it. Should we adjust this to match the old behavior? I see the tests don't always keep the enable-first-set-level after, but maybe we can do that:

    diff --git a/src/test/kernel/test_kernel.cpp b/src/test/kernel/test_kernel.cpp
    --- a/src/test/kernel/test_kernel.cpp	(revision 06d9f340624c880883fba2b03aeab1b74d8812a8)
    +++ b/src/test/kernel/test_kernel.cpp	(revision c9516e72f9c26f76aacc510e9d39448291fc2994)
    @@ -636,6 +636,7 @@
         };
     
         logging_set_options(logging_options);
    +    logging_enable_category(LogCategory::BENCH);
         logging_set_level_category(LogCategory::BENCH, LogLevel::TRACE_LEVEL);
         logging_disable_category(LogCategory::BENCH);
         logging_enable_category(LogCategory::VALIDATION);
    @@ -643,8 +644,8 @@
     
         // Check that connecting, connecting another, and then disconnecting and connecting a logger again works.
         {
    -        logging_set_level_category(LogCategory::KERNEL, LogLevel::TRACE_LEVEL);
             logging_enable_category(LogCategory::KERNEL);
    +        logging_set_level_category(LogCategory::KERNEL, LogLevel::TRACE_LEVEL);
             Logger logger{std::make_unique<TestLog>()};
             Logger logger_2{std::make_unique<TestLog>()};
         }
    

    ryanofsky commented at 5:16 PM on May 27, 2026:

    re: #35322 (review)

    This looks similar to the earlier concern. Isn't this a behavior change as well?

    Yes this analogous to the other change you pointed out #35322 (review).

    In this case I think the new behavior is probably better as there is no point to kernel code setting a level for a category that is ignored because the category is enabled. (For bitcoind it might have made sense previously because logging RPC is more limited than the kernel API.)

    But this behavior change should be documented and tested, so I will try to add your test suggestion and follow up.

  16. l0rinc changes_requested
  17. l0rinc commented at 2:56 PM on May 22, 2026: contributor

    I tried splitting this locally to make review easier. I still think the current version mixes several separate concerns, and I have questions about whether grouping levels and categories this way is the right model, since it seems to apply a general rule to behavior that is currently quite specific and ad hoc.

    Left comments on splitting the commit into smaller reviewable steps, clarifying or preserving behavior changes, and adding focused tests for intentional changes.

    Main concerns are startup/kernel logging semantics, runtime-enable behavior coverage, and making the new m_levels model easier to reason about.

  18. ryanofsky force-pushed on May 26, 2026
  19. ryanofsky commented at 8:20 PM on May 26, 2026: contributor

    <!-- begin push-2 -->

    Rebased ccd90a75125586f4f5a03fda5477738b10745d20 -> a76b13796a59e0c91548882c1a1970943a200153 (pr/levels.1 -> pr/levels.2, compare)<!-- end --> due to conflict with #34806

  20. DrahtBot added the label CI failed on May 26, 2026
  21. DrahtBot commented at 9:47 PM on May 26, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task Windows-cross to x86_64, msvcrt: https://github.com/bitcoin/bitcoin/actions/runs/26472785758/job/77960548589</sub> <sub>LLM reason (✨ experimental): CI failed due to a C++ build error in src/init/common.cpp: GetLogCategory is not declared (missing/renamed logging helper).</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  22. DrahtBot removed the label Needs rebase on May 26, 2026
  23. logging test: extend -loglevel test to check behavior with -debug
    Currently there are no tests checking behavior of -loglevel and -debug options
    together, even though they need to be used together for -loglevel to have any
    visible effect. This commit extends -loglevel unit tests to check its behavior
    with and without -debug.
    
    Background: Using the -loglevel option alone has no effect on log output,
    because it only assigns log levels (info/debug/trace) to categories, but
    doesn't "enable" the categories, It needs to be combined with -debug which does
    enable them to take effect. (There are reasons to want to change this behavior,
    see bitcoin/bitcoin#35387, but changing it is beyond the scope of this PR.) The
    point of this commit to extend the test to provide meaningful coverage for
    subsequent commits.
    
    In addition to adding -debug arguments, make the following test improvements:
    
    - Add `init::SetLoggingCategories` calls needed to parse -debug options
    
    - Replace const char*[] variables with std::array to avoid needing to hardcode
      parameter counts.
    
    - Drop unused result variables
    
    - Check WillLogCategoryLevel results to verify -loglevel settings actually
      have expected impact on what is logged and not logged.
    
    - Replace -loglevel=debug options with -loglevel=trace options. Passing
      -loglevel=debug is somewhat pointless because the default global log level is
      already -debug.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    af3e850146
  24. ryanofsky force-pushed on May 27, 2026
  25. ryanofsky commented at 5:39 PM on May 27, 2026: contributor

    Thanks for the review! PR is now split up instead of being a single commit and I implemented most suggestions, but need to follow up on the ideas for adding more tests. (EDIT: tests are added now)

    <!-- begin push-3 -->

    Updated a76b13796a59e0c91548882c1a1970943a200153 -> 30cb9ee48027f4b32ca1c98e2cafc1a6e4388e53 (pr/levels.2 -> pr/levels.3, compare)<!-- end --> with review suggestions

    <!-- begin push-4 -->

    Updated 30cb9ee48027f4b32ca1c98e2cafc1a6e4388e53 -> dd4b33e01ca2441fcb875e0ecb084643ecc50cd8 (pr/levels.3 -> pr/levels.4, compare)<!-- end --> adding new tests commits before changes to cover more behaviors, also making other small cleanups

    <!-- begin push-5 -->

    Updated dd4b33e01ca2441fcb875e0ecb084643ecc50cd8 -> 70b4bee3cc7392a8189a98d35f382e210ae7c2cc (pr/levels.4 -> pr/levels.5, compare)<!-- end --> to fix CI link errors: LogInstance() hidden by REDUCE_EXPORTS=ON; override visibility for logging.cpp in kernel build https://github.com/bitcoin/bitcoin/actions/runs/26643504769/job/78546254795<!-- end -->

    <!-- begin push-6 -->

    Updated 70b4bee3cc7392a8189a98d35f382e210ae7c2cc -> be5e736f4976686c3fa0200f35e10ce0b656c351 (pr/levels.5 -> pr/levels.6, compare) to fix Windows DLL link errors in test_kernel https://github.com/bitcoin/bitcoin/actions/runs/26787798064/job/78967518054<!-- end -->

    <!-- begin push-7 -->

    Updated be5e736f4976686c3fa0200f35e10ce0b656c351 -> 395473b97ad229d4c80c45bd5dcfa4c885acf09d (pr/levels.6 -> pr/levels.7, compare)<!-- end --> to fix Windows cross CI: --export-dynamic-symbol and -fvisibility=default are ELF-only; use .def file to export logging symbols from DLL https://github.com/bitcoin/bitcoin/actions/runs/27141219495/job/80118211985<!-- end -->

  26. DrahtBot removed the label CI failed on May 27, 2026
  27. logging test: add behavior assertions to kernel logging_tests
    Documents two behaviors of the kernel logging API that change in the
    upcoming m_levels refactor:
    
    1. logging_set_level_category does not enable the category; a separate
       logging_enable_category call is required. After the refactor,
       set_level_category will also enable the category.
    
    2. When a category is enabled after its level has been set,
       logging_enable_category uses the previously-assigned level (Trace here).
       After the refactor, logging_enable_category always enables at Debug
       regardless of any previously-assigned level.
    
    Adds #include <logging.h> to access LogInstance().WillLogCategoryLevel()
    for the assertions.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    804ea4858e
  28. logging: replace m_categories/m_log_level/m_category_log_levels with m_levels array
    Replaces three separate private fields with a single array of atomic
    category bitmasks indexed by log level:
    
      std::array<std::atomic<CategoryMask>, 2> m_levels;
    
    Each category now has a single level threshold (Trace, Debug, or Info),
    eliminating the separate enabled/disabled flag and stored global log level.
    This reduces the number of possible states, removes mutex locking and hash
    lookups from log-level checks, and simplifies the Logger interface by
    dropping multiple enable, disable, and query methods that are no longer
    necessary.
    
    This is mostly a behavior-preserving refactor. The one exception is that
    categories enabled at runtime via the `logging` RPC are now always enabled
    at debug level, regardless of whether `-loglevel=trace` is set. The kernel
    logging API (`btck_logging_*`) has analogous behavior changes for the same
    reason. This is unlikely to matter in practice: very little LogTrace logging
    exists, and both APIs are being reworked (bitcoin/bitcoin#34038 and
    bitcoin/bitcoin#34374). The `-loglevel` setting otherwise continues to work
    exactly as before when categories are not changed at runtime.
    
    Old methods (EnableCategory, DisableCategory, WillLogCategory,
    GetCategoryMask, CategoryLevels, AddCategoryLogLevel, SetCategoryLogLevel
    overloads, LogLevel, SetLogLevel, DefaultShrinkDebugFile) are kept as
    backward-compatible wrappers and will be removed in subsequent commits.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    1cfb06db14
  29. logging refactor: replace WillLogCategory with WillLogCategoryLevel
    This commit does not change behavior in any way.
    
    Co-authored-by: l0rinc <pap.lorinc@gmail.com>
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    97c1b4ce94
  30. logging refactor: replace DefaultShrinkDebugFile with WillLogCategoryLevel
    This commit does not change behavior in any way.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    5e173f8388
  31. logging refactor: rename node::getLogCategories() to isAnyDebugLoggingEnabled()
    This commit does not change behavior in any way.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    3bdc550773
  32. logging refactor: add GetLogLevels/SetLogLevels; use them in tests
    This commit does not change behavior in any way.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    7ec14aae82
  33. logging refactor: replace EnableCategory with SetCategoryLogLevel
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    1dff4a07d9
  34. logging refactor: replace DisableCategory with SetCategoryLogLevel
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    84ade296c4
  35. logging refactor: replace AddCategoryLogLevel with SetCategoryLogLevel
    This commit does not change behavior in any way.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    5b4bde0fbb
  36. logging refactor: drop EnableCategory/DisableCategory
    This commit does not change behavior in any way.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    cd9c75f3da
  37. logging refactor: replace SetLogLevel with SetCategoryLogLevel
    This commit does not change behavior in any way.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    e54be1375b
  38. logging refactor: drop SetCategoryLogLevel(string_view)
    This commit does not change behavior in any way.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    990d4c55da
  39. logging refactor: drop LogLevel
    This commit does not change behavior in any way.
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    395473b97a
  40. ryanofsky force-pushed on May 29, 2026
  41. DrahtBot added the label CI failed on May 29, 2026
  42. DrahtBot commented at 6:10 PM on May 29, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task Windows-cross to x86_64, ucrt: https://github.com/bitcoin/bitcoin/actions/runs/26643504769/job/78546254615</sub> <sub>LLM reason (✨ experimental): Link step for test_kernel.exe failed with undefined references to LogInstance() / BCLog::Logger::WillLogCategoryLevel().</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  43. ryanofsky force-pushed on Jun 1, 2026
  44. ryanofsky force-pushed on Jun 8, 2026
  45. ryanofsky force-pushed on Jun 8, 2026
  46. DrahtBot removed the label CI failed on Jun 8, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-06-11 10:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me