feat: add query functionality to log level command (#10885)
* feat: update log level command to show log levels * test: add log level tests * update TestCommands test * docs: relation to GOLOG_LOG_LEVEL * chore: update to latest go-log * fix: do not output single subsystem name in CLI * test: explicit subsystem request dont output subsystem * LevelFromString renamed to Parse * Modify `ipfs log level` * Denote default level with sdubsystem name '(defult)'. * make "*" an dalias for "all". Test to make sure both work the same.
Russell Dempsey committed
Aug 11, 2025 at 15:43 UTC
d3cc4ff587e70848bbbf206dc119922791692993
10 files changed
+860
-42
cmd/ipfs/kubo/start.go
+2
-2
@@ -214,8 +214,8 @@ func insideGUI() bool {
214
func checkDebug(req *cmds.Request) {
215
// check if user wants to debug. option OR env var.
216
debug, _ := req.Options["debug"].(bool)
217
- ipfsLogLevel, _ := logging.LevelFromString(os.Getenv("IPFS_LOGGING")) // IPFS_LOGGING is deprecated
218
- goLogLevel, _ := logging.LevelFromString(os.Getenv("GOLOG_LOG_LEVEL"))
217
+ ipfsLogLevel, _ := logging.Parse(os.Getenv("IPFS_LOGGING")) // IPFS_LOGGING is deprecated
218
+ goLogLevel, _ := logging.Parse(os.Getenv("GOLOG_LOG_LEVEL"))
219
220
if debug || goLogLevel == logging.LevelDebug || ipfsLogLevel == logging.LevelDebug {
221
u.Debug = true
core/commands/log.go
+164
-30
@@ -3,16 +3,37 @@ package commands
3
import (
4
"fmt"
5
"io"
6
+ "slices"
7
8
cmds "github.com/ipfs/go-ipfs-cmds"
9
logging "github.com/ipfs/go-log/v2"
10
)
11
11
-// Golang os.Args overrides * and replaces the character argument with
12
-// an array which includes every file in the user's CWD. As a
13
-// workaround, we use 'all' instead. The util library still uses * so
14
-// we convert it at this step.
15
-var logAllKeyword = "all"
12
+const (
13
+ // allLogSubsystems is used to specify all log subsystems when setting the
14
+ // log level.
15
+ allLogSubsystems = "*"
16
+ // allLogSubsystemsAlias is a convenience alias for allLogSubsystems that
17
+ // doesn't require shell escaping.
18
+ allLogSubsystemsAlias = "all"
19
+ // defaultLogLevel is used to request and to identify the default log
20
+ // level.
21
+ defaultLogLevel = "default"
22
+ // defaultSubsystemKey is the subsystem name that is used to denote the
23
+ // default log level. We use parentheses for UI clarity to distinguish it
24
+ // from regular subsystem names.
25
+ defaultSubsystemKey = "(default)"
26
+ // logLevelOption is an option for the tail subcommand to select the log
27
+ // level to output.
28
+ logLevelOption = "log-level"
29
+ // noSubsystemSpecified is used when no subsystem argument is provided
30
+ noSubsystemSpecified = ""
31
+)
32
+
33
+type logLevelOutput struct {
34
+ Levels map[string]string `json:",omitempty"`
35
+ Message string `json:",omitempty"`
36
+}
37
38
var LogCmd = &cmds.Command{
39
Helptext: cmds.HelpText{
@@ -39,46 +60,161 @@ system (not just for the daemon logs, but all commands):
60
61
var logLevelCmd = &cmds.Command{
62
Helptext: cmds.HelpText{
42
- Tagline: "Change the logging level.",
63
+ Tagline: "Change or get the logging level.",
64
ShortDescription: `
44
-Change the verbosity of one or all subsystems log output. This does not affect
45
-the event log.
65
+Get or change the logging level of one or all logging subsystems.
66
+
67
+This command provides a runtime alternative to the GOLOG_LOG_LEVEL
68
+environment variable for debugging and troubleshooting.
69
+
70
+UNDERSTANDING DEFAULT vs '*':
71
+
72
+The "default" level is the fallback used by unconfigured subsystems.
73
+You cannot set the default level directly - it only changes when you use '*'.
74
+
75
+The '*' wildcard represents ALL subsystems including the default level.
76
+Setting '*' changes everything at once, including the default.
77
+
78
+EXAMPLES - Getting levels:
79
+
80
+ ipfs log level # Show only the default fallback level
81
+ ipfs log level all # Show all subsystem levels (100+ lines)
82
+ ipfs log level core # Show level for 'core' subsystem only
83
+
84
+EXAMPLES - Setting levels:
85
+
86
+ ipfs log level core debug # Set 'core' to 'debug' (default unchanged)
87
+ ipfs log level all info # Set ALL to 'info' (including default)
88
+ ipfs log level core default # Reset 'core' to use current default level
89
+
90
+WILDCARD OPTIONS:
91
+
92
+Use 'all' (convenient) or '*' (requires escaping) to affect all subsystems:
93
+ ipfs log level all debug # Convenient - no shell escaping needed
94
+ ipfs log level '*' debug # Equivalent but needs quotes: '*' or "*" or \*
95
+
96
+BEHAVIOR EXAMPLES:
97
+
98
+Initial state (all using default 'error'):
99
+ $ ipfs log level => error
100
+ $ ipfs log level core => error
101
+
102
+After setting one subsystem:
103
+ $ ipfs log level core debug
104
+ $ ipfs log level => error (default unchanged!)
105
+ $ ipfs log level core => debug (explicitly set)
106
+ $ ipfs log level dht => error (still uses default)
107
+
108
+After setting everything with 'all':
109
+ $ ipfs log level all info
110
+ $ ipfs log level => info (default changed!)
111
+ $ ipfs log level core => info (all changed)
112
+ $ ipfs log level dht => info (all changed)
113
+
114
+The 'default' keyword always refers to the current default level:
115
+ $ ipfs log level => error
116
+ $ ipfs log level core default # Sets core to 'error'
117
+ $ ipfs log level all info # Changes default to 'info'
118
+ $ ipfs log level core default # Now sets core to 'info'
119
`,
120
},
121
122
Arguments: []cmds.Argument{
50
- // TODO use a different keyword for 'all' because all can theoretically
51
- // clash with a subsystem name
52
- cmds.StringArg("subsystem", true, false, fmt.Sprintf("The subsystem logging identifier. Use '%s' for all subsystems.", logAllKeyword)),
53
- cmds.StringArg("level", true, false, `The log level, with 'debug' the most verbose and 'fatal' the least verbose.
54
- One of: debug, info, warn, error, dpanic, panic, fatal.
55
- `),
123
+ cmds.StringArg("subsystem", false, false, fmt.Sprintf("The subsystem logging identifier. Use '%s' or '%s' to get or set the log level of all subsystems including the default. If not specified, only show the default log level.", allLogSubsystemsAlias, allLogSubsystems)),
124
+ cmds.StringArg("level", false, false, fmt.Sprintf("The log level, with 'debug' as the most verbose and 'fatal' the least verbose. Use '%s' to set to the current default level. One of: debug, info, warn, error, dpanic, panic, fatal, %s", defaultLogLevel, defaultLogLevel)),
125
},
126
NoLocal: true,
127
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
59
- args := req.Arguments
60
- subsystem, level := args[0], args[1]
128
+ var level, subsystem string
129
62
- if subsystem == logAllKeyword {
63
- subsystem = "*"
130
+ if len(req.Arguments) > 0 {
131
+ subsystem = req.Arguments[0]
132
+ if len(req.Arguments) > 1 {
133
+ level = req.Arguments[1]
134
+ }
135
+
136
+ // Normalize aliases to the canonical "*" form
137
+ if subsystem == allLogSubsystems || subsystem == allLogSubsystemsAlias {
138
+ subsystem = "*"
139
+ }
140
}
141
66
- if err := logging.SetLogLevel(subsystem, level); err != nil {
67
- return err
142
+ // If a level is specified, then set the log level.
143
+ if level != "" {
144
+ if level == defaultLogLevel {
145
+ level = logging.DefaultLevel().String()
146
+ }
147
+
148
+ if err := logging.SetLogLevel(subsystem, level); err != nil {
149
+ return err
150
+ }
151
+
152
+ s := fmt.Sprintf("Changed log level of '%s' to '%s'\n", subsystem, level)
153
+ log.Info(s)
154
+
155
+ return cmds.EmitOnce(res, &logLevelOutput{Message: s})
156
}
157
70
- s := fmt.Sprintf("Changed log level of '%s' to '%s'\n", subsystem, level)
71
- log.Info(s)
158
+ // Get the level for the requested subsystem.
159
+ switch subsystem {
160
+ case noSubsystemSpecified:
161
+ // Return the default log level
162
+ levelMap := map[string]string{logging.DefaultName: logging.DefaultLevel().String()}
163
+ return cmds.EmitOnce(res, &logLevelOutput{Levels: levelMap})
164
+ case allLogSubsystems, allLogSubsystemsAlias:
165
+ // Return levels for all subsystems (default behavior)
166
+ levels := logging.SubsystemLevelNames()
167
+
168
+ // Replace default subsystem key with defaultSubsystemKey.
169
+ levels[defaultSubsystemKey] = levels[logging.DefaultName]
170
+ delete(levels, logging.DefaultName)
171
+ return cmds.EmitOnce(res, &logLevelOutput{Levels: levels})
172
+ default:
173
+ // Return level for a specific subsystem.
174
+ level, err := logging.SubsystemLevelName(subsystem)
175
+ if err != nil {
176
+ return err
177
+ }
178
+ levelMap := map[string]string{subsystem: level}
179
+ return cmds.EmitOnce(res, &logLevelOutput{Levels: levelMap})
180
+ }
181
73
- return cmds.EmitOnce(res, &MessageOutput{s})
182
},
183
Encoders: cmds.EncoderMap{
76
- cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *MessageOutput) error {
77
- fmt.Fprint(w, out.Message)
184
+ cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *logLevelOutput) error {
185
+ if out.Message != "" {
186
+ fmt.Fprint(w, out.Message)
187
+ return nil
188
+ }
189
+
190
+ // Check if this is an RPC call by looking for the encoding option
191
+ encoding, _ := req.Options["encoding"].(string)
192
+ isRPC := encoding == "json"
193
+
194
+ // Determine whether to show subsystem names in output.
195
+ // Show subsystem names when:
196
+ // 1. It's an RPC call (needs JSON structure with named fields)
197
+ // 2. Multiple subsystems are displayed (for clarity when showing many levels)
198
+ showNames := isRPC || len(out.Levels) > 1
199
+
200
+ levelNames := make([]string, 0, len(out.Levels))
201
+ for subsystem, level := range out.Levels {
202
+ if showNames {
203
+ // Show subsystem name when it's RPC or when showing multiple subsystems
204
+ levelNames = append(levelNames, fmt.Sprintf("%s: %s", subsystem, level))
205
+ } else {
206
+ // For CLI calls with single subsystem, only show the level
207
+ levelNames = append(levelNames, level)
208
+ }
209
+ }
210
+ slices.Sort(levelNames)
211
+ for _, ln := range levelNames {
212
+ fmt.Fprintln(w, ln)
213
+ }
214
return nil
215
}),
216
},
81
- Type: MessageOutput{},
217
+ Type: logLevelOutput{},
218
}
219
220
var logLsCmd = &cmds.Command{
@@ -103,12 +239,10 @@ subsystems of a running daemon.
239
Type: stringList{},
240
}
241
106
-const logLevelOption = "log-level"
107
-
242
var logTailCmd = &cmds.Command{
243
Status: cmds.Experimental,
244
Helptext: cmds.HelpText{
111
- Tagline: "Read and outpt log messages.",
245
+ Tagline: "Read and output log messages.",
246
ShortDescription: `
247
Outputs log messages as they are generated.
248
@@ -130,7 +264,7 @@ This will only return 'info' logs from bitswap and skip 'debug'.
264
var pipeReader *logging.PipeReader
265
logLevelString, _ := req.Options[logLevelOption].(string)
266
if logLevelString != "" {
133
- logLevel, err := logging.LevelFromString(logLevelString)
267
+ logLevel, err := logging.Parse(logLevelString)
268
if err != nil {
269
return fmt.Errorf("setting log level %s: %w", logLevelString, err)
270
}
docs/changelogs/v0.37.md
+22
-1
@@ -11,7 +11,8 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
11
- [Overview](#overview)
12
- [🔦 Highlights](#-highlights)
13
- [Clear provide queue when reprovide strategy changes](#clear-provide-queue-when-reprovide-strategy-changes)
14
- - [Named pins in `ipfs add` command](#-named-pins-in-ipfs-add-command)
14
+ - [🪵 Revamped `ipfs log level` command](#-revamped-ipfs-log-level-command)
15
+ - [📌 Named pins in `ipfs add` command](#-named-pins-in-ipfs-add-command)
16
- [⚙️ `Reprovider.Strategy` is now consistently respected](#-reprovider-strategy-is-now-consistently-respected)
17
- [Removed unnecessary dependencies](#removed-unnecessary-dependencies)
18
- [Deprecated `ipfs stats reprovide`](#deprecated-ipfs-stats-reprovide)
@@ -34,6 +35,26 @@ A new `ipfs provide clear` command also allows manual queue clearing for debuggi
35
> [!NOTE]
36
> Upgrading to Kubo 0.37 will automatically clear any preexisting provide queue. The next time `Reprovider.Interval` hits, `Reprovider.Strategy` will be executed on a clean slate, ensuring consistent behavior with your current configuration.
37
38
+#### 🪵 Revamped `ipfs log level` command
39
+
40
+The `ipfs log level` command has been completely revamped to support both getting and setting log levels with a unified interface.
41
+
42
+**New: Getting log levels**
43
+
44
+- `ipfs log level` - Shows default level only
45
+- `ipfs log level all` - Shows log level for every subsystem, including default level
46
+- `ipfs log level foo` - Shows log level for a specific subsystem only
47
+- Kubo RPC API: `POST /api/v0/log/level?arg=<subsystem>`
48
+
49
+**Enhanced: Setting log levels**
50
+
51
+- `ipfs log level foo debug` - Sets "foo" subsystem to "debug" level
52
+- `ipfs log level all info` - Sets all subsystems to "info" level (convenient, no escaping)
53
+- `ipfs log level '*' info` - Equivalent to above but requires shell escaping
54
+- `ipfs log level foo default` - Sets "foo" subsystem to current default level
55
+
56
+The command now provides full visibility into your current logging configuration while maintaining full backward compatibility. Both `all` and `*` work for specifying all subsystems, with `all` being more convenient since it doesn't require shell escaping.
57
+
58
#### 🧷 Named pins in `ipfs add` command
59
60
Added `--pin-name` flag to `ipfs add` for assigning names to pins.
docs/examples/kubo-as-a-library/go.mod
+1
-1
@@ -90,7 +90,7 @@ require (
90
github.com/ipfs/go-ipld-format v0.6.2 // indirect
91
github.com/ipfs/go-ipld-git v0.1.1 // indirect
92
github.com/ipfs/go-ipld-legacy v0.2.2 // indirect
93
- github.com/ipfs/go-log/v2 v2.6.0 // indirect
93
+ github.com/ipfs/go-log/v2 v2.8.0 // indirect
94
github.com/ipfs/go-metrics-interface v0.3.0 // indirect
95
github.com/ipfs/go-peertaskqueue v0.8.2 // indirect
96
github.com/ipfs/go-unixfsnode v1.10.1 // indirect
docs/examples/kubo-as-a-library/go.sum
+2
-2
@@ -345,8 +345,8 @@ github.com/ipfs/go-ipld-legacy v0.2.2/go.mod h1:hhkj+b3kG9b2BcUNw8IFYAsfeNo8E3U7
345
github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9leM=
346
github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8=
347
github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo=
348
-github.com/ipfs/go-log/v2 v2.6.0 h1:2Nu1KKQQ2ayonKp4MPo6pXCjqw1ULc9iohRqWV5EYqg=
349
-github.com/ipfs/go-log/v2 v2.6.0/go.mod h1:p+Efr3qaY5YXpx9TX7MoLCSEZX5boSWj9wh86P5HJa8=
348
+github.com/ipfs/go-log/v2 v2.8.0 h1:SptNTPJQV3s5EF4FdrTu/yVdOKfGbDgn1EBZx4til2o=
349
+github.com/ipfs/go-log/v2 v2.8.0/go.mod h1:2LEEhdv8BGubPeSFTyzbqhCqrwqxCbuTNTLWqgNAipo=
350
github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6q/JR9V40TU=
351
github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY=
352
github.com/ipfs/go-peertaskqueue v0.8.2 h1:PaHFRaVFdxQk1Qo3OKiHPYjmmusQy7gKQUaL8JDszAU=
go.mod
+1
-1
@@ -39,7 +39,7 @@ require (
39
github.com/ipfs/go-ipld-format v0.6.2
40
github.com/ipfs/go-ipld-git v0.1.1
41
github.com/ipfs/go-ipld-legacy v0.2.2
42
- github.com/ipfs/go-log/v2 v2.6.0
42
+ github.com/ipfs/go-log/v2 v2.8.0
43
github.com/ipfs/go-metrics-interface v0.3.0
44
github.com/ipfs/go-metrics-prometheus v0.1.0
45
github.com/ipfs/go-test v0.2.2
go.sum
+2
-2
@@ -414,8 +414,8 @@ github.com/ipfs/go-ipld-legacy v0.2.2/go.mod h1:hhkj+b3kG9b2BcUNw8IFYAsfeNo8E3U7
414
github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9leM=
415
github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8=
416
github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo=
417
-github.com/ipfs/go-log/v2 v2.6.0 h1:2Nu1KKQQ2ayonKp4MPo6pXCjqw1ULc9iohRqWV5EYqg=
418
-github.com/ipfs/go-log/v2 v2.6.0/go.mod h1:p+Efr3qaY5YXpx9TX7MoLCSEZX5boSWj9wh86P5HJa8=
417
+github.com/ipfs/go-log/v2 v2.8.0 h1:SptNTPJQV3s5EF4FdrTu/yVdOKfGbDgn1EBZx4til2o=
418
+github.com/ipfs/go-log/v2 v2.8.0/go.mod h1:2LEEhdv8BGubPeSFTyzbqhCqrwqxCbuTNTLWqgNAipo=
419
github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6q/JR9V40TU=
420
github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY=
421
github.com/ipfs/go-metrics-prometheus v0.1.0 h1:bApWOHkrH3VTBHzTHrZSfq4n4weOZDzZFxUXv+HyKcA=
test/cli/log_level_test.go
new
+663
@@ -0,0 +1,663 @@
1
+package cli
2
+
3
+import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "net/http"
7
+ "strings"
8
+ "testing"
9
+
10
+ "github.com/ipfs/kubo/test/cli/harness"
11
+ . "github.com/ipfs/kubo/test/cli/testutils"
12
+ "github.com/stretchr/testify/assert"
13
+ "github.com/stretchr/testify/require"
14
+)
15
+
16
+func TestLogLevel(t *testing.T) {
17
+
18
+ t.Run("CLI", func(t *testing.T) {
19
+ t.Run("level '*' shows all subsystems", func(t *testing.T) {
20
+ t.Parallel()
21
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
22
+ defer node.StopDaemon()
23
+
24
+ expectedSubsystems := getExpectedSubsystems(t, node)
25
+
26
+ res := node.IPFS("log", "level", "*")
27
+ assert.NoError(t, res.Err)
28
+ assert.Empty(t, res.Stderr.Lines())
29
+
30
+ actualSubsystems := parseCLIOutput(t, res.Stdout.String())
31
+
32
+ // Should show all subsystems plus the (default) entry
33
+ assert.GreaterOrEqual(t, len(actualSubsystems), len(expectedSubsystems))
34
+
35
+ validateAllSubsystemsPresentCLI(t, expectedSubsystems, actualSubsystems, "CLI output")
36
+
37
+ // Should have the (default) entry
38
+ _, hasDefault := actualSubsystems["(default)"]
39
+ assert.True(t, hasDefault, "Should have '(default)' entry")
40
+ })
41
+
42
+ t.Run("level 'all' shows all subsystems (alias for '*')", func(t *testing.T) {
43
+ t.Parallel()
44
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
45
+ defer node.StopDaemon()
46
+
47
+ expectedSubsystems := getExpectedSubsystems(t, node)
48
+
49
+ res := node.IPFS("log", "level", "all")
50
+ assert.NoError(t, res.Err)
51
+ assert.Empty(t, res.Stderr.Lines())
52
+
53
+ actualSubsystems := parseCLIOutput(t, res.Stdout.String())
54
+
55
+ // Should show all subsystems plus the (default) entry
56
+ assert.GreaterOrEqual(t, len(actualSubsystems), len(expectedSubsystems))
57
+
58
+ validateAllSubsystemsPresentCLI(t, expectedSubsystems, actualSubsystems, "CLI output")
59
+
60
+ // Should have the (default) entry
61
+ _, hasDefault := actualSubsystems["(default)"]
62
+ assert.True(t, hasDefault, "Should have '(default)' entry")
63
+ })
64
+
65
+ t.Run("get level for specific subsystem", func(t *testing.T) {
66
+ t.Parallel()
67
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
68
+ defer node.StopDaemon()
69
+
70
+ node.IPFS("log", "level", "core", "debug")
71
+ res := node.IPFS("log", "level", "core")
72
+ assert.NoError(t, res.Err)
73
+ assert.Empty(t, res.Stderr.Lines())
74
+
75
+ output := res.Stdout.String()
76
+ lines := SplitLines(output)
77
+
78
+ assert.Equal(t, 1, len(lines))
79
+
80
+ line := strings.TrimSpace(lines[0])
81
+ assert.Equal(t, "debug", line)
82
+ })
83
+
84
+ t.Run("get level with no args returns default level", func(t *testing.T) {
85
+ t.Parallel()
86
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
87
+ defer node.StopDaemon()
88
+
89
+ res1 := node.IPFS("log", "level", "*", "fatal")
90
+ assert.NoError(t, res1.Err)
91
+ assert.Empty(t, res1.Stderr.Lines())
92
+
93
+ res := node.IPFS("log", "level")
94
+ assert.NoError(t, res.Err)
95
+ assert.Equal(t, 0, len(res.Stderr.Lines()))
96
+
97
+ output := res.Stdout.String()
98
+ lines := SplitLines(output)
99
+
100
+ assert.Equal(t, 1, len(lines))
101
+
102
+ line := strings.TrimSpace(lines[0])
103
+ assert.Equal(t, "fatal", line)
104
+ })
105
+
106
+ t.Run("get level reflects runtime log level changes", func(t *testing.T) {
107
+ t.Parallel()
108
+ node := harness.NewT(t).NewNode().Init().StartDaemon("--offline")
109
+ defer node.StopDaemon()
110
+
111
+ node.IPFS("log", "level", "core", "debug")
112
+ res := node.IPFS("log", "level", "core")
113
+ assert.NoError(t, res.Err)
114
+
115
+ output := res.Stdout.String()
116
+ lines := SplitLines(output)
117
+
118
+ assert.Equal(t, 1, len(lines))
119
+
120
+ line := strings.TrimSpace(lines[0])
121
+ assert.Equal(t, "debug", line)
122
+ })
123
+
124
+ t.Run("get level with non-existent subsystem returns error", func(t *testing.T) {
125
+ t.Parallel()
126
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
127
+ defer node.StopDaemon()
128
+
129
+ res := node.RunIPFS("log", "level", "non-existent-subsystem")
130
+ assert.Error(t, res.Err)
131
+ assert.NotEqual(t, 0, len(res.Stderr.Lines()))
132
+ })
133
+
134
+ t.Run("set level to 'default' keyword", func(t *testing.T) {
135
+ t.Parallel()
136
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
137
+ defer node.StopDaemon()
138
+
139
+ // First set a specific subsystem to a different level
140
+ res1 := node.IPFS("log", "level", "core", "debug")
141
+ assert.NoError(t, res1.Err)
142
+ assert.Contains(t, res1.Stdout.String(), "Changed log level of 'core' to 'debug'")
143
+
144
+ // Verify it was set to debug
145
+ res2 := node.IPFS("log", "level", "core")
146
+ assert.NoError(t, res2.Err)
147
+ assert.Equal(t, "debug", strings.TrimSpace(res2.Stdout.String()))
148
+
149
+ // Get the current default level (should be 'error' since unchanged)
150
+ res3 := node.IPFS("log", "level")
151
+ assert.NoError(t, res3.Err)
152
+ defaultLevel := strings.TrimSpace(res3.Stdout.String())
153
+ assert.Equal(t, "error", defaultLevel, "Default level should be 'error' when unchanged")
154
+
155
+ // Now set the subsystem back to default
156
+ res4 := node.IPFS("log", "level", "core", "default")
157
+ assert.NoError(t, res4.Err)
158
+ assert.Contains(t, res4.Stdout.String(), "Changed log level of 'core' to")
159
+
160
+ // Verify it's now at the default level (should be 'error')
161
+ res5 := node.IPFS("log", "level", "core")
162
+ assert.NoError(t, res5.Err)
163
+ assert.Equal(t, "error", strings.TrimSpace(res5.Stdout.String()))
164
+ })
165
+
166
+ t.Run("set all subsystems with 'all' changes default (alias for '*')", func(t *testing.T) {
167
+ t.Parallel()
168
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
169
+ defer node.StopDaemon()
170
+
171
+ // Initial state - default should be 'error'
172
+ res := node.IPFS("log", "level")
173
+ assert.NoError(t, res.Err)
174
+ assert.Equal(t, "error", strings.TrimSpace(res.Stdout.String()))
175
+
176
+ // Set one subsystem to a different level
177
+ res = node.IPFS("log", "level", "core", "debug")
178
+ assert.NoError(t, res.Err)
179
+
180
+ // Default should still be 'error'
181
+ res = node.IPFS("log", "level")
182
+ assert.NoError(t, res.Err)
183
+ assert.Equal(t, "error", strings.TrimSpace(res.Stdout.String()))
184
+
185
+ // Now use 'all' to set everything to 'info'
186
+ res = node.IPFS("log", "level", "all", "info")
187
+ assert.NoError(t, res.Err)
188
+ assert.Contains(t, res.Stdout.String(), "Changed log level of '*' to 'info'")
189
+
190
+ // Default should now be 'info'
191
+ res = node.IPFS("log", "level")
192
+ assert.NoError(t, res.Err)
193
+ assert.Equal(t, "info", strings.TrimSpace(res.Stdout.String()))
194
+
195
+ // Core should also be 'info' (overwritten by 'all')
196
+ res = node.IPFS("log", "level", "core")
197
+ assert.NoError(t, res.Err)
198
+ assert.Equal(t, "info", strings.TrimSpace(res.Stdout.String()))
199
+
200
+ // Any other subsystem should also be 'info'
201
+ res = node.IPFS("log", "level", "dht")
202
+ assert.NoError(t, res.Err)
203
+ assert.Equal(t, "info", strings.TrimSpace(res.Stdout.String()))
204
+ })
205
+
206
+ t.Run("set all subsystems with '*' changes default", func(t *testing.T) {
207
+ t.Parallel()
208
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
209
+ defer node.StopDaemon()
210
+
211
+ // Initial state - default should be 'error'
212
+ res := node.IPFS("log", "level")
213
+ assert.NoError(t, res.Err)
214
+ assert.Equal(t, "error", strings.TrimSpace(res.Stdout.String()))
215
+
216
+ // Set one subsystem to a different level
217
+ res = node.IPFS("log", "level", "core", "debug")
218
+ assert.NoError(t, res.Err)
219
+
220
+ // Default should still be 'error'
221
+ res = node.IPFS("log", "level")
222
+ assert.NoError(t, res.Err)
223
+ assert.Equal(t, "error", strings.TrimSpace(res.Stdout.String()))
224
+
225
+ // Now use '*' to set everything to 'info'
226
+ res = node.IPFS("log", "level", "*", "info")
227
+ assert.NoError(t, res.Err)
228
+ assert.Contains(t, res.Stdout.String(), "Changed log level of '*' to 'info'")
229
+
230
+ // Default should now be 'info'
231
+ res = node.IPFS("log", "level")
232
+ assert.NoError(t, res.Err)
233
+ assert.Equal(t, "info", strings.TrimSpace(res.Stdout.String()))
234
+
235
+ // Core should also be 'info' (overwritten by '*')
236
+ res = node.IPFS("log", "level", "core")
237
+ assert.NoError(t, res.Err)
238
+ assert.Equal(t, "info", strings.TrimSpace(res.Stdout.String()))
239
+
240
+ // Any other subsystem should also be 'info'
241
+ res = node.IPFS("log", "level", "dht")
242
+ assert.NoError(t, res.Err)
243
+ assert.Equal(t, "info", strings.TrimSpace(res.Stdout.String()))
244
+ })
245
+
246
+ t.Run("'all' in get mode shows (default) entry (alias for '*')", func(t *testing.T) {
247
+ t.Parallel()
248
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
249
+ defer node.StopDaemon()
250
+
251
+ // Get all levels with 'all'
252
+ res := node.IPFS("log", "level", "all")
253
+ assert.NoError(t, res.Err)
254
+
255
+ output := res.Stdout.String()
256
+
257
+ // Should contain "(default): error" entry
258
+ assert.Contains(t, output, "(default): error", "Should show default level with (default) key")
259
+
260
+ // Should also contain various subsystems
261
+ assert.Contains(t, output, "core: error")
262
+ assert.Contains(t, output, "dht: error")
263
+ })
264
+
265
+ t.Run("'*' in get mode shows (default) entry", func(t *testing.T) {
266
+ t.Parallel()
267
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
268
+ defer node.StopDaemon()
269
+
270
+ // Get all levels with '*'
271
+ res := node.IPFS("log", "level", "*")
272
+ assert.NoError(t, res.Err)
273
+
274
+ output := res.Stdout.String()
275
+
276
+ // Should contain "(default): error" entry
277
+ assert.Contains(t, output, "(default): error", "Should show default level with (default) key")
278
+
279
+ // Should also contain various subsystems
280
+ assert.Contains(t, output, "core: error")
281
+ assert.Contains(t, output, "dht: error")
282
+ })
283
+
284
+ t.Run("set all subsystems to 'default' using 'all' (alias for '*')", func(t *testing.T) {
285
+ t.Parallel()
286
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
287
+ defer node.StopDaemon()
288
+
289
+ // Get the original default level (just for reference, it should be "error")
290
+ res0 := node.IPFS("log", "level")
291
+ assert.NoError(t, res0.Err)
292
+ assert.Equal(t, "error", strings.TrimSpace(res0.Stdout.String()))
293
+
294
+ // First set all subsystems to debug using 'all'
295
+ res1 := node.IPFS("log", "level", "all", "debug")
296
+ assert.NoError(t, res1.Err)
297
+ assert.Contains(t, res1.Stdout.String(), "Changed log level of '*' to 'debug'")
298
+
299
+ // Verify a specific subsystem is at debug
300
+ res2 := node.IPFS("log", "level", "core")
301
+ assert.NoError(t, res2.Err)
302
+ assert.Equal(t, "debug", strings.TrimSpace(res2.Stdout.String()))
303
+
304
+ // Verify the default level is now debug
305
+ res3 := node.IPFS("log", "level")
306
+ assert.NoError(t, res3.Err)
307
+ assert.Equal(t, "debug", strings.TrimSpace(res3.Stdout.String()))
308
+
309
+ // Now set all subsystems back to default (which is now "debug") using 'all'
310
+ res4 := node.IPFS("log", "level", "all", "default")
311
+ assert.NoError(t, res4.Err)
312
+ assert.Contains(t, res4.Stdout.String(), "Changed log level of '*' to")
313
+
314
+ // The subsystem should still be at debug (because that's what default is now)
315
+ res5 := node.IPFS("log", "level", "core")
316
+ assert.NoError(t, res5.Err)
317
+ assert.Equal(t, "debug", strings.TrimSpace(res5.Stdout.String()))
318
+
319
+ // The behavior is correct: "default" uses the current default level,
320
+ // which was changed to "debug" when we set "all" to "debug"
321
+ })
322
+
323
+ t.Run("set all subsystems to 'default' keyword", func(t *testing.T) {
324
+ t.Parallel()
325
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
326
+ defer node.StopDaemon()
327
+
328
+ // Get the original default level (just for reference, it should be "error")
329
+ res0 := node.IPFS("log", "level")
330
+ assert.NoError(t, res0.Err)
331
+ // originalDefault := strings.TrimSpace(res0.Stdout.String())
332
+ assert.Equal(t, "error", strings.TrimSpace(res0.Stdout.String()))
333
+
334
+ // First set all subsystems to debug
335
+ res1 := node.IPFS("log", "level", "*", "debug")
336
+ assert.NoError(t, res1.Err)
337
+ assert.Contains(t, res1.Stdout.String(), "Changed log level of '*' to 'debug'")
338
+
339
+ // Verify a specific subsystem is at debug
340
+ res2 := node.IPFS("log", "level", "core")
341
+ assert.NoError(t, res2.Err)
342
+ assert.Equal(t, "debug", strings.TrimSpace(res2.Stdout.String()))
343
+
344
+ // Verify the default level is now debug
345
+ res3 := node.IPFS("log", "level")
346
+ assert.NoError(t, res3.Err)
347
+ assert.Equal(t, "debug", strings.TrimSpace(res3.Stdout.String()))
348
+
349
+ // Now set all subsystems back to default (which is now "debug")
350
+ res4 := node.IPFS("log", "level", "*", "default")
351
+ assert.NoError(t, res4.Err)
352
+ assert.Contains(t, res4.Stdout.String(), "Changed log level of '*' to")
353
+
354
+ // The subsystem should still be at debug (because that's what default is now)
355
+ res5 := node.IPFS("log", "level", "core")
356
+ assert.NoError(t, res5.Err)
357
+ assert.Equal(t, "debug", strings.TrimSpace(res5.Stdout.String()))
358
+
359
+ // The behavior is correct: "default" uses the current default level,
360
+ // which was changed to "debug" when we set "*" to "debug"
361
+ })
362
+
363
+ t.Run("shell escaping variants for '*' wildcard", func(t *testing.T) {
364
+ t.Parallel()
365
+ h := harness.NewT(t)
366
+ node := h.NewNode().Init().StartDaemon()
367
+ defer node.StopDaemon()
368
+
369
+ // Test different shell escaping methods work for '*'
370
+ // This tests the behavior documented in help text: '*' or "*" or \*
371
+
372
+ // Test 1: Single quotes '*' (should work)
373
+ cmd1 := fmt.Sprintf("IPFS_PATH='%s' %s --api='%s' log level '*' info",
374
+ node.Dir, node.IPFSBin, node.APIAddr())
375
+ res1 := h.Sh(cmd1)
376
+ assert.NoError(t, res1.Err)
377
+ assert.Contains(t, res1.Stdout.String(), "Changed log level of '*' to 'info'")
378
+
379
+ // Test 2: Double quotes "*" (should work)
380
+ cmd2 := fmt.Sprintf("IPFS_PATH='%s' %s --api='%s' log level \"*\" debug",
381
+ node.Dir, node.IPFSBin, node.APIAddr())
382
+ res2 := h.Sh(cmd2)
383
+ assert.NoError(t, res2.Err)
384
+ assert.Contains(t, res2.Stdout.String(), "Changed log level of '*' to 'debug'")
385
+
386
+ // Test 3: Backslash escape \* (should work)
387
+ cmd3 := fmt.Sprintf("IPFS_PATH='%s' %s --api='%s' log level \\* warn",
388
+ node.Dir, node.IPFSBin, node.APIAddr())
389
+ res3 := h.Sh(cmd3)
390
+ assert.NoError(t, res3.Err)
391
+ assert.Contains(t, res3.Stdout.String(), "Changed log level of '*' to 'warn'")
392
+
393
+ // Test 4: Verify the final state - should show 'warn' as default
394
+ res4 := node.IPFS("log", "level")
395
+ assert.NoError(t, res4.Err)
396
+ assert.Equal(t, "warn", strings.TrimSpace(res4.Stdout.String()))
397
+
398
+ // Test 5: Get all levels using escaped '*' to verify it shows all subsystems
399
+ cmd5 := fmt.Sprintf("IPFS_PATH='%s' %s --api='%s' log level \\*",
400
+ node.Dir, node.IPFSBin, node.APIAddr())
401
+ res5 := h.Sh(cmd5)
402
+ assert.NoError(t, res5.Err)
403
+ output := res5.Stdout.String()
404
+ assert.Contains(t, output, "(default): warn", "Should show updated default level")
405
+ assert.Contains(t, output, "core: warn", "Should show core subsystem at warn level")
406
+ })
407
+ })
408
+
409
+ t.Run("HTTP RPC", func(t *testing.T) {
410
+ t.Run("get default level returns JSON", func(t *testing.T) {
411
+ t.Parallel()
412
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
413
+ defer node.StopDaemon()
414
+
415
+ // Make HTTP request to get default log level
416
+ resp, err := http.Post(node.APIURL()+"/api/v0/log/level", "", nil)
417
+ require.NoError(t, err)
418
+ defer resp.Body.Close()
419
+
420
+ // Parse JSON response
421
+ var result map[string]interface{}
422
+ err = json.NewDecoder(resp.Body).Decode(&result)
423
+ require.NoError(t, err)
424
+
425
+ // Check that we have the Levels field
426
+ levels, ok := result["Levels"].(map[string]interface{})
427
+ require.True(t, ok, "Response should have 'Levels' field")
428
+
429
+ // Should have exactly one entry for the default level
430
+ assert.Equal(t, 1, len(levels))
431
+
432
+ // The default level should be present
433
+ defaultLevel, ok := levels[""]
434
+ require.True(t, ok, "Should have empty string key for default level")
435
+ assert.Equal(t, "error", defaultLevel, "Default level should be 'error'")
436
+ })
437
+
438
+ t.Run("get all levels using 'all' returns JSON (alias for '*')", func(t *testing.T) {
439
+ t.Parallel()
440
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
441
+ defer node.StopDaemon()
442
+
443
+ expectedSubsystems := getExpectedSubsystems(t, node)
444
+
445
+ // Make HTTP request to get all log levels using 'all'
446
+ resp, err := http.Post(node.APIURL()+"/api/v0/log/level?arg=all", "", nil)
447
+ require.NoError(t, err)
448
+ defer resp.Body.Close()
449
+
450
+ levels := parseHTTPResponse(t, resp)
451
+ validateAllSubsystemsPresent(t, expectedSubsystems, levels, "JSON response")
452
+
453
+ // Should have the (default) entry
454
+ defaultLevel, ok := levels["(default)"]
455
+ require.True(t, ok, "Should have '(default)' key")
456
+ assert.Equal(t, "error", defaultLevel, "Default level should be 'error'")
457
+ })
458
+
459
+ t.Run("get all levels returns JSON", func(t *testing.T) {
460
+ t.Parallel()
461
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
462
+ defer node.StopDaemon()
463
+
464
+ expectedSubsystems := getExpectedSubsystems(t, node)
465
+
466
+ // Make HTTP request to get all log levels
467
+ resp, err := http.Post(node.APIURL()+"/api/v0/log/level?arg=*", "", nil)
468
+ require.NoError(t, err)
469
+ defer resp.Body.Close()
470
+
471
+ levels := parseHTTPResponse(t, resp)
472
+ validateAllSubsystemsPresent(t, expectedSubsystems, levels, "JSON response")
473
+
474
+ // Should have the (default) entry
475
+ defaultLevel, ok := levels["(default)"]
476
+ require.True(t, ok, "Should have '(default)' key")
477
+ assert.Equal(t, "error", defaultLevel, "Default level should be 'error'")
478
+ })
479
+
480
+ t.Run("get specific subsystem level returns JSON", func(t *testing.T) {
481
+ t.Parallel()
482
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
483
+ defer node.StopDaemon()
484
+
485
+ // First set a specific level for a subsystem
486
+ resp, err := http.Post(node.APIURL()+"/api/v0/log/level?arg=core&arg=debug", "", nil)
487
+ require.NoError(t, err)
488
+ resp.Body.Close()
489
+
490
+ // Now get the level for that subsystem
491
+ resp, err = http.Post(node.APIURL()+"/api/v0/log/level?arg=core", "", nil)
492
+ require.NoError(t, err)
493
+ defer resp.Body.Close()
494
+
495
+ // Parse JSON response
496
+ var result map[string]interface{}
497
+ err = json.NewDecoder(resp.Body).Decode(&result)
498
+ require.NoError(t, err)
499
+
500
+ // Check that we have the Levels field
501
+ levels, ok := result["Levels"].(map[string]interface{})
502
+ require.True(t, ok, "Response should have 'Levels' field")
503
+
504
+ // Should have exactly one entry
505
+ assert.Equal(t, 1, len(levels))
506
+
507
+ // Check the level for 'core' subsystem
508
+ coreLevel, ok := levels["core"]
509
+ require.True(t, ok, "Should have 'core' key")
510
+ assert.Equal(t, "debug", coreLevel, "Core level should be 'debug'")
511
+ })
512
+
513
+ t.Run("set level using 'all' returns JSON message (alias for '*')", func(t *testing.T) {
514
+ t.Parallel()
515
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
516
+ defer node.StopDaemon()
517
+
518
+ // Set a log level using 'all'
519
+ resp, err := http.Post(node.APIURL()+"/api/v0/log/level?arg=all&arg=info", "", nil)
520
+ require.NoError(t, err)
521
+ defer resp.Body.Close()
522
+
523
+ // Parse JSON response
524
+ var result map[string]interface{}
525
+ err = json.NewDecoder(resp.Body).Decode(&result)
526
+ require.NoError(t, err)
527
+
528
+ // Check that we have the Message field
529
+ message, ok := result["Message"].(string)
530
+ require.True(t, ok, "Response should have 'Message' field")
531
+
532
+ // Check the message content (should show '*' in message even when 'all' was used)
533
+ assert.Contains(t, message, "Changed log level of '*' to 'info'")
534
+ })
535
+
536
+ t.Run("set level returns JSON message", func(t *testing.T) {
537
+ t.Parallel()
538
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
539
+ defer node.StopDaemon()
540
+
541
+ // Set a log level
542
+ resp, err := http.Post(node.APIURL()+"/api/v0/log/level?arg=core&arg=info", "", nil)
543
+ require.NoError(t, err)
544
+ defer resp.Body.Close()
545
+
546
+ // Parse JSON response
547
+ var result map[string]interface{}
548
+ err = json.NewDecoder(resp.Body).Decode(&result)
549
+ require.NoError(t, err)
550
+
551
+ // Check that we have the Message field
552
+ message, ok := result["Message"].(string)
553
+ require.True(t, ok, "Response should have 'Message' field")
554
+
555
+ // Check the message content
556
+ assert.Contains(t, message, "Changed log level of 'core' to 'info'")
557
+ })
558
+
559
+ t.Run("set level to 'default' keyword", func(t *testing.T) {
560
+ t.Parallel()
561
+ node := harness.NewT(t).NewNode().Init().StartDaemon()
562
+ defer node.StopDaemon()
563
+
564
+ // First set a subsystem to debug
565
+ resp, err := http.Post(node.APIURL()+"/api/v0/log/level?arg=core&arg=debug", "", nil)
566
+ require.NoError(t, err)
567
+ resp.Body.Close()
568
+
569
+ // Now set it back to default
570
+ resp, err = http.Post(node.APIURL()+"/api/v0/log/level?arg=core&arg=default", "", nil)
571
+ require.NoError(t, err)
572
+ defer resp.Body.Close()
573
+
574
+ // Parse JSON response
575
+ var result map[string]interface{}
576
+ err = json.NewDecoder(resp.Body).Decode(&result)
577
+ require.NoError(t, err)
578
+
579
+ // Check that we have the Message field
580
+ message, ok := result["Message"].(string)
581
+ require.True(t, ok, "Response should have 'Message' field")
582
+
583
+ // The message should indicate the change
584
+ assert.True(t, strings.Contains(message, "Changed log level of 'core' to"),
585
+ "Message should indicate level change")
586
+
587
+ // Verify the level is back to error (default)
588
+ resp, err = http.Post(node.APIURL()+"/api/v0/log/level?arg=core", "", nil)
589
+ require.NoError(t, err)
590
+ defer resp.Body.Close()
591
+
592
+ var getResult map[string]interface{}
593
+ err = json.NewDecoder(resp.Body).Decode(&getResult)
594
+ require.NoError(t, err)
595
+
596
+ levels, _ := getResult["Levels"].(map[string]interface{})
597
+ coreLevel, _ := levels["core"].(string)
598
+ assert.Equal(t, "error", coreLevel, "Core level should be back to 'error' (default)")
599
+ })
600
+ })
601
+
602
+}
603
+
604
+func getExpectedSubsystems(t *testing.T, node *harness.Node) []string {
605
+ t.Helper()
606
+ lsRes := node.IPFS("log", "ls")
607
+ require.NoError(t, lsRes.Err)
608
+ expectedSubsystems := SplitLines(lsRes.Stdout.String())
609
+ assert.Greater(t, len(expectedSubsystems), 10, "Should have many subsystems")
610
+ return expectedSubsystems
611
+}
612
+
613
+func parseCLIOutput(t *testing.T, output string) map[string]string {
614
+ t.Helper()
615
+ lines := SplitLines(output)
616
+ actualSubsystems := make(map[string]string)
617
+ for _, line := range lines {
618
+ if strings.TrimSpace(line) == "" {
619
+ continue
620
+ }
621
+ parts := strings.Split(line, ": ")
622
+ assert.Equal(t, 2, len(parts), "Line should have format 'subsystem: level', got: %s", line)
623
+ assert.NotEmpty(t, parts[0], "Subsystem should not be empty")
624
+ assert.NotEmpty(t, parts[1], "Level should not be empty")
625
+ actualSubsystems[parts[0]] = parts[1]
626
+ }
627
+ return actualSubsystems
628
+}
629
+
630
+func parseHTTPResponse(t *testing.T, resp *http.Response) map[string]interface{} {
631
+ t.Helper()
632
+ var result map[string]interface{}
633
+ err := json.NewDecoder(resp.Body).Decode(&result)
634
+ require.NoError(t, err)
635
+ levels, ok := result["Levels"].(map[string]interface{})
636
+ require.True(t, ok, "Response should have 'Levels' field")
637
+ assert.Greater(t, len(levels), 10, "Should have many subsystems")
638
+ return levels
639
+}
640
+
641
+func validateAllSubsystemsPresent(t *testing.T, expectedSubsystems []string, actualLevels map[string]interface{}, context string) {
642
+ t.Helper()
643
+ for _, expectedSub := range expectedSubsystems {
644
+ expectedSub = strings.TrimSpace(expectedSub)
645
+ if expectedSub == "" {
646
+ continue
647
+ }
648
+ _, found := actualLevels[expectedSub]
649
+ assert.True(t, found, "Expected subsystem '%s' should be present in %s", expectedSub, context)
650
+ }
651
+}
652
+
653
+func validateAllSubsystemsPresentCLI(t *testing.T, expectedSubsystems []string, actualLevels map[string]string, context string) {
654
+ t.Helper()
655
+ for _, expectedSub := range expectedSubsystems {
656
+ expectedSub = strings.TrimSpace(expectedSub)
657
+ if expectedSub == "" {
658
+ continue
659
+ }
660
+ _, found := actualLevels[expectedSub]
661
+ assert.True(t, found, "Expected subsystem '%s' should be present in %s", expectedSub, context)
662
+ }
663
+}
test/dependencies/go.mod
+1
-1
@@ -8,7 +8,7 @@ require (
8
github.com/Kubuxu/gocovmerge v0.0.0-20161216165753-7ecaa51963cd
9
github.com/golangci/golangci-lint v1.60.2
10
github.com/ipfs/go-cidutil v0.1.0
11
- github.com/ipfs/go-log/v2 v2.6.0
11
+ github.com/ipfs/go-log/v2 v2.8.0
12
github.com/ipfs/go-test v0.2.2
13
github.com/ipfs/hang-fds v0.1.0
14
github.com/ipfs/iptb v1.4.1
test/dependencies/go.sum
+2
-2
@@ -343,8 +343,8 @@ github.com/ipfs/go-ipld-format v0.6.2 h1:bPZQ+A05ol0b3lsJSl0bLvwbuQ+HQbSsdGTy4xt
343
github.com/ipfs/go-ipld-format v0.6.2/go.mod h1:nni2xFdHKx5lxvXJ6brt/pndtGxKAE+FPR1rg4jTkyk=
344
github.com/ipfs/go-ipld-legacy v0.2.2 h1:DThbqCPVLpWBcGtU23KDLiY2YRZZnTkXQyfz8aOfBkQ=
345
github.com/ipfs/go-ipld-legacy v0.2.2/go.mod h1:hhkj+b3kG9b2BcUNw8IFYAsfeNo8E3U7eYlWeAOPyDU=
346
-github.com/ipfs/go-log/v2 v2.6.0 h1:2Nu1KKQQ2ayonKp4MPo6pXCjqw1ULc9iohRqWV5EYqg=
347
-github.com/ipfs/go-log/v2 v2.6.0/go.mod h1:p+Efr3qaY5YXpx9TX7MoLCSEZX5boSWj9wh86P5HJa8=
346
+github.com/ipfs/go-log/v2 v2.8.0 h1:SptNTPJQV3s5EF4FdrTu/yVdOKfGbDgn1EBZx4til2o=
347
+github.com/ipfs/go-log/v2 v2.8.0/go.mod h1:2LEEhdv8BGubPeSFTyzbqhCqrwqxCbuTNTLWqgNAipo=
348
github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6q/JR9V40TU=
349
github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY=
350
github.com/ipfs/go-peertaskqueue v0.8.2 h1:PaHFRaVFdxQk1Qo3OKiHPYjmmusQy7gKQUaL8JDszAU=