logging: make trace logging easily usable #35387

pull ryanofsky wants to merge 2 commits into bitcoin:master from ryanofsky:pr/loglevel changing 11 files +290 −41
  1. ryanofsky commented at 8:34 PM on May 26, 2026: contributor

    Problem: Currently it is difficult to enable trace logging.

    The -loglevel=trace setting works, but does nothing by default and needs to be combined with -debug settings to have an effect.

    Additionally, it is not possible to switch between debug and trace logging at runtime without restarting the node. These problems make trace logging less useful than it could be, effectively make existing trace logs undiscoverable, and making it not worthwhile to add new tracing.

    Solution: Fix these issues by allowing the -loglevel option to work without an accompanying -debug option, and by adding a matching loglevel RPC that allows log levels to be configured at runtime.

    Usage examples:

    bitcoin rpc loglevel                               # See current log levels
    bitcoin rpc loglevel trace                         # Set global level to -loglevel=trace
    bitcoin rpc loglevel debug                         # Set global level to -loglevel=debug
    bitcoin rpc loglevel info                          # Set global level to -loglevel=info
    bitcoin rpc loglevel libevent=info                 # Set category level to -loglevel=libevent:info
    bitcoin rpc loglevel trace net=debug libevent=info # Set global & category levels to -loglevel=trace,net:debug,libevent:info
    

    Compatibility: This PR is backwards compatible and doesn't change the -debug and -debugexclude options (which become synonyms for -loglevel=debug and -loglevel=info, respectively). The logging RPC is also mostly unchanged, except now it validates category names before updating log levels, instead of failing with half-applied changes. Also the logging RPC now only toggles between info and debug levels instead of trying to remember previously assigned levels, which was needlessly confusing.

  2. DrahtBot commented at 8:34 PM on May 26, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #36268 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36268.svg"></sub> (refactor: prune unused semi-colons by fanquake)
    • #35322 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35322.svg"></sub> (logging: streamline Logger state and drop redundant methods by ryanofsky)
    • #34038 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/34038.svg"></sub> (logging: replace -loglevel with -trace, expose trace logging via RPC by ajtowns)
    • #31260 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/31260.svg"></sub> (scripted-diff: Type-safe settings retrieval by ryanofsky)
    • #17783 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/17783.svg"></sub> (common: Disallow calling IsArgSet() on ALLOW_LIST options by ryanofsky)
    • #17581 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/17581.svg"></sub> (refactor: Remove settings merge reverse precedence code by ryanofsky)
    • #17580 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/17580.svg"></sub> (refactor: Add ALLOW_LIST flags and enforce usage in CheckArgFlags by ryanofsky)
    • #17493 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/17493.svg"></sub> (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-->

    LLM Linter (✨ experimental)

    Possible places where comparison-specific test macros should replace generic comparisons:

    • test/functional/feature_logging.py
      assert result['net'] == 'trace' / assert result['http'] == 'info' -> use assert_equal(result['net'], 'trace') and assert_equal(result['http'], 'info')

    • test/functional/feature_logging.py
      assert result['net'] == 'debug' -> use assert_equal(result['net'], 'debug')

    • test/functional/feature_logging.py
      assert result['net'] == 'info' -> use assert_equal(result['net'], 'info')

    • test/functional/feature_logging.py
      assert result['net'] == 'trace' / assert result['http'] == 'info' (in the all='info', net='trace' block) -> use assert_equal(...) for each comparison

    • test/functional/feature_logging.py
      assert result['net'] == 'info' / assert result['http'] == 'trace' (in the all='trace', net='info' block) -> use assert_equal(...) for each comparison

    • test/functional/feature_logging.py
      assert active == expected_active, f"Inconsistency for {cat}: logging={active}, loglevel={levels_result[cat]}" -> use assert_equal(active, expected_active, f"...")

    <sup>2026-07-14 00:37:40</sup>

  3. DrahtBot added the label CI failed on May 26, 2026
  4. DrahtBot commented at 10:07 PM on May 26, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task macOS native, fuzz: https://github.com/bitcoin/bitcoin/actions/runs/26473571406/job/77953158807</sub> <sub>LLM reason (✨ experimental): CI failed because the fuzz target rpc crashed on an unknown RPC command loglevel (not listed in RPC_COMMANDS_SAFE_FOR_FUZZING/RPC_COMMANDS_NOT_SAFE_FOR_FUZZING), causing exit code -6.</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>

  5. ryanofsky force-pushed on May 27, 2026
  6. ryanofsky commented at 12:50 AM on May 27, 2026: contributor

    Updated 2e4ac6d568af2ae0bf3e81748ba1a63d0cdb71b8 -> 828a0287fa194b7b94e600a10e06e21f74434366 (pr/loglevel.2 -> pr/loglevel.3, compare)<!-- end --> to fix clang-tidy and other ci errors https://github.com/bitcoin/bitcoin/actions/runs/26473571406/job/77954549771?pr=35387

    <!-- begin push-4 -->

    Rebased 828a0287fa194b7b94e600a10e06e21f74434366 -> bcf1c44df17a2c6ec3ff04582ee435b67cf2f187 (pr/loglevel.3 -> pr/loglevel.4, compare)<!-- end --> due to conflict with #34411 (Full Libevent removal)

    <!-- begin push-5 -->

    Rebased bcf1c44df17a2c6ec3ff04582ee435b67cf2f187 -> dffb8cf9e7bda3a1bf1f26e1760e486006ae6a63 (pr/loglevel.4 -> pr/loglevel.5, compare)<!-- end --> due to conflict with #35597

  7. DrahtBot removed the label CI failed on May 27, 2026
  8. in src/rpc/node.cpp:258 in 828a0287fa
     264 | +        [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue
     265 | +{
     266 | +    std::vector<std::pair<BCLog::LogFlags, BCLog::Level>> changes;
     267 | +
     268 | +    // Optional positional "level" param.
     269 | +    if (!request.params[0].isNull()) {
    


    stickies-v commented at 10:46 AM on May 27, 2026:

    nit: the MaybeArg helper is better suited here:

    <details> <summary>git diff on 828a0287fa</summary>

    diff --git a/src/rpc/node.cpp b/src/rpc/node.cpp
    index 6de4562495..88bf0404f9 100644
    --- a/src/rpc/node.cpp
    +++ b/src/rpc/node.cpp
    @@ -235,7 +235,7 @@ static RPCMethod loglevel()
                 "The valid log levels are: " + LogInstance().LogLevelsString() + "\n"
                 ,
                     {
    -                    {"all", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Log level to set for all categories."},
    +                    {"level", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Log level to set for all categories."},
                         {"categories", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "Per-category log levels.", std::move(category_args)},
                     },
                     RPCResult{
    @@ -255,21 +255,19 @@ static RPCMethod loglevel()
         std::vector<std::pair<BCLog::LogFlags, BCLog::Level>> changes;
     
         // Optional positional "level" param.
    -    if (!request.params[0].isNull()) {
    -        const std::string level_str = request.params[0].get_str();
    -        const auto level = BCLog::Logger::GetLogLevel(level_str);
    +    if (auto level_str{self.MaybeArg<std::string_view>("level")}) {
    +        const auto level = BCLog::Logger::GetLogLevel(*level_str);
             if (!level || *level > BCLog::Level::Info) {
    -            throw JSONRPCError(RPC_INVALID_PARAMETER, "unknown log level \"" + level_str + "\". Valid values: " + LogInstance().LogLevelsString());
    +            throw JSONRPCError(RPC_INVALID_PARAMETER, tfm::format("unknown log level \"%s\". Valid values: %s", *level_str, LogInstance().LogLevelsString()));
             }
             changes.emplace_back(BCLog::ALL, *level);
         }
     
         // Named "categories" params: applied in the order they appear in the request.
         // Category names are validated by OBJ_NAMED_PARAMS, so GetLogCategory always succeeds here.
    -    if (!request.params[1].isNull()) {
    -        const UniValue& cats = request.params[1].get_obj();
    -        for (const std::string& cat : cats.getKeys()) {
    -            const std::string level_str = cats[cat].get_str();
    +    if (auto cats{self.MaybeArg<UniValue>("categories")}) {
    +        for (const std::string& cat : cats->getKeys()) {
    +            const std::string level_str = (*cats)[cat].get_str();
                 const auto level = BCLog::Logger::GetLogLevel(level_str);
                 if (!level || *level > BCLog::Level::Info) {
                     throw JSONRPCError(RPC_INVALID_PARAMETER, "unknown log level \"" + level_str + "\". Valid values: " + LogInstance().LogLevelsString());
    
    

    </details>

  9. stickies-v commented at 10:46 AM on May 27, 2026: contributor

    This looks like an elegant approach, I think it's an intuitive RPC and -loglevel interface. The PR description should probably reference #34038 as an alternativate?

    I don't think we should have 2 parallel active logging RPC methods, so imo this PR should mark logging deprecated, and we should keep it around for a long time as it's probably heavily used.

  10. ryanofsky referenced this in commit af3e850146 on May 29, 2026
  11. DrahtBot added the label Needs rebase on Jun 22, 2026
  12. ryanofsky force-pushed on Jun 24, 2026
  13. ryanofsky referenced this in commit 3299bf821f on Jun 24, 2026
  14. DrahtBot removed the label Needs rebase on Jun 24, 2026
  15. DrahtBot added the label Needs rebase on Jun 30, 2026
  16. logging: make -loglevel work standalone without -debug
    Previously, -loglevel only set level thresholds but required -debug to
    be separately specified to actually enable log categories. This made
    -loglevel harder to use than necessary.
    
    After this change, -loglevel enables categories automatically:
    - A global level (-loglevel=debug) enables all categories at that level,
      replacing the need for -debug=1.
    - A per-category level (-loglevel=net:trace) enables that category,
      replacing the need for -debug=net.
    - Comma-separated entries in one argument are now supported
      (-loglevel=debug,net:trace).
    
    The three logging options now have a fixed processing order and clear
    precedence: -debug is processed first (lowest precedence), then
    -loglevel, then -debugexclude (highest precedence). This ordering holds
    regardless of the order arguments appear on the command line or in
    config files. Equivalences:
      -debug=1        ==  -loglevel=debug
      -debug=<cat>    ==  -loglevel=<cat>:debug
      -debugexclude=<cat>  ==  -loglevel=<cat>:info
    
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    c3700df020
  17. rpc: add loglevel RPC
    Add loglevel RPC to control log levels.
    
    Co-Authored-By: stickies-v <stickies-v@protonmail.com>
    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
    dffb8cf9e7
  18. ryanofsky referenced this in commit 0d8920bb5b on Jul 14, 2026
  19. ryanofsky force-pushed on Jul 14, 2026
  20. DrahtBot removed the label Needs rebase on Jul 14, 2026
  21. ryanofsky referenced this in commit 22ef791f7e on Aug 15, 2026
  22. ryanofsky marked this as ready for review on Aug 21, 2026
  23. ryanofsky referenced this in commit 277da2d4dc on Oct 1, 2026
  24. sedited commented at 1:03 PM on October 2, 2026: contributor

    @ryanofsky can you respond to @stickies-v's comments?

  25. in doc/release-notes-35387.md:26 in dffb8cf9e7
      21 | +   bitcoin rpc loglevel                               # See current log levels
      22 | +   bitcoin rpc loglevel trace                         # Set global log level
      23 | +   bitcoin rpc loglevel debug                         # Set global log level
      24 | +   bitcoin rpc loglevel info                          # Set global log level
      25 | +   bitcoin rpc loglevel libevent=info                 # Set per-category log level
      26 | +   bitcoin rpc loglevel trace net=debug libevent=info # Set global and category levels
    


    stickies-v commented at 1:11 PM on October 7, 2026:

    I think this RPC would be easier to use and reason about if we split it into a read and a write method, where the write method makes a single change per call:

    1. getlogconfig -> see current log levels (under a levels key, so we can add other runtime log options later without breaking anything)
    2. setloglevel <level> -> set global log level
    3. setloglevel <level> <category> -> set category level

    For example, consider bitcoin rpc loglevel zmq=info trace. It's not obvious (and not documented) what the log level of zmq is going to be after this call: info or trace? If every call makes just one change, the user sorts priorities, and we keep the interface easier to understand while limiting implementation complexity and test surface (no more OBJ_NAMED_PARAMS, dynamically generated args, or client conversion entry). Furthermore, it makes it easier to upgrade the RPC later if we need to.

    It also aligns much better with the new OpenRPC spec. Params are described independently, so the schema can't express precedence or mutual exclusion between them. With setloglevel, every request that's valid according to the schema has exactly one meaning, and the signature doesn't change whenever a category is added. Splitting read and write also makes side effects obvious from the method name, and lets -rpcwhitelist grant read-only access. The main downside is that the schema no longer lists the valid categories, at least until RPCArg supports enums.

    Being able to set multiple levels in a single command is convenient on the CLI, but I think that's a CLI concern rather than an RPC one: RPC clients can just make multiple calls (or use a JSON-RPC batch), and I don't think atomicity is meaningful for logging.

    <details> <summary>git diff on 828a0287fa</summary>

    diff --git a/doc/release-notes-35387.md b/doc/release-notes-35387.md
    index 131a12f5b5..43cae071cb 100644
    --- a/doc/release-notes-35387.md
    +++ b/doc/release-notes-35387.md
    @@ -13,15 +13,14 @@ Logging
       `-loglevel=<category>:debug` being a synonym for `-debug=<category>`, and
       `-loglevel=<category>:info` being a synonym for `-debugexclude=<category>`.
     
    -- A new `loglevel` RPC has been added, which provides a superset of
    -  functionality of the `logging` RPC, and allows enabling `trace` logs as well
    -  as `debug` logs. Examples:
    +- New `getlogconfig` and `setloglevel` RPCs have been added, which provide a
    +  superset of functionality of the `logging` RPC, and allow enabling `trace`
    +  logs as well as `debug` logs. Examples:
     
       ```sh
    -   bitcoin rpc loglevel                               # See current log levels
    -   bitcoin rpc loglevel trace                         # Set global log level
    -   bitcoin rpc loglevel debug                         # Set global log level
    -   bitcoin rpc loglevel info                          # Set global log level
    -   bitcoin rpc loglevel libevent=info                 # Set per-category log level
    -   bitcoin rpc loglevel trace net=debug libevent=info # Set global and category levels
    +   bitcoin rpc getlogconfig              # See current logging configuration
    +   bitcoin rpc setloglevel trace         # Set global log level
    +   bitcoin rpc setloglevel debug         # Set global log level
    +   bitcoin rpc setloglevel info          # Set global log level
    +   bitcoin rpc setloglevel info libevent # Set per-category log level
    

    diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 98c2557a18..a28543fb3b 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -334,7 +334,6 @@ static const CRPCConvertParam vRPCConvertParams[] = { "psbtbumpfee", 1, "psbt_version"}, { "logging", 0, "include" }, { "logging", 1, "exclude" },

    • { "loglevel", 1, "categories" }, { "disconnectnode", 1, "nodeid" }, { "gethdkeys", 0, "active_only" }, { "gethdkeys", 0, "options" }, diff --git a/src/rpc/node.cpp b/src/rpc/node.cpp index 6de4562495..4c45ed129a 100644 --- a/src/rpc/node.cpp +++ b/src/rpc/node.cpp @@ -220,72 +220,80 @@ static void UpdateLogCategories(std::vector<std::pair<BCLog::LogFlags, BCLog::Le } }

    -static RPCMethod loglevel() +static UniValue LogLevelsToJSON() {

    • std::vector<RPCArg> category_args;
    • for (const auto& cat : LogInstance().LogCategoriesList()) {
    •    category_args.emplace_back(cat.category, RPCArg::Type::STR, RPCArg::Optional::OMITTED,
    •        "log level for the \"" + cat.category + "\" category");
    • UniValue levels(UniValue::VOBJ);
    • for (const auto& logCat : LogInstance().LogCategoriesList()) {
    •    levels.pushKV(logCat.category, BCLog::Logger::LogLevelToStr(logCat.level));
      }
    • return RPCMethod{"loglevel",
    •        "Gets and sets per-category log levels.\n"
    •        "When called without arguments, returns all log categories with their current log level.\n"
    •        "When called with arguments, sets the log level for specified categories,\n"
    •        "then returns the updated state of all categories.\n"
    •        "The valid log levels are: " + LogInstance().LogLevelsString() + "\n"
    •        ,
    •            {
    •                {"all", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Log level to set for all categories."},
    •                {"categories", RPCArg::Type::OBJ_NAMED_PARAMS, RPCArg::Optional::OMITTED, "Per-category log levels.", std::move(category_args)},
    •            },
    • return levels; +}
    • +static RPCResult LogLevelsResult(std::string key_name) +{
    • return RPCResult{
    •    RPCResult::Type::OBJ_DYN, std::move(key_name), "keys are the logging categories, values are their current log levels",
    •    {
    •        {RPCResult::Type::STR, "category", "current log level"},
    •    }};
      +}
    • +static RPCMethod getlogconfig() +{
    • return RPCMethod{"getlogconfig",
    •        "Returns the current logging configuration.\n",
    •            {},
                 RPCResult{
    •                RPCResult::Type::OBJ_DYN, "", "keys are the logging categories, values are their current log levels",
    •                RPCResult::Type::OBJ, "", "",
                     {
    •                    {RPCResult::Type::STR, "category", "current log level"},
    •                    LogLevelsResult("levels"),
                     }
                 },
                 RPCExamples{
    •                HelpExampleCli("loglevel", "")
    •              + HelpExampleCli("loglevel", "debug")
    •              + HelpExampleCli("-named loglevel", "info net=debug")
    •              + HelpExampleRpc("loglevel", "\"debug\"")
    •                HelpExampleCli("getlogconfig", "")
    •              + HelpExampleRpc("getlogconfig", "")
                 },
         [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue
      {
    • std::vector<std::pair<BCLog::LogFlags, BCLog::Level>> changes;
    • // Optional positional "level" param.
    • if (!request.params[0].isNull()) {
    •    const std::string level_str = request.params[0].get_str();
    •    const auto level = BCLog::Logger::GetLogLevel(level_str);
    •    if (!level || *level > BCLog::Level::Info) {
    •        throw JSONRPCError(RPC_INVALID_PARAMETER, "unknown log level \"" + level_str + "\". Valid values: " + LogInstance().LogLevelsString());
    •    }
    •    changes.emplace_back(BCLog::ALL, *level);
    • }
    • UniValue result(UniValue::VOBJ);
    • result.pushKV("levels", LogLevelsToJSON());
    • return result; +},
    • }; +}
    • // Named "categories" params: applied in the order they appear in the request.
    • // Category names are validated by OBJ_NAMED_PARAMS, so GetLogCategory always succeeds here.
    • if (!request.params[1].isNull()) {
    •    const UniValue& cats = request.params[1].get_obj();
    •    for (const std::string& cat : cats.getKeys()) {
    •        const std::string level_str = cats[cat].get_str();
    •        const auto level = BCLog::Logger::GetLogLevel(level_str);
    •        if (!level || *level > BCLog::Level::Info) {
    •            throw JSONRPCError(RPC_INVALID_PARAMETER, "unknown log level \"" + level_str + "\". Valid values: " + LogInstance().LogLevelsString());
    •        }
    •        changes.emplace_back(*BCLog::Logger::GetLogCategory(cat), *level);
    •    }
      +static RPCMethod setloglevel() +{
    • return RPCMethod{"setloglevel",
    •        "Sets the log level for a single logging category, or for all categories.\n"
    •        "Setting the log level for all categories also resets all per-category log levels.\n"
    •        "Returns the updated log levels of all categories.\n"
    •        "The valid log levels are: " + LogInstance().LogLevelsString() + "\n"
    •        "The valid logging categories are: " + LogInstance().LogCategoriesString() + "\n"
    •        ,
    •            {
    •                {"level", RPCArg::Type::STR, RPCArg::Optional::NO, "Log level to set."},
    •                {"category", RPCArg::Type::STR, RPCArg::Default{"all"}, "Category to set the log level for, or \"all\" for all categories."},
    •            },
    •            LogLevelsResult(""),
    •            RPCExamples{
    •                HelpExampleCli("setloglevel", "debug")
    •              + HelpExampleCli("setloglevel", "trace net")
    •              + HelpExampleRpc("setloglevel", "\"trace\", \"net\"")
    •            },
    •    [](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue
      +{
    • const auto level{BCLog::Logger::GetLogLevel(self.Argstd::string_view("level"))};
    • if (!level || *level > BCLog::Level::Info) {
    •    throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("unknown log level \"%s\". Valid values: %s", self.Arg<std::string_view>("level"), LogInstance().LogLevelsString()));
      }
    • if (!changes.empty()) UpdateLogCategories(std::move(changes));
    • UniValue result(UniValue::VOBJ);
    • for (const auto& logCat : LogInstance().LogCategoriesList()) {
    •    result.pushKV(logCat.category, BCLog::Logger::LogLevelToStr(logCat.level));
    • const auto category{BCLog::Logger::GetLogCategory(self.Argstd::string_view("category"))};
    • if (!category) {
    •    throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("unknown logging category %s", self.Arg<std::string_view>("category")));
      }
    • UpdateLogCategories({{*category, *level}});
    • return result;
    • return LogLevelsToJSON(); }, }; } @@ -301,7 +309,7 @@ static RPCMethod logging() "The valid logging categories are: " + LogInstance().LogCategoriesString() + "\n" "In addition, the following are available as category names with special meanings:\n" " - "all", "1" : represent all logging categories.\n"
    •        "See also: the \"loglevel\" RPC, which provides a superset of functionality, allowing trace logs to be enabled in addition to debug logs.\n"
    •        "See also: the \"getlogconfig\" and \"setloglevel\" RPCs, which provide a superset of functionality, allowing trace logs to be enabled in addition to debug logs.\n"
             ,
                 {
                     {"include", RPCArg::Type::ARR, RPCArg::Optional::OMITTED, "The categories to add to debug logging",
      @@ -489,8 +497,9 @@ void RegisterNodeRPCCommands(CRPCTable& t) { static const CRPCCommand commands[]{ {"control", &getmemoryinfo},
    •    {"control", &loglevel},
    •    {"control", &getlogconfig},
         {"control", &logging},
    •    {"control", &setloglevel},
         {"util", &getindexinfo},
         {"hidden", &setmocktime},
         {"hidden", &mockscheduler},
      diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index c81156a164..548e08f5bf 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -135,6 +135,7 @@ const std::vectorstd::string RPC_COMMANDS_SAFE_FOR_FUZZING{ "getdescriptorinfo", "getdifficulty", "getindexinfo",
    • "getlogconfig", "getmemoryinfo", "getmempoolancestors", "getmempooldescendants", @@ -163,7 +164,6 @@ const std::vectorstd::string RPC_COMMANDS_SAFE_FOR_FUZZING{ "joinpsbts", "listbanned", "logging",
    • "loglevel", "mockscheduler", "ping", "preciousblock", @@ -174,6 +174,7 @@ const std::vectorstd::string RPC_COMMANDS_SAFE_FOR_FUZZING{ "scantxoutset", "sendmsgtopeer", // when no peers are connected, no p2p message is sent "sendrawtransaction",
    • "setloglevel", "setmocktime", "setnetworkactive", "signmessagewithprivkey", diff --git a/test/functional/feature_logging.py b/test/functional/feature_logging.py index 9d6ea2be35..1f41f3e0b1 100755 --- a/test/functional/feature_logging.py +++ b/test/functional/feature_logging.py @@ -9,7 +9,10 @@ import os from test_framework.test_framework import BitcoinTestFramework from test_framework.p2p import P2PInterface from test_framework.test_node import ErrorMatch -from test_framework.util import assert_raises_rpc_error +from test_framework.util import (
    • assert_equal,
    • assert_raises_rpc_error, +)

    class LoggingTest(BitcoinTestFramework): @@ -160,66 +163,59 @@ class LoggingTest(BitcoinTestFramework): p2p.wait_for_verack() self.nodes[0].disconnect_p2ps()

    •    self.log.info("Test loglevel RPC")
    •    self.log.info("Test getlogconfig and setloglevel RPCs")
         # Start with a clean baseline: all categories at info (disabled)
         self.restart_node(0, ['-nologlevel', '-nodebug'])
    •    # Read-only call returns all categories with level strings
    •    levels = self.nodes[0].loglevel()
    •    assert isinstance(levels, dict)
    •    # getlogconfig returns all categories with level strings
    •    levels = self.nodes[0].getlogconfig()['levels']
         assert 'net' in levels
         assert 'http' in levels
    •    assert all(v in ('trace', 'debug', 'info') for v in levels.values())
    •    # All categories should be at 'info' (disabled) with our clean baseline
         assert all(v == 'info' for v in levels.values()), f"Expected all info, got: {levels}"
      
         # Set a single category to trace
    •    result = self.nodes[0].loglevel(net='trace')
    •    assert result['net'] == 'trace'
    •    assert result['http'] == 'info'
    •    result = self.nodes[0].setloglevel('trace', 'net')
    •    assert_equal(result, self.nodes[0].getlogconfig()['levels'])
    •    assert_equal(result['net'], 'trace')
    •    assert_equal(result['http'], 'info')
         # Cross-check with logging RPC: net should be active (trace < info), http inactive
         assert self.nodes[0].logging()['net']
         assert not self.nodes[0].logging()['http']
      
         # Set a single category to debug
    •    result = self.nodes[0].loglevel(net='debug')
    •    assert result['net'] == 'debug'
    •    result = self.nodes[0].setloglevel(level='debug', category='net')
    •    assert_equal(result['net'], 'debug')
      
         # Set a category back to info (disable it)
    •    result = self.nodes[0].loglevel(net='info')
    •    assert result['net'] == 'info'
    •    result = self.nodes[0].setloglevel('info', 'net')
    •    assert_equal(result['net'], 'info')
         assert not self.nodes[0].logging()['net']
    •    # Set all categories to debug using the positional shorthand
    •    result = self.nodes[0].loglevel('debug')
    •    # Set all categories to debug, the category defaults to "all"
    •    result = self.nodes[0].setloglevel('debug')
         assert all(v == 'debug' for v in result.values()), f"Expected all debug, got: {result}"
         assert self.nodes[0].logging()['net']
         assert self.nodes[0].logging()['http']
    •    # "all" named argument combined with per-category override
    •    result = self.nodes[0].loglevel(all='info', net='trace')
    •    assert result['net'] == 'trace'
    •    assert result['http'] == 'info'
    •    # Set all categories to trace, then override net back to info
    •    result = self.nodes[0].loglevel(all='trace', net='info')
    •    assert result['net'] == 'info'
    •    assert result['http'] == 'trace'
    •    # Setting the level for all categories resets per-category levels
    •    self.nodes[0].setloglevel('trace', 'net')
    •    result = self.nodes[0].setloglevel('info', 'all')
    •    assert all(v == 'info' for v in result.values()), f"Expected all info, got: {result}"
      
         # Invalid category raises an error
    •    assert_raises_rpc_error(-8, "Unknown named parameter notacategory", self.nodes[0].loglevel, **{'notacategory': 'debug'})
    •    assert_raises_rpc_error(-8, "unknown logging category notacategory", self.nodes[0].setloglevel, 'debug', 'notacategory')
      
         # Invalid level raises an error
    •    assert_raises_rpc_error(-8, "unknown log level", self.nodes[0].loglevel, net='verbose')
    •    assert_raises_rpc_error(-8, "unknown log level", self.nodes[0].setloglevel, 'verbose', 'net')
    •    # loglevel and logging RPCs stay consistent
    •    self.nodes[0].loglevel(all='info', rpc='debug')
    •    # getlogconfig and logging RPCs stay consistent
    •    self.nodes[0].setloglevel('info')
    •    self.nodes[0].setloglevel('debug', 'rpc')
         logging_result = self.nodes[0].logging()
    •    levels_result = self.nodes[0].loglevel()
    •    levels_result = self.nodes[0].getlogconfig()['levels']
         for cat, active in logging_result.items():
             expected_active = levels_result[cat] != 'info'
    •        assert active == expected_active, f"Inconsistency for {cat}: logging={active}, loglevel={levels_result[cat]}"
    •        assert active == expected_active, f"Inconsistency for {cat}: logging={active}, getlogconfig={levels_result[cat]}"

    if name == 'main': LoggingTest(file).main()

    </details>
    
  26. in src/init/common.cpp:30 in dffb8cf9e7
      26 | @@ -27,12 +27,23 @@ namespace init {
      27 |  void AddLoggingArgs(ArgsManager& argsman)
      28 |  {
      29 |      argsman.AddArg("-debuglogfile=<file>", strprintf("Specify location of debug log file (default: %s). Relative paths will be prefixed by a net-specific datadir location. Pass -nodebuglogfile to disable writing the log to a file.", DEFAULT_DEBUGLOGFILE), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
      30 | +    argsman.AddArg("-loglevel=<level>|<category>:<level>[,<category>:<level>...]",
    


    stickies-v commented at 3:19 PM on October 7, 2026:

    At startup, I think it makes sense for -loglevel to be declarative (unlike the RPC, which is imperative). <category>:<level> always overrides a less specific global <level>, regardless of ordering (or source*). If the same key is specified multiple times, the last occurrence should take precedence (taking into account source*).

    E.g. -loglevel=net:trace -loglevel=debug, or loglevel=debug in bitcoin.conf with -loglevel=net:trace on CLI, should yield trace for net and debug for everything else, but this PR makes everything debug.

    Keeping it declarative is also simpler: it removes the "global level must be specified first" error, and a global level doesn't need to reset anything.

    * bitcoin.conf vs CLI. For the same key, bitcoin.conf currently takes precedence over CLI (already the case on master). I think that's a separate issue, probably best fixed generically in ArgsManager, so leaving that out of scope here).


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-10-11 11:51 UTC

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