// Copyright 2014 Manu Martinez-Almeida. All rights reserved. // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. package gin import ( "fmt" "io" "net/http" "os" "strings" "time" "github.com/mattn/go-isatty" ) type consoleColorModeValue int const ( autoColor consoleColorModeValue = iota disableColor forceColor ) const ( green = "\033[97;42m" white = "\033[90;47m" yellow = "\033[90;43m" red = "\033[97;41m" blue = "\033[97;44m" magenta = "\033[97;45m" cyan = "\033[97;46m" reset = "\033[0m" ) var consoleColorMode = autoColor // LoggerConfig defines the config for Logger middleware. type LoggerConfig struct { // Optional. Default value is gin.defaultLogFormatter Formatter LogFormatter // Output is a writer where logs are written. // Optional. Default value is gin.DefaultWriter. Output io.Writer // SkipPaths is a url path array which logs are not written. // Optional. SkipPaths []string } // LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter type LogFormatter func(params LogFormatterParams) string // LogFormatterParams is the structure any formatter will be handed when time to log comes type LogFormatterParams struct { Request *http.Request // TimeStamp shows the time after the server returns a response. TimeStamp time.Time // StatusCode is HTTP response code. StatusCode int // Latency is how much time the server cost to process a certain request. Latency time.Duration // ClientIP equals Context's ClientIP method. ClientIP string // Method is the HTTP method given to the request. Method string // Path is a path the client requests. Path string // ErrorMessage is set if error has occurred in processing the request. ErrorMessage string // isTerm shows whether does gin's output descriptor refers to a terminal. isTerm bool // BodySize is the size of the Response Body BodySize int // Keys are the keys set on the request's context. Keys map[string]interface{} } // StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal. func (p *LogFormatterParams) StatusCodeColor() string { code := p.StatusCode switch { case code >= http.StatusOK && code < http.StatusMultipleChoices: return green case code >= http.StatusMultipleChoices && code < http.StatusBadRequest: return white case code >= http.StatusBadRequest && code < http.StatusInternalServerError: return yellow default: return red } } // MethodColor is the ANSI color for appropriately logging http method to a terminal. func (p *LogFormatterParams) MethodColor() string { method := p.Method switch method { case http.MethodGet: return blue case http.MethodPost: return cyan case http.MethodPut: return yellow case http.MethodDelete: return red case http.MethodPatch: return green case http.MethodHead: return magenta case http.MethodOptions: return white default: return reset } } // ResetColor resets all escape attributes. func (p *LogFormatterParams) ResetColor() string { return reset } // IsOutputColor indicates whether can colors be outputted to the log. func (p *LogFormatterParams) IsOutputColor() bool { return consoleColorMode == forceColor || (consoleColorMode == autoColor && p.isTerm) } // defaultLogFormatter is the default log format function Logger middleware uses. var defaultLogFormatter = func(param LogFormatterParams) string { var statusColor, methodColor, resetColor string if param.IsOutputColor() { statusColor = param.StatusCodeColor() methodColor = param.MethodColor() resetColor = param.ResetColor() } if param.Latency > time.Minute { param.Latency = param.Latency.Truncate(time.Second) } return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s", param.TimeStamp.Format("2006/01/02 - 15:04:05"), statusColor, param.StatusCode, resetColor, param.Latency, param.ClientIP, methodColor, param.Method, resetColor, param.Path, param.ErrorMessage, ) } // DisableConsoleColor disables color output in the console. func DisableConsoleColor() { consoleColorMode = disableColor } // ForceConsoleColor force color output in the console. func ForceConsoleColor() { consoleColorMode = forceColor } // ErrorLogger returns a handlerfunc for any error type. func ErrorLogger() HandlerFunc { return ErrorLoggerT(ErrorTypeAny) } // ErrorLoggerT returns a handlerfunc for a given error type. func ErrorLoggerT(typ ErrorType) HandlerFunc { return func(c *Context) { c.Next() errors := c.Errors.ByType(typ) if len(errors) > 0 { c.JSON(-1, errors) } } } // Logger instances a Logger middleware that will write the logs to gin.DefaultWriter. // By default gin.DefaultWriter = os.Stdout. func Logger() HandlerFunc { return LoggerWithConfig(LoggerConfig{}) } // LoggerWithFormatter instance a Logger middleware with the specified log format function. func LoggerWithFormatter(f LogFormatter) HandlerFunc { return LoggerWithConfig(LoggerConfig{ Formatter: f, }) } // LoggerWithWriter instance a Logger middleware with the specified writer buffer. // Example: os.Stdout, a file opened in write mode, a socket... func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc { return LoggerWithConfig(LoggerConfig{ Output: out, SkipPaths: notlogged, }) } // LoggerWithConfig instance a Logger middleware with config. func LoggerWithConfig(conf LoggerConfig) HandlerFunc { formatter := conf.Formatter if formatter == nil { formatter = defaultLogFormatter } out := conf.Output if out == nil { out = DefaultWriter } notlogged := conf.SkipPaths isTerm := true if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) { isTerm = false } var skip map[string]struct{} var skipSub bool if length := len(notlogged); length > 0 { skip = make(map[string]struct{}, length) for _, path := range notlogged { skip[path] = struct{}{} if strings.HasSuffix(path, "/") { skipSub = true } } } return func(c *Context) { // Start timer start := time.Now() path := c.Request.URL.Path raw := c.Request.URL.RawQuery // Process request c.Next() // Log only when path is not being skipped if _, ok := skip[path]; !ok && (!skipSub || !willSkipLog(path, skip)) { param := LogFormatterParams{ Request: c.Request, isTerm: isTerm, Keys: c.Keys, } // Stop timer param.TimeStamp = time.Now() param.Latency = param.TimeStamp.Sub(start) param.ClientIP = c.ClientIP() param.Method = c.Request.Method param.StatusCode = c.Writer.Status() param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String() param.BodySize = c.Writer.Size() if raw != "" { path = path + "?" + raw } param.Path = path fmt.Fprint(out, formatter(param)) } } } // willSkipLog if skip path is "/attachments/", url like "/attachments/producthunt/*" will be skipped func willSkipLog(path string, skip map[string]struct{}) bool { for p := range skip { if strings.HasPrefix(path, p[:len(p)-1]) { return true } } return false }