github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/commands/debuglog.go (about) 1 // Copyright 2013, 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package commands 5 6 import ( 7 "fmt" 8 "os" 9 "strings" 10 "time" 11 12 "github.com/juju/ansiterm" 13 "github.com/juju/cmd" 14 "github.com/juju/errors" 15 "github.com/juju/gnuflag" 16 "github.com/juju/juju/jujuclient" 17 "github.com/juju/loggo" 18 "github.com/juju/loggo/loggocolor" 19 "github.com/mattn/go-isatty" 20 "gopkg.in/juju/names.v2" 21 22 "github.com/juju/juju/api/common" 23 jujucmd "github.com/juju/juju/cmd" 24 "github.com/juju/juju/cmd/modelcmd" 25 ) 26 27 // defaultLineCount is the default number of lines to 28 // display, from the end of the consolidated log. 29 const defaultLineCount = 10 30 31 var usageDebugLogSummary = ` 32 Displays log messages for a model.`[1:] 33 34 var usageDebugLogDetails = ` 35 36 This command provides access to all logged Juju activity on a per-model 37 basis. By default, the logs for the currently select model are shown. 38 39 Each log line is emitted in this format: 40 41 <entity> <timestamp> <log-level> <module>:<line-no> <message> 42 43 The "entity" is the source of the message: a machine or unit. The names for 44 machines and units can be seen in the output of `[1:] + "`juju status`" + `. 45 46 The '--include' and '--exclude' options filter by entity. The entity can be 47 a machine, unit, or application. 48 49 The '--include-module' and '--exclude-module' options filter by (dotted) 50 logging module name. The module name can be truncated such that all loggers 51 with the prefix will match. 52 53 The filtering options combine as follows: 54 * All --include options are logically ORed together. 55 * All --exclude options are logically ORed together. 56 * All --include-module options are logically ORed together. 57 * All --exclude-module options are logically ORed together. 58 * The combined --include, --exclude, --include-module and --exclude-module 59 selections are logically ANDed to form the complete filter. 60 61 Examples: 62 63 Exclude all machine 0 messages; show a maximum of 100 lines; and continue to 64 append filtered messages: 65 66 juju debug-log --exclude machine-0 --lines 100 67 68 Include only unit mysql/0 messages; show a maximum of 50 lines; and then 69 exit: 70 71 juju debug-log -T --include unit-mysql-0 --lines 50 72 73 Show all messages from unit apache2/3 or machine 1 and then exit: 74 75 juju debug-log -T --replay --include unit-apache2-3 --include machine-1 76 77 Show all juju.worker.uniter logging module messages that are also unit 78 wordpress/0 messages, and then show any new log messages which match the 79 filter: 80 81 juju debug-log --replay 82 --include-module juju.worker.uniter \ 83 --include unit-wordpress-0 84 85 Show all messages from the juju.worker.uniter module, except those sent from 86 machine-3 or machine-4, and then stop: 87 88 juju debug-log --replay --no-tail 89 --include-module juju.worker.uniter \ 90 --exclude machine-3 \ 91 --exclude machine-4 92 93 To see all WARNING and ERROR messages and then continue showing any 94 new WARNING and ERROR messages as they are logged: 95 96 juju debug-log --replay --level WARNING 97 98 See also: 99 status 100 ssh` 101 102 func (c *debugLogCommand) Info() *cmd.Info { 103 return jujucmd.Info(&cmd.Info{ 104 Name: "debug-log", 105 Purpose: usageDebugLogSummary, 106 Doc: usageDebugLogDetails, 107 }) 108 } 109 110 func newDebugLogCommand(store jujuclient.ClientStore) cmd.Command { 111 return newDebugLogCommandTZ(store, time.Local) 112 } 113 114 func newDebugLogCommandTZ(store jujuclient.ClientStore, tz *time.Location) cmd.Command { 115 cmd := &debugLogCommand{tz: tz} 116 cmd.SetClientStore(store) 117 return modelcmd.Wrap(cmd) 118 } 119 120 type debugLogCommand struct { 121 modelcmd.ModelCommandBase 122 123 level string 124 params common.DebugLogParams 125 126 utc bool 127 location bool 128 date bool 129 ms bool 130 131 tail bool 132 notail bool 133 color bool 134 135 format string 136 tz *time.Location 137 } 138 139 func (c *debugLogCommand) SetFlags(f *gnuflag.FlagSet) { 140 c.ModelCommandBase.SetFlags(f) 141 f.Var(cmd.NewAppendStringsValue(&c.params.IncludeEntity), "i", "Only show log messages for these entities") 142 f.Var(cmd.NewAppendStringsValue(&c.params.IncludeEntity), "include", "Only show log messages for these entities") 143 f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeEntity), "x", "Do not show log messages for these entities") 144 f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeEntity), "exclude", "Do not show log messages for these entities") 145 f.Var(cmd.NewAppendStringsValue(&c.params.IncludeModule), "include-module", "Only show log messages for these logging modules") 146 f.Var(cmd.NewAppendStringsValue(&c.params.ExcludeModule), "exclude-module", "Do not show log messages for these logging modules") 147 148 f.StringVar(&c.level, "l", "", "Log level to show, one of [TRACE, DEBUG, INFO, WARNING, ERROR]") 149 f.StringVar(&c.level, "level", "", "") 150 151 f.UintVar(&c.params.Backlog, "n", defaultLineCount, "Show this many of the most recent (possibly filtered) lines, and continue to append") 152 f.UintVar(&c.params.Backlog, "lines", defaultLineCount, "") 153 f.UintVar(&c.params.Limit, "limit", 0, "Exit once this many of the most recent (possibly filtered) lines are shown") 154 f.BoolVar(&c.params.Replay, "replay", false, "Show the entire (possibly filtered) log and continue to append") 155 156 f.BoolVar(&c.notail, "no-tail", false, "Stop after returning existing log messages") 157 f.BoolVar(&c.tail, "tail", false, "Wait for new logs") 158 f.BoolVar(&c.color, "color", false, "Force use of ANSI color codes") 159 160 f.BoolVar(&c.utc, "utc", false, "Show times in UTC") 161 f.BoolVar(&c.location, "location", false, "Show filename and line numbers") 162 f.BoolVar(&c.date, "date", false, "Show dates as well as times") 163 f.BoolVar(&c.ms, "ms", false, "Show times to millisecond precision") 164 } 165 166 func (c *debugLogCommand) Init(args []string) error { 167 if c.level != "" { 168 level, ok := loggo.ParseLevel(c.level) 169 if !ok || level < loggo.TRACE || level > loggo.ERROR { 170 return errors.Errorf("level value %q is not one of %q, %q, %q, %q, %q", 171 c.level, loggo.TRACE, loggo.DEBUG, loggo.INFO, loggo.WARNING, loggo.ERROR) 172 } 173 c.params.Level = level 174 } 175 if c.tail && c.notail { 176 return errors.NotValidf("setting --tail and --no-tail") 177 } 178 if c.utc { 179 c.tz = time.UTC 180 } 181 if c.date { 182 c.format = "2006-01-02 15:04:05" 183 } else { 184 c.format = "15:04:05" 185 } 186 if c.ms { 187 c.format = c.format + ".000" 188 } 189 c.params.IncludeEntity = c.processEntities(c.params.IncludeEntity) 190 c.params.ExcludeEntity = c.processEntities(c.params.ExcludeEntity) 191 return cmd.CheckEmpty(args) 192 } 193 194 func (c *debugLogCommand) processEntities(entities []string) []string { 195 if entities == nil { 196 return nil 197 } 198 result := make([]string, len(entities)) 199 for i, entity := range entities { 200 // A stringified unit or machine tag never match their "IsValid" 201 // function from names, so if the string value passed in is a valid 202 // machine or unit, then convert here. 203 if names.IsValidMachine(entity) { 204 entity = names.NewMachineTag(entity).String() 205 } else if names.IsValidUnit(entity) { 206 entity = names.NewUnitTag(entity).String() 207 } else { 208 // Now we want to deal with a special case. Both stringified 209 // machine tags and stringified units are valid application names. 210 // So here we use special knowledge about how tags are serialized to 211 // be able to give a better user experience. If the user asks for 212 // --include nova-compute, we should give all nova-compute units. 213 if strings.HasPrefix(entity, names.UnitTagKind+"-") || 214 strings.HasPrefix(entity, names.MachineTagKind+"-") { 215 // no-op pass through 216 } else if names.IsValidApplication(entity) { 217 // Assume that the entity refers to an application. 218 entity = names.UnitTagKind + "-" + entity + "-*" 219 } 220 } 221 result[i] = entity 222 } 223 return result 224 } 225 226 type DebugLogAPI interface { 227 WatchDebugLog(params common.DebugLogParams) (<-chan common.LogMessage, error) 228 Close() error 229 } 230 231 var getDebugLogAPI = func(c *debugLogCommand) (DebugLogAPI, error) { 232 return c.NewAPIClient() 233 } 234 235 func isTerminal(f interface{}) bool { 236 f_, ok := f.(*os.File) 237 if !ok { 238 return false 239 } 240 return isatty.IsTerminal(f_.Fd()) 241 } 242 243 // Run retrieves the debug log via the API. 244 func (c *debugLogCommand) Run(ctx *cmd.Context) (err error) { 245 if c.tail { 246 c.params.NoTail = false 247 } else if c.notail { 248 c.params.NoTail = true 249 } else { 250 // Set the default tail option to true if the caller is 251 // using a terminal. 252 c.params.NoTail = !isTerminal(ctx.Stdout) 253 } 254 255 client, err := getDebugLogAPI(c) 256 if err != nil { 257 return err 258 } 259 defer client.Close() 260 messages, err := client.WatchDebugLog(c.params) 261 if err != nil { 262 return err 263 } 264 writer := ansiterm.NewWriter(ctx.Stdout) 265 if c.color { 266 writer.SetColorCapable(true) 267 } 268 for { 269 msg, ok := <-messages 270 if !ok { 271 break 272 } 273 c.writeLogRecord(writer, msg) 274 } 275 276 return nil 277 } 278 279 var SeverityColor = map[string]*ansiterm.Context{ 280 "TRACE": ansiterm.Foreground(ansiterm.Default), 281 "DEBUG": ansiterm.Foreground(ansiterm.Green), 282 "INFO": ansiterm.Foreground(ansiterm.BrightBlue), 283 "WARNING": ansiterm.Foreground(ansiterm.Yellow), 284 "ERROR": ansiterm.Foreground(ansiterm.BrightRed), 285 "CRITICAL": { 286 Foreground: ansiterm.White, 287 Background: ansiterm.Red, 288 }, 289 } 290 291 func (c *debugLogCommand) writeLogRecord(w *ansiterm.Writer, r common.LogMessage) { 292 ts := r.Timestamp.In(c.tz).Format(c.format) 293 fmt.Fprintf(w, "%s: %s ", r.Entity, ts) 294 SeverityColor[r.Severity].Fprintf(w, r.Severity) 295 fmt.Fprintf(w, " %s ", r.Module) 296 if c.location { 297 loggocolor.LocationColor.Fprintf(w, "%s ", r.Location) 298 } 299 fmt.Fprintln(w, r.Message) 300 }