| 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 | } |