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