master
go 282 lines 9.09 KB
Raw
1 package commands
2
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
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{
40 Tagline: "Interact with the daemon log output.",
41 ShortDescription: `
42 'ipfs log' contains utility commands to affect or read the logging
43 output of a running daemon.
44
45 There are also two environmental variables that direct the logging
46 system (not just for the daemon logs, but all commands):
47 GOLOG_LOG_LEVEL - sets the level of verbosity of the logging.
48 One of: debug, info, warn, error, dpanic, panic, fatal
49 GOLOG_LOG_FMT - sets formatting of the log output.
50 One of: color, nocolor, json
51 `,
52 },
53
54 Subcommands: map[string]*cmds.Command{
55 "level": logLevelCmd,
56 "ls": logLsCmd,
57 "tail": logTailCmd,
58 },
59 }
60
61 var logLevelCmd = &cmds.Command{
62 Helptext: cmds.HelpText{
63 Tagline: "Change or get the logging level.",
64 ShortDescription: `
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{
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 {
128 var level, subsystem string
129
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
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
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
182 },
183 Encoders: cmds.EncoderMap{
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 },
217 Type: logLevelOutput{},
218 }
219
220 var logLsCmd = &cmds.Command{
221 Helptext: cmds.HelpText{
222 Tagline: "List the logging subsystems.",
223 ShortDescription: `
224 'ipfs log ls' is a utility command used to list the logging
225 subsystems of a running daemon.
226 `,
227 },
228 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
229 return cmds.EmitOnce(res, &stringList{logging.GetSubsystems()})
230 },
231 Encoders: cmds.EncoderMap{
232 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, list *stringList) error {
233 for _, s := range list.Strings {
234 fmt.Fprintln(w, s)
235 }
236 return nil
237 }),
238 },
239 Type: stringList{},
240 }
241
242 var logTailCmd = &cmds.Command{
243 Status: cmds.Experimental,
244 Helptext: cmds.HelpText{
245 Tagline: "Read and output log messages.",
246 ShortDescription: `
247 Outputs log messages as they are generated.
248
249 NOTE: --log-level requires the server to be logging at least at this level
250
251 Example:
252
253 GOLOG_LOG_LEVEL="error,bitswap=debug" ipfs daemon
254 ipfs log tail --log-level info
255
256 This will only return 'info' logs from bitswap and skip 'debug'.
257 `,
258 },
259
260 Options: []cmds.Option{
261 cmds.StringOption(logLevelOption, "Log level to listen to.").WithDefault(""),
262 },
263 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
264 var pipeReader *logging.PipeReader
265 logLevelString, _ := req.Options[logLevelOption].(string)
266 if logLevelString != "" {
267 logLevel, err := logging.Parse(logLevelString)
268 if err != nil {
269 return fmt.Errorf("setting log level %s: %w", logLevelString, err)
270 }
271 pipeReader = logging.NewPipeReader(logging.PipeLevel(logLevel))
272 } else {
273 pipeReader = logging.NewPipeReader()
274 }
275
276 go func() {
277 <-req.Context.Done()
278 pipeReader.Close()
279 }()
280 return res.Emit(pipeReader)
281 },
282 }