【sduoj】记录访问日志

2021SC@SDUSC

引言

在 sduoj 项目的开发中,日志是一个难以回避的存在。虽然就算没有日志的记录系统也能运行,但是当系统出现异常的时候,我们便可以通过查看日志来寻找异常出现的地点,以便于达到快读定位、快速解决异常的目的。

我们需要对每个访问都进行记录,这时我们可以编写一个中间件,每个请求到来的时候,都会通过这个中间件。这样的话,我们便可比较容易的获取它的请求方法、响应码、方法调用开始时间、方法调用结束时间等信息。

源码

在写入流时,我们调用的是http.ResponseWriter(在下面代码中的gin.ResponseWriter中包含这个),但是我们我们无法直接获取方法返回的响应主体,我们需要编写一个针对访问日志的 Writer 结构体来解决这个问题。

type AccessLogWriter struct {
    
    
	gin.ResponseWriter
	body *bytes.Buffer
}

在下面的方法中,我们发现它内部调用了两个Write方法,也就是向两个不同的地方写数据,这样的话,在我们读取数据时候,直接从body中读就行了。

func (w AccessLogWriter) Write(p []byte) (int, error) {
    
    
	if n, err := w.body.Write(p); err != nil {
    
    
		return n, err
	}
	return w.ResponseWriter.Write(p)
}

以下就是记录访问日志的中间件,它返回一个形参为gin.Context的函数。具体业务的实现放在c.Next中,在此之前有一个beginTime记录请求开始时间,在此之后有一个endTime记录请求结束时间。

func AccessLog() gin.HandlerFunc {
    
    
	return func(c *gin.Context) {
    
    
		bodyWrite := AccessLogWriter{
    
    
			body:           bytes.NewBufferString(""),
			ResponseWriter: c.Writer,
		}
		c.Writer = bodyWrite

		beginTime := time.Now().Unix()
		c.Next()
		endTime := time.Now().Unix()

		fields := logger.Fields{
    
    
			"request":  c.Request.PostForm.Encode(),
			"response": bodyWrite.body.String(),
		}
		s := "access log: method %s, status_code: %d, " +
			"begin_time: %d, end_time: %d"
		global.Logger.WithFields(fields).Infof(s, 
			c.Request.Method, bodyWrite.Status(), beginTime, endTime)
	}
}

Fields是我们预定义的类型,它是一个stringinterface的映射。Logger是日志结构体,下文中的WithFields则是它具体的方法。

type Fields map[string]interface{
    
    }

type Logger struct {
    
    
	newLogger *log.Logger
	ctx       context.Context
	fields    Fields
	callers   []string
}

WithFidlds用来编写公共字段,它以一个Fields为入参,返回一个新的Logger。在方法内部,它先clone了一个新的Logger,并对f中的映射进行遍历,将其中的映射添加到ll.fields中。如果此前ll.fields为空的话,就新创建一个映射。

func (l *Logger) WithFields(f Fields) *Logger {
    
    
	ll := l.clone()
	if ll.fields == nil {
    
    
		ll.fields = make(Fields)
	}
	for k, v := range f {
    
    
		ll.fields[k] = v
	}
	return ll
}

request字段中,我们查找了它的 http 请求体,PostForm中包含了从 POST 请求体参数中解析的表格数据,它的Encode方法会对表格数据进行编码,将请求体数据编码成类似于a=1&b=2的格式。

func (v Values) Encode() string {
    
    
	if v == nil {
    
    
		return ""
	}
	var buf strings.Builder
	keys := make([]string, 0, len(v))
	for k := range v {
    
    
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
    
    
		vs := v[k]
		keyEscaped := QueryEscape(k)
		for _, v := range vs {
    
    
			if buf.Len() > 0 {
    
    
				buf.WriteByte('&')
			}
			buf.WriteString(keyEscaped)
			buf.WriteByte('=')
			buf.WriteString(QueryEscape(v))
		}
	}
	return buf.String()
}

response字段中,我们通过调用bodyString方法,获取到了它内部的数据。

func (b *Buffer) String() string {
    
    
	if b == nil {
    
    
		return "<nil>"
	}
	return string(b.buf[b.off:])
}

关于最后的日志打印,我们将日志基本字段的格式和其中的值用fmt.Sprintf组装成一个字符串,然后调用LoggerOutput方法。Output方法会调用l.JSONFormat方法,根据传入的日志级别和日志信息,进行日志内容的格式化。然后我们需要将格式化后的结果传入json.Marshal,并对结果进行 JSON 编码。最后,将编码后的结果转化成字符串后,通过l.newLogger打印出来就行了。

func (l *Logger) Infof(format string, v ...interface{
    
    }) {
    
    
	l.Output(LevelInfo, fmt.Sprintf(format, v...))
}

func (l *Logger) Output(level Level, message string) {
    
    
	body, _ := json.Marshal(l.JSONFormat(level, message))
	content := string(body)
	switch level {
    
    
	...
	case LevelInfo:
		l.newLogger.Print(content)
	...
	}
}

func (l *Logger) JSONFormat(level Level, message string) map[string]interface{
    
    } {
    
    
	data := make(Fields, len(l.fields)+4)
	data["level"] = level.String()
	data["time"] = time.Now().Local().UnixNano()
	data["message"] = message
	data["callers"] = l.callers
	if len(l.fields) > 0 {
    
    
		for k, v := range l.fields {
    
    
			if _, ok := data[k]; !ok {
    
    
				data[k] = v
			}
		}
	}

	return data
}

猜你喜欢

转载自blog.csdn.net/weixin_45922876/article/details/121154131