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 }