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
是我们预定义的类型,它是一个string
到interface
的映射。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
字段中,我们通过调用body
的String
方法,获取到了它内部的数据。
func (b *Buffer) String() string {
if b == nil {
return "<nil>"
}
return string(b.buf[b.off:])
}
关于最后的日志打印,我们将日志基本字段的格式和其中的值用fmt.Sprintf
组装成一个字符串,然后调用Logger
的Output
方法。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
}