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