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 }