github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/state/apiserver/debuglog.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io" 10 "net/http" 11 "net/url" 12 "os" 13 "path/filepath" 14 "strconv" 15 "strings" 16 17 "code.google.com/p/go.net/websocket" 18 "github.com/juju/loggo" 19 "github.com/juju/utils/tailer" 20 "launchpad.net/tomb" 21 22 "github.com/juju/juju/state/api/params" 23 ) 24 25 // debugLogHandler takes requests to watch the debug log. 26 type debugLogHandler struct { 27 httpHandler 28 logDir string 29 } 30 31 var maxLinesReached = fmt.Errorf("max lines reached") 32 33 // ServeHTTP will serve up connections as a websocket. 34 // Args for the HTTP request are as follows: 35 // includeEntity -> []string - lists entity tags to include in the response 36 // - tags may finish with a '*' to match a prefix e.g.: unit-mysql-*, machine-2 37 // - if none are set, then all lines are considered included 38 // includeModule -> []string - lists logging modules to include in the response 39 // - if none are set, then all lines are considered included 40 // excludeEntity -> []string - lists entity tags to exclude from the response 41 // - as with include, it may finish with a '*' 42 // excludeModule -> []string - lists logging modules to exclude from the response 43 // limit -> uint - show *at most* this many lines 44 // backlog -> uint 45 // - go back this many lines from the end before starting to filter 46 // - has no meaning if 'replay' is true 47 // level -> string one of [TRACE, DEBUG, INFO, WARNING, ERROR] 48 // replay -> string - one of [true, false], if true, start the file from the start 49 func (h *debugLogHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 50 server := websocket.Server{ 51 Handler: func(socket *websocket.Conn) { 52 logger.Infof("debug log handler starting") 53 if err := h.authenticate(req); err != nil { 54 h.sendError(socket, fmt.Errorf("auth failed: %v", err)) 55 socket.Close() 56 return 57 } 58 if err := h.validateEnvironUUID(req); err != nil { 59 h.sendError(socket, err) 60 socket.Close() 61 return 62 } 63 stream, err := newLogStream(req.URL.Query()) 64 if err != nil { 65 h.sendError(socket, err) 66 socket.Close() 67 return 68 } 69 // Open log file. 70 logLocation := filepath.Join(h.logDir, "all-machines.log") 71 logFile, err := os.Open(logLocation) 72 if err != nil { 73 h.sendError(socket, fmt.Errorf("cannot open log file: %v", err)) 74 socket.Close() 75 return 76 } 77 defer logFile.Close() 78 if err := stream.positionLogFile(logFile); err != nil { 79 h.sendError(socket, fmt.Errorf("cannot position log file: %v", err)) 80 socket.Close() 81 return 82 } 83 84 // If we get to here, no more errors to report, so we report a nil 85 // error. This way the first line of the socket is always a json 86 // formatted simple error. 87 if err := h.sendError(socket, nil); err != nil { 88 logger.Errorf("could not send good log stream start") 89 socket.Close() 90 return 91 } 92 93 stream.start(logFile, socket) 94 go func() { 95 defer stream.tomb.Done() 96 defer socket.Close() 97 stream.tomb.Kill(stream.loop()) 98 }() 99 if err := stream.tomb.Wait(); err != nil { 100 if err != maxLinesReached { 101 logger.Errorf("debug-log handler error: %v", err) 102 } 103 } 104 }} 105 server.ServeHTTP(w, req) 106 } 107 108 func newLogStream(queryMap url.Values) (*logStream, error) { 109 maxLines := uint(0) 110 if value := queryMap.Get("maxLines"); value != "" { 111 num, err := strconv.ParseUint(value, 10, 64) 112 if err != nil { 113 return nil, fmt.Errorf("maxLines value %q is not a valid unsigned number", value) 114 } 115 maxLines = uint(num) 116 } 117 118 fromTheStart := false 119 if value := queryMap.Get("replay"); value != "" { 120 replay, err := strconv.ParseBool(value) 121 if err != nil { 122 return nil, fmt.Errorf("replay value %q is not a valid boolean", value) 123 } 124 fromTheStart = replay 125 } 126 127 backlog := uint(0) 128 if value := queryMap.Get("backlog"); value != "" { 129 num, err := strconv.ParseUint(value, 10, 64) 130 if err != nil { 131 return nil, fmt.Errorf("backlog value %q is not a valid unsigned number", value) 132 } 133 backlog = uint(num) 134 } 135 136 level := loggo.UNSPECIFIED 137 if value := queryMap.Get("level"); value != "" { 138 var ok bool 139 level, ok = loggo.ParseLevel(value) 140 if !ok || level < loggo.TRACE || level > loggo.ERROR { 141 return nil, fmt.Errorf("level value %q is not one of %q, %q, %q, %q, %q", 142 value, loggo.TRACE, loggo.DEBUG, loggo.INFO, loggo.WARNING, loggo.ERROR) 143 } 144 } 145 146 return &logStream{ 147 includeEntity: queryMap["includeEntity"], 148 includeModule: queryMap["includeModule"], 149 excludeEntity: queryMap["excludeEntity"], 150 excludeModule: queryMap["excludeModule"], 151 maxLines: maxLines, 152 fromTheStart: fromTheStart, 153 backlog: backlog, 154 filterLevel: level, 155 }, nil 156 } 157 158 // sendError sends a JSON-encoded error response. 159 func (h *debugLogHandler) sendError(w io.Writer, err error) error { 160 response := ¶ms.ErrorResult{} 161 if err != nil { 162 response.Error = ¶ms.Error{Message: fmt.Sprint(err)} 163 } 164 message, err := json.Marshal(response) 165 if err != nil { 166 // If we are having trouble marshalling the error, we are in big trouble. 167 logger.Errorf("failure to marshal SimpleError: %v", err) 168 return err 169 } 170 message = append(message, []byte("\n")...) 171 _, err = w.Write(message) 172 return err 173 } 174 175 type logLine struct { 176 line string 177 agent string 178 level loggo.Level 179 module string 180 } 181 182 func parseLogLine(line string) *logLine { 183 const ( 184 agentField = 0 185 levelField = 3 186 moduleField = 4 187 ) 188 fields := strings.Fields(line) 189 result := &logLine{ 190 line: line, 191 } 192 if len(fields) > agentField { 193 agent := fields[agentField] 194 if strings.HasSuffix(agent, ":") { 195 result.agent = agent[:len(agent)-1] 196 } 197 } 198 if len(fields) > moduleField { 199 if level, valid := loggo.ParseLevel(fields[levelField]); valid { 200 result.level = level 201 result.module = fields[moduleField] 202 } 203 } 204 205 return result 206 } 207 208 // logStream runs the tailer to read a log file and stream 209 // it via a web socket. 210 type logStream struct { 211 tomb tomb.Tomb 212 logTailer *tailer.Tailer 213 filterLevel loggo.Level 214 includeEntity []string 215 includeModule []string 216 excludeEntity []string 217 excludeModule []string 218 backlog uint 219 maxLines uint 220 lineCount uint 221 fromTheStart bool 222 } 223 224 // positionLogFile will update the internal read position of the logFile to be 225 // at the end of the file or somewhere in the middle if backlog has been specified. 226 func (stream *logStream) positionLogFile(logFile io.ReadSeeker) error { 227 // Seek to the end, or lines back from the end if we need to. 228 if !stream.fromTheStart { 229 return tailer.SeekLastLines(logFile, stream.backlog, stream.filterLine) 230 } 231 return nil 232 } 233 234 // start the tailer listening to the logFile, and sending the matching 235 // lines to the writer. 236 func (stream *logStream) start(logFile io.ReadSeeker, writer io.Writer) { 237 stream.logTailer = tailer.NewTailer(logFile, writer, stream.countedFilterLine) 238 } 239 240 // loop starts the tailer with the log file and the web socket. 241 func (stream *logStream) loop() error { 242 select { 243 case <-stream.logTailer.Dead(): 244 return stream.logTailer.Err() 245 case <-stream.tomb.Dying(): 246 stream.logTailer.Stop() 247 } 248 return nil 249 } 250 251 // filterLine checks the received line for one of the confgured tags. 252 func (stream *logStream) filterLine(line []byte) bool { 253 log := parseLogLine(string(line)) 254 return stream.checkIncludeEntity(log) && 255 stream.checkIncludeModule(log) && 256 !stream.exclude(log) && 257 stream.checkLevel(log) 258 } 259 260 // countedFilterLine checks the received line for one of the confgured tags, 261 // and also checks to make sure the stream doesn't send more than the 262 // specified number of lines. 263 func (stream *logStream) countedFilterLine(line []byte) bool { 264 result := stream.filterLine(line) 265 if result && stream.maxLines > 0 { 266 stream.lineCount++ 267 result = stream.lineCount <= stream.maxLines 268 if stream.lineCount == stream.maxLines { 269 stream.tomb.Kill(maxLinesReached) 270 } 271 } 272 return result 273 } 274 275 func (stream *logStream) checkIncludeEntity(line *logLine) bool { 276 if len(stream.includeEntity) == 0 { 277 return true 278 } 279 for _, value := range stream.includeEntity { 280 // special handling, if ends with '*', check prefix 281 if strings.HasSuffix(value, "*") { 282 if strings.HasPrefix(line.agent, value[:len(value)-1]) { 283 return true 284 } 285 } else if line.agent == value { 286 return true 287 } 288 } 289 return false 290 } 291 292 func (stream *logStream) checkIncludeModule(line *logLine) bool { 293 if len(stream.includeModule) == 0 { 294 return true 295 } 296 for _, value := range stream.includeModule { 297 if strings.HasPrefix(line.module, value) { 298 return true 299 } 300 } 301 return false 302 } 303 304 func (stream *logStream) exclude(line *logLine) bool { 305 for _, value := range stream.excludeEntity { 306 // special handling, if ends with '*', check prefix 307 if strings.HasSuffix(value, "*") { 308 if strings.HasPrefix(line.agent, value[:len(value)-1]) { 309 return true 310 } 311 } else if line.agent == value { 312 return true 313 } 314 } 315 for _, value := range stream.excludeModule { 316 if strings.HasPrefix(line.module, value) { 317 return true 318 } 319 } 320 return false 321 } 322 323 func (stream *logStream) checkLevel(line *logLine) bool { 324 return line.level >= stream.filterLevel 325 }