I've currently written three: - Request ID generation - Request logging, with color (!) - Recovery from panics
| @ -0,0 +1,149 @@ | |||
| package middleware | |||
| import ( | |||
| "bufio" | |||
| "bytes" | |||
| "log" | |||
| "net" | |||
| "net/http" | |||
| "time" | |||
| "github.com/zenazn/goji/web" | |||
| ) | |||
| // Logger is a middleware that logs the start and end of each request, along | |||
| // with some useful data about what was requested, what the response status was, | |||
| // and how long it took to return. When standard output is a TTY, Logger will | |||
| // print in color, otherwise it will print in black and white. | |||
| // | |||
| // Logger prints a request ID if one is provided. | |||
| func Logger(c *web.C, h http.Handler) http.Handler { | |||
| fn := func(w http.ResponseWriter, r *http.Request) { | |||
| reqId := GetReqId(*c) | |||
| printStart(reqId, r) | |||
| lw := wrapWriter(w) | |||
| t1 := time.Now() | |||
| h.ServeHTTP(lw, r) | |||
| lw.maybeWriteHeader() | |||
| t2 := time.Now() | |||
| printEnd(reqId, lw, t2.Sub(t1)) | |||
| } | |||
| return http.HandlerFunc(fn) | |||
| } | |||
| func printStart(reqId string, r *http.Request) { | |||
| var buf bytes.Buffer | |||
| if reqId != "" { | |||
| cW(&buf, bBlack, "[%s] ", reqId) | |||
| } | |||
| buf.WriteString("Started ") | |||
| cW(&buf, bMagenta, "%s ", r.Method) | |||
| cW(&buf, nBlue, "%q ", r.URL.String()) | |||
| buf.WriteString("from ") | |||
| buf.WriteString(r.RemoteAddr) | |||
| log.Print(buf.String()) | |||
| } | |||
| func printEnd(reqId string, w writerProxy, dt time.Duration) { | |||
| var buf bytes.Buffer | |||
| if reqId != "" { | |||
| cW(&buf, bBlack, "[%s] ", reqId) | |||
| } | |||
| buf.WriteString("Returning ") | |||
| if w.status() < 200 { | |||
| cW(&buf, bBlue, "%03d", w.status()) | |||
| } else if w.status() < 300 { | |||
| cW(&buf, bGreen, "%03d", w.status()) | |||
| } else if w.status() < 400 { | |||
| cW(&buf, bCyan, "%03d", w.status()) | |||
| } else if w.status() < 500 { | |||
| cW(&buf, bYellow, "%03d", w.status()) | |||
| } else { | |||
| cW(&buf, bRed, "%03d", w.status()) | |||
| } | |||
| buf.WriteString(" in ") | |||
| if dt < 500*time.Millisecond { | |||
| cW(&buf, nGreen, "%s", dt) | |||
| } else if dt < 5*time.Second { | |||
| cW(&buf, nYellow, "%s", dt) | |||
| } else { | |||
| cW(&buf, nRed, "%s", dt) | |||
| } | |||
| log.Print(buf.String()) | |||
| } | |||
| func wrapWriter(w http.ResponseWriter) writerProxy { | |||
| _, cn := w.(http.CloseNotifier) | |||
| _, fl := w.(http.Flusher) | |||
| _, hj := w.(http.Hijacker) | |||
| bw := basicWriter{ResponseWriter: w} | |||
| if cn && fl && hj { | |||
| return &fancyWriter{bw} | |||
| } else { | |||
| return &bw | |||
| } | |||
| } | |||
| type writerProxy interface { | |||
| http.ResponseWriter | |||
| maybeWriteHeader() | |||
| status() int | |||
| } | |||
| type basicWriter struct { | |||
| http.ResponseWriter | |||
| wroteHeader bool | |||
| code int | |||
| } | |||
| func (b *basicWriter) WriteHeader(code int) { | |||
| b.code = code | |||
| b.wroteHeader = true | |||
| b.ResponseWriter.WriteHeader(code) | |||
| } | |||
| func (b *basicWriter) Write(buf []byte) (int, error) { | |||
| b.maybeWriteHeader() | |||
| return b.ResponseWriter.Write(buf) | |||
| } | |||
| func (b *basicWriter) maybeWriteHeader() { | |||
| if !b.wroteHeader { | |||
| b.WriteHeader(http.StatusOK) | |||
| } | |||
| } | |||
| func (b *basicWriter) status() int { | |||
| return b.code | |||
| } | |||
| func (b *basicWriter) Unwrap() http.ResponseWriter { | |||
| return b.ResponseWriter | |||
| } | |||
| type fancyWriter struct { | |||
| basicWriter | |||
| } | |||
| func (f *fancyWriter) CloseNotify() <-chan bool { | |||
| cn := f.basicWriter.ResponseWriter.(http.CloseNotifier) | |||
| return cn.CloseNotify() | |||
| } | |||
| func (f *fancyWriter) Flush() { | |||
| fl := f.basicWriter.ResponseWriter.(http.Flusher) | |||
| fl.Flush() | |||
| } | |||
| func (f *fancyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { | |||
| hj := f.basicWriter.ResponseWriter.(http.Hijacker) | |||
| return hj.Hijack() | |||
| } | |||
| var _ http.CloseNotifier = &fancyWriter{} | |||
| var _ http.Flusher = &fancyWriter{} | |||
| var _ http.Hijacker = &fancyWriter{} | |||
| @ -0,0 +1,4 @@ | |||
| /* | |||
| Package middleware provides several standard middleware implementations. | |||
| */ | |||
| package middleware | |||
| @ -0,0 +1,44 @@ | |||
| package middleware | |||
| import ( | |||
| "bytes" | |||
| "log" | |||
| "net/http" | |||
| "runtime/debug" | |||
| "github.com/zenazn/goji/web" | |||
| ) | |||
| // Recoverer is a middleware that recovers from panics, logs the panic (and a | |||
| // backtrace), and returns a HTTP 500 (Internal Server Error) status if | |||
| // possible. | |||
| // | |||
| // Recoverer prints a request ID if one is provided. | |||
| func Recoverer(c *web.C, h http.Handler) http.Handler { | |||
| fn := func(w http.ResponseWriter, r *http.Request) { | |||
| reqId := GetReqId(*c) | |||
| defer func() { | |||
| if err := recover(); err != nil { | |||
| printPanic(reqId, err) | |||
| debug.PrintStack() | |||
| http.Error(w, http.StatusText(500), 500) | |||
| } | |||
| }() | |||
| h.ServeHTTP(w, r) | |||
| } | |||
| return http.HandlerFunc(fn) | |||
| } | |||
| func printPanic(reqId string, err interface{}) { | |||
| var buf bytes.Buffer | |||
| if reqId != "" { | |||
| cW(&buf, bBlack, "[%s] ", reqId) | |||
| } | |||
| cW(&buf, bRed, "panic: %#v", err) | |||
| log.Print(buf.String()) | |||
| } | |||
| @ -0,0 +1,70 @@ | |||
| package middleware | |||
| import ( | |||
| "crypto/rand" | |||
| "encoding/base64" | |||
| "fmt" | |||
| "net/http" | |||
| "os" | |||
| "strings" | |||
| "sync/atomic" | |||
| "github.com/zenazn/goji/web" | |||
| ) | |||
| // Key to use when setting the request ID. | |||
| const RequestIdKey = "reqId" | |||
| var prefix string | |||
| var reqid uint64 | |||
| func init() { | |||
| hostname, err := os.Hostname() | |||
| if hostname == "" || err != nil { | |||
| hostname = "localhost" | |||
| } | |||
| var buf [12]byte | |||
| rand.Read(buf[:]) | |||
| b64 := base64.StdEncoding.EncodeToString(buf[:]) | |||
| // Strip out annoying characters. We have something like a billion to | |||
| // one chance of having enough from 12 bytes of entropy | |||
| b64 = strings.NewReplacer("+", "", "/", "").Replace(b64) | |||
| prefix = fmt.Sprintf("%s/%s", hostname, b64[0:8]) | |||
| } | |||
| // RequestId is a middleware that injects a request ID into the context of each | |||
| // request. A request ID is a string of the form "host.example.com/random-0001", | |||
| // where "random" is a base62 random string that uniquely identifies this go | |||
| // process, and where the last number is an atomically incremented request | |||
| // counter. | |||
| func RequestId(c *web.C, h http.Handler) http.Handler { | |||
| fn := func(w http.ResponseWriter, r *http.Request) { | |||
| if c.Env == nil { | |||
| c.Env = make(map[string]interface{}) | |||
| } | |||
| myid := atomic.AddUint64(&reqid, 1) | |||
| c.Env[RequestIdKey] = fmt.Sprintf("%s-%06d", prefix, myid) | |||
| h.ServeHTTP(w, r) | |||
| } | |||
| return http.HandlerFunc(fn) | |||
| } | |||
| // Get a request ID from the given context if one is present. Returns the empty | |||
| // string if a request ID cannot be found. | |||
| func GetReqId(c web.C) string { | |||
| if c.Env == nil { | |||
| return "" | |||
| } | |||
| v, ok := c.Env[RequestIdKey] | |||
| if !ok { | |||
| return "" | |||
| } | |||
| if reqId, ok := v.(string); ok { | |||
| return reqId | |||
| } else { | |||
| return "" | |||
| } | |||
| } | |||
| @ -0,0 +1,44 @@ | |||
| package middleware | |||
| import ( | |||
| "bytes" | |||
| "fmt" | |||
| "code.google.com/p/go.crypto/ssh/terminal" | |||
| ) | |||
| var ( | |||
| // Normal colors | |||
| nBlack = []byte{'\033', '[', '3', '0', 'm'} | |||
| nRed = []byte{'\033', '[', '3', '1', 'm'} | |||
| nGreen = []byte{'\033', '[', '3', '2', 'm'} | |||
| nYellow = []byte{'\033', '[', '3', '3', 'm'} | |||
| nBlue = []byte{'\033', '[', '3', '4', 'm'} | |||
| nMagenta = []byte{'\033', '[', '3', '5', 'm'} | |||
| nCyan = []byte{'\033', '[', '3', '6', 'm'} | |||
| nWhite = []byte{'\033', '[', '3', '7', 'm'} | |||
| // Bright colors | |||
| bBlack = []byte{'\033', '[', '3', '0', ';', '1', 'm'} | |||
| bRed = []byte{'\033', '[', '3', '1', ';', '1', 'm'} | |||
| bGreen = []byte{'\033', '[', '3', '2', ';', '1', 'm'} | |||
| bYellow = []byte{'\033', '[', '3', '3', ';', '1', 'm'} | |||
| bBlue = []byte{'\033', '[', '3', '4', ';', '1', 'm'} | |||
| bMagenta = []byte{'\033', '[', '3', '5', ';', '1', 'm'} | |||
| bCyan = []byte{'\033', '[', '3', '6', ';', '1', 'm'} | |||
| bWhite = []byte{'\033', '[', '3', '7', ';', '1', 'm'} | |||
| reset = []byte{'\033', '[', '0', 'm'} | |||
| ) | |||
| var isTTY = terminal.IsTerminal(1) | |||
| // colorWrite | |||
| func cW(buf *bytes.Buffer, color []byte, s string, args ...interface{}) { | |||
| if isTTY { | |||
| buf.Write(color) | |||
| } | |||
| fmt.Fprintf(buf, s, args...) | |||
| if isTTY { | |||
| buf.Write(reset) | |||
| } | |||
| } | |||