Gin源码分析 – 中间件 – Logger

1 介绍

本文将对Gin的内置中间件Logger进行分析,Gin在创建默认的Enginer时将添加到全局中间件中,代码如下。

// Default returns an Engine instance 
// with the Logger and Recovery middleware already attached.
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

2 Logger

有四种创建Logger中间件的形式,分别是:

(1)func Logger() HandlerFunc,这个是最基本的形式,代码如下。在内部使用LoggerWithConfig使用默认的配置创建中间件,通过注释可以看出,通过该方法创建的中间件将日志写到gin.DefaultWriter,默认情况下是os.Stdout。

// 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{})
}

(2)func LoggerWithFormatter(f LogFormatter) HandlerFunc,代码如下,同样使用LoggerWithConfig完成中间件的创建,使用用户提供的格式记录日志。

// LoggerWithFormatter instance a Logger middleware 
// with the specified log format function.
func LoggerWithFormatter(f LogFormatter) HandlerFunc {
	return LoggerWithConfig(LoggerConfig{
		Formatter: f,
	})
}

(3)func LoggerWithWriter(out io.Writer, notlogged …string) HandlerFunc,代码如下,将日志就到特定的Writer中,同时忽略相应的路径。

// 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,
	})
}

(4)func LoggerWithConfig(conf LoggerConfig) HandlerFunc,这个函数可以使用更加详细的配置用于创建中间件,是本中间件中最复杂的一个函数,放到最后在进行分析。

3 基础数据结构

3.1 LoggerConfig

// 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
}

(1)Formatter,日志输出格式,默认值是gin.defaultLogFormatter;

(2)Output,日志输出器,默认是gin.DefaultWriter,在文件mode.go中,代码如下:

// DefaultWriter is the default io.Writer used by Gin for debug output 
// and middleware output like Logger() or Recovery().
// Note that both Logger and Recovery provides 
// custom ways to configure their
// output io.Writer.
// To support coloring in Windows use:
// 		import "github.com/mattn/go-colorable"
// 		gin.DefaultWriter = colorable.NewColorableStdout()
var DefaultWriter io.Writer = os.Stdout

(3)SkipPaths,是一个忽略就的路径数组,默认是空。

LoggerConfig的创建在函数LoggerWithConfig的开始部分,参加如下代码:

// LoggerWithConfig instance a Logger middleware with config.
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {

  // 如果为空,则formatter设置为defaultLogFormatter
	formatter := conf.Formatter
	if formatter == nil {
		formatter = defaultLogFormatter
	}

  // 如果为空, 则out设置为DefaultWriter
	out := conf.Output
	if out == nil {
		out = DefaultWriter
	}

	notlogged := conf.SkipPaths

3.2 LogFormatter

日式格式化器是一个方法,方法的定义如下:

// 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{}
}
  • Request,HTTP的请求体;
  • TimeStame,返回响应体Response的时间;
  • StatusCode,返回的HTTP状态码;
  • Latency,处理HTTP请求的用时;
  • ClientIP,客户端IP;
  • Method,HTTP请求方法;
  • Path,请求路径;
  • ErrorMessage,错误信息;
  • isTerm,是否是将日志输出到终端;
  • BodySize,响应体的大小;
  • 请求上下文中的元数据。

下面介绍一些进行格式化日志的辅助函数。

(1)StatusCodeColor,根据HTTP响应码设置相应的显示颜色。

// 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
	}
}

该中间件中定义了一组常用的颜色,代码如下:

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"
)

(2)MethodColor,根据HTTP请求类型设置响应的显示颜色。

// 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
	}
}

(3)颜色控制方法

  • RestColor,重新颜色;
  • IsOutputColor,判断是否能够已有色模式进行输出,有自动颜色输出模式(autoColor)和强制颜色输出模式(autoColor),在自动模式下如果输出终端是控制台则进行有颜色输出,否则无颜色。
  • DisableConsoleColor,禁止控制台输出颜色
  • ForceConsoleColor,控制台输出颜色
type consoleColorModeValue int

const (
	autoColor consoleColorModeValue = iota
	disableColor
	forceColor
)

var consoleColorMode = autoColor

// 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)
}

// DisableConsoleColor disables color output in the console.
func DisableConsoleColor() {
	consoleColorMode = disableColor
}

// ForceConsoleColor force color output in the console.
func ForceConsoleColor() {
	consoleColorMode = forceColor
}

3.3 defaultLogFormatter

下面开始分析defaultLogFormatter,该方法提供了默认的日志格式化操作,具体代码如下:

// 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()
	}

  // 如何大于1分钟,则将时间换算为整秒
	if param.Latency > time.Minute {
		// Truncate in a golang < 1.8 safe way
		param.Latency = param.Latency - param.Latency%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,
	)
}

4 LoggerWithConfig

完成上述的各种铺垫后让我们开始分析LoggerWithConfig方法。

(1)这部分已经分析过了,主要是获取Formatter,Output和SkipPaths。

// 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

(2)这部分主要是判读当前的输出是否是终端。

	isTerm := true

	if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" ||
		(!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) {
		isTerm = false
	}

(3)将SkipPaths保存到map中,方便后续的过滤,把过滤的Path作为map的key进行存储。

	var skip map[string]struct{}

	if length := len(notlogged); length > 0 {
		skip = make(map[string]struct{}, length)

		for _, path := range notlogged {
			skip[path] = struct{}{}
		}
	}

(4)后面的具体的中间件处理函数,首先是获取开始时间,请求路径和请求参数

	return func(c *Context) {
		// Start timer
		start := time.Now()
		path := c.Request.URL.Path
		raw := c.Request.URL.RawQuery

		// Process request
		c.Next()

(5)计算处理时间,获取请求的路径,然后生成日志字符串,并输出到out中。

		// Log only when path is not being skipped
		if _, ok := skip[path]; !ok {
			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))
		}

5 总结

本文将逐行的对该中间件的实现进行分析,Logger中间件在logger.go文件中实现,一共272行非常适合新手学习。