github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/cmd/jujud/dumplogs/dumplogs.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // A simple command for dumping out the logs stored in
     5  // MongoDB. Intended to be use in emergency situations to recover logs
     6  // when Juju is broken somehow.
     7  
     8  package dumplogs
     9  
    10  import (
    11  	"bufio"
    12  	"fmt"
    13  	"io/ioutil"
    14  	"os"
    15  	"path/filepath"
    16  	"time"
    17  
    18  	"github.com/juju/cmd"
    19  	"github.com/juju/errors"
    20  	"github.com/juju/gnuflag"
    21  	"github.com/juju/loggo"
    22  	"gopkg.in/juju/names.v2"
    23  
    24  	"github.com/juju/juju/agent"
    25  	jujudagent "github.com/juju/juju/cmd/jujud/agent"
    26  	corenames "github.com/juju/juju/juju/names"
    27  	"github.com/juju/juju/mongo"
    28  	"github.com/juju/juju/state"
    29  )
    30  
    31  // NewCommand returns a new Command instance which implements the
    32  // "juju-dumplogs" command.
    33  func NewCommand() cmd.Command {
    34  	return &dumpLogsCommand{
    35  		agentConfig: jujudagent.NewAgentConf(""),
    36  	}
    37  }
    38  
    39  type dumpLogsCommand struct {
    40  	cmd.CommandBase
    41  	agentConfig jujudagent.AgentConf
    42  	machineId   string
    43  	outDir      string
    44  }
    45  
    46  // Info implements cmd.Command.
    47  func (c *dumpLogsCommand) Info() *cmd.Info {
    48  	doc := `
    49  This tool can be used to access Juju's logs when the Juju controller
    50  isn't functioning for some reason. It must be run on a Juju controller
    51  server, connecting to the Juju database instance and generating a log
    52  file for each model that exists in the controller.
    53  
    54  Log files are written out to the current working directory by
    55  default. Use -d / --output-directory option to specify an alternate
    56  target directory.
    57  
    58  In order to connect to the database, the local machine agent's
    59  configuration is needed. In most circumstances the configuration will
    60  be found automatically. The --data-dir and/or --machine-id options may
    61  be required if the agent configuration can't be found automatically.
    62  `[1:]
    63  	return &cmd.Info{
    64  		Name:    corenames.JujuDumpLogs,
    65  		Purpose: "output the logs that are stored in the local Juju database",
    66  		Doc:     doc,
    67  	}
    68  }
    69  
    70  // SetFlags implements cmd.Command.
    71  func (c *dumpLogsCommand) SetFlags(f *gnuflag.FlagSet) {
    72  	c.agentConfig.AddFlags(f)
    73  	f.StringVar(&c.outDir, "d", ".", "directory to write logs files to")
    74  	f.StringVar(&c.outDir, "output-directory", ".", "")
    75  	f.StringVar(&c.machineId, "machine-id", "", "id of the machine on this host (optional)")
    76  }
    77  
    78  // Init implements cmd.Command.
    79  func (c *dumpLogsCommand) Init(args []string) error {
    80  	err := c.agentConfig.CheckArgs(args)
    81  	if err != nil {
    82  		return errors.Trace(err)
    83  	}
    84  
    85  	if c.machineId == "" {
    86  		machineId, err := c.findMachineId(c.agentConfig.DataDir())
    87  		if err != nil {
    88  			return errors.Trace(err)
    89  		}
    90  		c.machineId = machineId
    91  	} else if !names.IsValidMachine(c.machineId) {
    92  		return errors.New("--machine-id option expects a non-negative integer")
    93  	}
    94  
    95  	err = c.agentConfig.ReadConfig(names.NewMachineTag(c.machineId).String())
    96  	if err != nil {
    97  		return errors.Trace(err)
    98  	}
    99  
   100  	return nil
   101  }
   102  
   103  // Run implements cmd.Command.
   104  func (c *dumpLogsCommand) Run(ctx *cmd.Context) error {
   105  	config := c.agentConfig.CurrentConfig()
   106  	info, ok := config.MongoInfo()
   107  	if !ok {
   108  		return errors.New("no database connection info available (is this a controller host?)")
   109  	}
   110  
   111  	st0, err := state.Open(config.Model(), config.Controller(), info, mongo.DefaultDialOpts(), nil)
   112  	if err != nil {
   113  		return errors.Annotate(err, "failed to connect to database")
   114  	}
   115  	defer st0.Close()
   116  
   117  	envs, err := st0.AllModels()
   118  	if err != nil {
   119  		return errors.Annotate(err, "failed to look up models")
   120  	}
   121  	for _, env := range envs {
   122  		err := c.dumpLogsForEnv(ctx, st0, env.ModelTag())
   123  		if err != nil {
   124  			return errors.Annotatef(err, "failed to dump logs for model %s", env.UUID())
   125  		}
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  func (c *dumpLogsCommand) findMachineId(dataDir string) (string, error) {
   132  	entries, err := ioutil.ReadDir(agent.BaseDir(dataDir))
   133  	if err != nil {
   134  		return "", errors.Annotate(err, "failed to read agent configuration base directory")
   135  	}
   136  	for _, entry := range entries {
   137  		if entry.IsDir() {
   138  			tag, err := names.ParseMachineTag(entry.Name())
   139  			if err == nil {
   140  				return tag.Id(), nil
   141  			}
   142  		}
   143  	}
   144  	return "", errors.New("no machine agent configuration found")
   145  }
   146  
   147  func (c *dumpLogsCommand) dumpLogsForEnv(ctx *cmd.Context, st0 *state.State, tag names.ModelTag) error {
   148  	st, err := st0.ForModel(tag)
   149  	if err != nil {
   150  		return errors.Annotate(err, "failed open model")
   151  	}
   152  	defer st.Close()
   153  
   154  	logName := ctx.AbsPath(filepath.Join(c.outDir, fmt.Sprintf("%s.log", tag.Id())))
   155  	ctx.Infof("writing to %s", logName)
   156  
   157  	file, err := os.Create(logName)
   158  	if err != nil {
   159  		return errors.Annotate(err, "failed to open output file")
   160  	}
   161  	defer file.Close()
   162  
   163  	writer := bufio.NewWriter(file)
   164  	defer writer.Flush()
   165  
   166  	tailer, err := state.NewLogTailer(st, &state.LogTailerParams{NoTail: true})
   167  	if err != nil {
   168  		return errors.Annotate(err, "failed to create a log tailer")
   169  	}
   170  	logs := tailer.Logs()
   171  	for {
   172  		rec, ok := <-logs
   173  		if !ok {
   174  			break
   175  		}
   176  		writer.WriteString(c.format(
   177  			rec.Time,
   178  			rec.Level,
   179  			rec.Entity.String(),
   180  			rec.Module,
   181  			rec.Message,
   182  		) + "\n")
   183  	}
   184  
   185  	return nil
   186  }
   187  
   188  func (c *dumpLogsCommand) format(timestamp time.Time, level loggo.Level, entity, module, message string) string {
   189  	ts := timestamp.In(time.UTC).Format("2006-01-02 15:04:05")
   190  	return fmt.Sprintf("%s: %s %s %s %s", entity, ts, level, module, message)
   191  }