github.com/Cloud-Foundations/Dominator@v0.3.4/lib/logbuf/http.go (about) 1 package logbuf 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "net/http" 8 "os" 9 "path" 10 "runtime" 11 "sort" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/Cloud-Foundations/Dominator/lib/html" 17 "github.com/Cloud-Foundations/Dominator/lib/url" 18 _ "github.com/Cloud-Foundations/tricorder/go/healthserver" 19 ) 20 21 type countingWriter struct { 22 count uint64 23 writer io.Writer 24 prefixLine string 25 } 26 27 func showRecentLinks(w io.Writer, recentFirstString string) { 28 var recentFirstStringOnly string 29 if recentFirstString != "" && recentFirstString[0] == '&' { 30 recentFirstStringOnly = "?" + recentFirstString[1:] 31 } 32 fmt.Fprintf(w, "Show last: <a href=\"logs/showLast?1m%s\">minute</a>\n", 33 recentFirstString) 34 fmt.Fprintf(w, " <a href=\"logs/showLast?10m%s\">10 min</a>\n", 35 recentFirstString) 36 fmt.Fprintf(w, " <a href=\"logs/showLast?1h%s\">hour</a>\n", 37 recentFirstString) 38 fmt.Fprintf(w, " <a href=\"logs/showLast?1d%s\">day</a>\n", 39 recentFirstString) 40 fmt.Fprintf(w, " <a href=\"logs/showLast?1w%s\">week</a>\n", 41 recentFirstString) 42 fmt.Fprintf(w, " <a href=\"logs/showLast?1M%s\">month</a>\n", 43 recentFirstString) 44 fmt.Fprintf(w, " <a href=\"logs/showLast?1y%s\">year</a>\n", 45 recentFirstString) 46 fmt.Fprintf(w, " <a href=\"logs/showSinceStartup%s\">since startup</a><br>\n", 47 recentFirstStringOnly) 48 fmt.Fprintln(w, `Show <a href="logs/showStackTrace">stack trace</a>`) 49 } 50 51 func (w *countingWriter) Write(p []byte) (n int, err error) { 52 if w.prefixLine != "" { 53 w.writer.Write([]byte(w.prefixLine)) 54 w.prefixLine = "" 55 } 56 n, err = w.writer.Write(p) 57 if n > 0 { 58 w.count += uint64(n) 59 } 60 return 61 } 62 63 func (lb *LogBuffer) addHttpHandlers() { 64 if lb.options.HttpServeMux == nil { 65 return 66 } 67 html.ServeMuxHandleFunc(lb.options.HttpServeMux, "/logs", 68 lb.httpListHandler) 69 html.ServeMuxHandleFunc(lb.options.HttpServeMux, "/logs/dump", 70 lb.httpDumpHandler) 71 html.ServeMuxHandleFunc(lb.options.HttpServeMux, "/logs/showLast", 72 lb.httpShowLastHandler) 73 html.ServeMuxHandleFunc(lb.options.HttpServeMux, "/logs/showPreviousPanic", 74 lb.httpShowPreviousPanicHandler) 75 html.ServeMuxHandleFunc(lb.options.HttpServeMux, "/logs/showSinceStartup", 76 lb.httpShowSinceStartupHandler) 77 html.ServeMuxHandleFunc(lb.options.HttpServeMux, "/logs/showStackTrace", 78 lb.httpShowStackTraceHandler) 79 } 80 81 func (lb *LogBuffer) dumpFile(writer io.Writer, filename string, 82 recentFirst bool) error { 83 file, err := os.Open(path.Join(lb.options.Directory, filename)) 84 if err != nil { 85 return err 86 } 87 defer file.Close() 88 if recentFirst { 89 scanner := bufio.NewScanner(file) 90 lines := make([]string, 0) 91 for scanner.Scan() { 92 line := scanner.Text() 93 if len(line) < 1 { 94 continue 95 } 96 lines = append(lines, line) 97 } 98 if err = scanner.Err(); err == nil { 99 reverseStrings(lines) 100 for _, line := range lines { 101 fmt.Fprintln(writer, line) 102 } 103 } 104 } else { 105 _, err = io.Copy(writer, bufio.NewReader(file)) 106 } 107 return err 108 } 109 110 func (lb *LogBuffer) httpDumpHandler(w http.ResponseWriter, req *http.Request) { 111 parsedQuery := url.ParseQuery(req.URL) 112 name, ok := parsedQuery.Table["name"] 113 if !ok { 114 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 115 w.WriteHeader(http.StatusBadRequest) 116 return 117 } 118 _, recentFirst := parsedQuery.Flags["recentFirst"] 119 if name == "latest" { 120 lbFilename := "" 121 lb.rwMutex.RLock() 122 if lb.file != nil { 123 lbFilename = lb.file.Name() 124 } 125 lb.rwMutex.RUnlock() 126 if lbFilename == "" { 127 writer := bufio.NewWriter(w) 128 defer writer.Flush() 129 lb.Dump(writer, "", "", recentFirst) 130 return 131 } 132 name = path.Base(lbFilename) 133 } 134 writer := bufio.NewWriter(w) 135 defer writer.Flush() 136 err := lb.dumpFile(writer, path.Base(path.Clean(name)), recentFirst) 137 if err != nil && os.IsNotExist(err) { 138 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 139 w.WriteHeader(http.StatusNotFound) 140 return 141 } 142 if err != nil { 143 fmt.Fprintln(writer, err) 144 } 145 } 146 147 func (lb *LogBuffer) httpListHandler(w http.ResponseWriter, req *http.Request) { 148 if lb.options.Directory == "" { 149 return 150 } 151 writer := bufio.NewWriter(w) 152 defer writer.Flush() 153 parsedQuery := url.ParseQuery(req.URL) 154 _, recentFirst := parsedQuery.Flags["recentFirst"] 155 names, panicMap, err := lb.list(recentFirst) 156 if err != nil { 157 fmt.Fprintln(writer, err) 158 return 159 } 160 recentFirstString := "" 161 if recentFirst { 162 recentFirstString = "&recentFirst" 163 } 164 if parsedQuery.OutputType() == url.OutputTypeText { 165 for _, name := range names { 166 fmt.Fprintln(writer, name) 167 } 168 return 169 } 170 fmt.Fprintln(writer, "<body>") 171 fmt.Fprint(writer, "Logs: ") 172 if recentFirst { 173 fmt.Fprintf(writer, "showing recent first ") 174 fmt.Fprintln(writer, `<a href="logs">show recent last</a><br>`) 175 } else { 176 fmt.Fprintf(writer, "showing recent last ") 177 fmt.Fprintln(writer, 178 `<a href="logs?recentFirst">show recent first</a><br>`) 179 } 180 showRecentLinks(writer, recentFirstString) 181 fmt.Fprintln(writer, "<p>") 182 currentName := "" 183 lb.rwMutex.RLock() 184 if lb.file != nil { 185 currentName = path.Base(lb.file.Name()) 186 } 187 lb.rwMutex.RUnlock() 188 if recentFirst { 189 fmt.Fprintf(writer, 190 "<a href=\"logs/dump?name=latest%s\">current</a><br>\n", 191 recentFirstString) 192 } 193 for _, name := range names { 194 if !recentFirst { 195 if name == lb.firstFile { 196 fmt.Fprintln(writer, "<hr>") 197 } 198 } 199 if name == currentName { 200 fmt.Fprintf(writer, 201 "<a href=\"logs/dump?name=%s%s\">%s</a> (current)<br>\n", 202 name, recentFirstString, name) 203 } else { 204 hasPanic := "" 205 if _, ok := panicMap[name]; ok { 206 hasPanic = " (has panic log)" 207 } 208 fmt.Fprintf(writer, 209 "<a href=\"logs/dump?name=%s%s\">%s</a>%s<br>\n", 210 name, recentFirstString, name, hasPanic) 211 } 212 if recentFirst { 213 if name == lb.firstFile { 214 fmt.Fprintln(writer, "<hr>") 215 } 216 } 217 } 218 if !recentFirst { 219 fmt.Fprintf(writer, 220 "<a href=\"logs/dump?name=latest%s\">current</a><br>\n", 221 recentFirstString) 222 } 223 fmt.Fprintln(writer, "</body>") 224 } 225 226 func (lb *LogBuffer) httpShowLastHandler(w http.ResponseWriter, 227 req *http.Request) { 228 parsedQuery := url.ParseQuery(req.URL) 229 _, recentFirst := parsedQuery.Flags["recentFirst"] 230 for flag := range parsedQuery.Flags { 231 length := len(flag) 232 if length < 2 { 233 continue 234 } 235 unitChar := flag[length-1] 236 var unit time.Duration 237 switch unitChar { 238 case 's': 239 unit = time.Second 240 case 'm': 241 unit = time.Minute 242 case 'h': 243 unit = time.Hour 244 case 'd': 245 unit = time.Hour * 24 246 case 'w': 247 unit = time.Hour * 24 * 7 248 case 'M': 249 // TODO(rgooch): Fix this crude approximation. 250 unit = time.Hour * 24 * 31 251 case 'y': 252 unit = time.Hour * 24 * 365 253 default: 254 continue 255 } 256 if val, err := strconv.ParseUint(flag[:length-1], 10, 64); err != nil { 257 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 258 w.WriteHeader(http.StatusBadRequest) 259 return 260 } else { 261 lb.showRecent(w, time.Duration(val)*unit, recentFirst) 262 return 263 } 264 } 265 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 266 w.WriteHeader(http.StatusBadRequest) 267 } 268 269 func (lb *LogBuffer) httpShowPreviousPanicHandler(w http.ResponseWriter, 270 req *http.Request) { 271 writer := bufio.NewWriter(w) 272 defer writer.Flush() 273 panicLogfile := lb.panicLogfile 274 if panicLogfile == nil { 275 fmt.Fprintln(writer, "Last invocation did not panic!") 276 return 277 } 278 if *panicLogfile == "" { 279 fmt.Fprintln(writer, "Logfile for previous invocation has expired") 280 return 281 } 282 file, err := os.Open(path.Join(lb.options.Directory, *panicLogfile)) 283 if err != nil { 284 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 285 w.WriteHeader(http.StatusNotFound) 286 return 287 } 288 defer file.Close() 289 _, err = io.Copy(writer, bufio.NewReader(file)) 290 if err != nil { 291 fmt.Fprintln(writer, err) 292 } 293 } 294 295 func (lb *LogBuffer) httpShowSinceStartupHandler(w http.ResponseWriter, 296 req *http.Request) { 297 parsedQuery := url.ParseQuery(req.URL) 298 _, recentFirst := parsedQuery.Flags["recentFirst"] 299 writer := bufio.NewWriter(w) 300 defer writer.Flush() 301 names, _, err := lb.list(recentFirst) 302 if err != nil { 303 fmt.Fprintln(writer, err) 304 return 305 } 306 lb.flush() 307 dumpFiles := recentFirst 308 for _, name := range names { 309 if !recentFirst && name == lb.firstFile { 310 dumpFiles = true 311 } 312 if dumpFiles { 313 lb.dumpFile(writer, name, recentFirst) 314 } 315 if recentFirst && name == lb.firstFile { 316 dumpFiles = false 317 } 318 } 319 } 320 321 func (lb *LogBuffer) httpShowStackTraceHandler(w http.ResponseWriter, 322 req *http.Request) { 323 writer := bufio.NewWriter(w) 324 defer writer.Flush() 325 lb.rwMutex.Lock() 326 if time.Since(lb.lastStackTrace) < 2*time.Second { 327 lb.rwMutex.Unlock() 328 writer.Write([]byte("Too soon\n")) 329 return 330 } 331 lb.lastStackTrace = time.Now() 332 lb.rwMutex.Unlock() 333 buffer := make([]byte, 1<<20) 334 nBytes := runtime.Stack(buffer, true) 335 writer.Write(buffer[:nBytes]) 336 } 337 338 func (lb *LogBuffer) list(recentFirst bool) ( 339 []string, map[string]struct{}, error) { 340 file, err := os.Open(lb.options.Directory) 341 if err != nil { 342 return nil, nil, err 343 } 344 fileInfos, err := file.Readdir(-1) 345 file.Close() 346 if err != nil { 347 return nil, nil, err 348 } 349 panicMap := make(map[string]struct{}) 350 names := make([]string, 0, len(fileInfos)) 351 for _, fi := range fileInfos { 352 if strings.Count(fi.Name(), ":") == 3 { 353 names = append(names, fi.Name()) 354 if fi.Mode()&os.ModeSticky != 0 { 355 panicMap[fi.Name()] = struct{}{} 356 } 357 } 358 } 359 sort.Strings(names) 360 if recentFirst { 361 reverseStrings(names) 362 } 363 return names, panicMap, nil 364 } 365 366 func (lb *LogBuffer) showRecent(w io.Writer, duration time.Duration, 367 recentFirst bool) { 368 writer := bufio.NewWriter(w) 369 defer writer.Flush() 370 names, _, err := lb.list(true) 371 if err != nil { 372 fmt.Fprintln(writer, err) 373 return 374 } 375 earliestTime := time.Now().Add(-duration) 376 // Get a list of names which may be recent enough. 377 tmpNames := make([]string, 0, len(names)) 378 for _, name := range names { 379 startTime, err := time.ParseInLocation(timeLayout, name, time.Local) 380 if err != nil { 381 continue 382 } 383 tmpNames = append(tmpNames, name) 384 if startTime.Before(earliestTime) { 385 break 386 } 387 } 388 names = tmpNames 389 if !recentFirst { 390 reverseStrings(names) 391 } 392 fmt.Fprintln(writer, "<body>") 393 cWriter := &countingWriter{writer: writer, prefixLine: "<pre>\n"} 394 lb.flush() 395 for _, name := range names { 396 cWriter.count = 0 397 foundReopenMessage, _ := lb.dumpSince(cWriter, name, earliestTime, "", 398 "\n", recentFirst) 399 if cWriter.count > 0 && !foundReopenMessage { 400 cWriter.prefixLine = "</pre>\n<hr>\n<pre>\n" 401 } 402 } 403 fmt.Fprintln(writer, "</pre>") 404 fmt.Fprintln(writer, "</body>") 405 } 406 407 func (lb *LogBuffer) writeHtml(writer io.Writer) { 408 if lb.options.Directory != "" { 409 fmt.Fprintln(writer, `<a href="logs">Logs:</a><br>`) 410 panicLogfile := lb.panicLogfile 411 if panicLogfile != nil { 412 fmt.Fprint(writer, 413 "<font color=\"red\">Last invocation paniced</font>, ") 414 if *panicLogfile == "" { 415 fmt.Fprintln(writer, "logfile no longer available<br>") 416 } else { 417 fmt.Fprintln(writer, 418 "<a href=\"logs/showPreviousPanic\">logfile</a><br>") 419 } 420 } 421 } else { 422 fmt.Fprintln(writer, "Logs:<br>") 423 } 424 fmt.Fprintln(writer, "<pre>") 425 lb.dump(writer, "", "", false, true) 426 fmt.Fprintln(writer, "</pre>") 427 }