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  }