错误日志和访问日志是一个服务器必须支持的功能, 我们教程里使用的服务器到目前为止还没有这两个功能. 正好前两天也写了篇介绍 logrus 日志库的文章, 那么今天的文章里就给我们自己写的服务器加上错误日志和访问日志的功能. 在介绍添加访问日志的时候会介绍一种通过编写中间件获取 HTTP 响应的 StausCode 和 Body 的方法.
Go web 编程系列的每篇文章的源代码都打了对应版本的软件包, 供大家参考. 公众号中回复 gohttp11 获取本文源代码
初始化日志记录器
我们先来做一下初始化工作, 在项目里初始化记录错误日志和访问日志的记录器 Logger .
- // ./utils/vlog
- package vlog
- import (
- "github.com/sirupsen/logrus"
- "os"
- )
- var ErrorLog *logrus.Logger
- var AccessLog *logrus.Logger
- var errorLogFile = "./tmp/log/error.log"
- var accessLogFile = "./tmp/log/access.log"
- func init () {
- initErrorLog()
- initAccessLog()
- }
- func initErrorLog() {
- ErrorLog = logrus.New()
- ErrorLog.SetFormatter(&logrus.JSONFormatter{})
- file , err := os.OpenFile(errorLogFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0755)
- if err != nil {
- panic(err)
- }
- ErrorLog.SetOutput(file)
- }
- func initAccessLog() {
- AccessLog = logrus.New()
- AccessLog.SetFormatter(&logrus.JSONFormatter{})
- file , err := os.OpenFile(accessLogFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0755)
- if err != nil {
- panic(err)
- }
- AccessLog.SetOutput(file)
- }
我们新定义一个 package 在 init 函数中来初始化记录器, 这样服务器成功启动前就会初始化好记录器.
/tmp/log 这个目录要提前创建好, 执行 init 函数时会自动创建好 access.log 和 error.log .
添加错误日志
我们创建服务器使用的 net/http 包的 Server 类型中, 有一个 ErrorLog 字段供开发者设置记录错误日志用的记录器 Logger , 默认使用的是 log 包默认的记录器 (应该是系统的标准错误):
- type Server struct {
- Addr string // TCP address to listen on, ":http" if empty
- Handler Handler // handler to invoke, http.DefaultServeMux if nil
- ...
- // ErrorLog specifies an optional logger for errors accepting
- // connections, unexpected behavior from handlers, and
- // underlying FileSystem errors.
- // If nil, logging is done via the log package's standard logger.
- ErrorLog *log.Logger
- ...
- }
我们之前在创建服务器的时候自己实现了 Server 类型的对象, 那么现在要做的就是将上面初始化好的错误日志的记录器指定给 Server 的 ErrorLog 字段.
- func main() {
- ...
- // 将 `logrus` 的 Logger 转换为 io.Writer
- errorWriter := vlog.ErrorLog.Writer()
- // 记得关闭 io.Writer
- defer errorWriter.Close()
- server := &http.Server{
- Addr: ":8080",
- Handler: muxRouter,
- // 用记录器转换成的 io.Writer 创建 log.Logger
- ErrorLog: log.New(vlog.ErrorLog.Writer(), "", 0),
- }
- ...
- }
添加好错误日志的记录器后, 我们找个路由处理函数, 在里面故意制造运行时错误验证一下是否能记录到错误.
- func (*HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- ints := []int{0, 1, 2}
- fmt.Fprintf(w, "%v", ints[0:5])
- }
在上面处理函数中, 通过切片表达式越界故意制造了一个运行时错误, 打开 error.log 后能看到文件里已经记录到这个运行时错误及其 Stack trace
添加访问日志
和 Server 对象可以设置错误日志的记录器不一样, 访问日志只能是我们通过自己编写中间件的方式来实现了. 在记录访问日志的中间件里我们会记录 ip , method , path , query , request_body , status 和 response_body 这些个字段的内容.
status 和 response_body 两个字段来自请求对应的响应. 响应在 net/http 包里是用 http.ResponseWriter 接口表示的
- type ResponseWriter interface {
- Header() Header
- Write([]byte) (int, error)
- WriteHeader(statusCode int)
- }
接口本身以及 net/http 提供的实现都没有让我们进行读取的方法, 所以在编写的用于记录访问日志的中间件里需要对 net/http 库本身实现的 ResponseWriter 做一层包装.
利用 Go 语言结构体类型嵌套匿名类型后, 结构体拥有了被嵌套类型的所有导出字段和方法的特性, 我们可以很方便地对原来的 ResponseWriter 做一层包装, 然后只重新实现需要更改的方法即可:
- type ResponseWithRecorder struct {
- http.ResponseWriter
- statusCode int
- body bytes.Buffer
- }
- func (rec *ResponseWithRecorder) WriteHeader(statusCode int) {
- rec.ResponseWriter.WriteHeader(statusCode)
- rec.statusCode = statusCode
- }
- func (rec *ResponseWithRecorder) Write(d []byte) (n int, err error) {
- n, err = rec.ResponseWriter.Write(d)
- if err != nil {
- return
- }
- rec.body.Write(d)
- return
- }
定义好新的类型后我们重新实现了 WriteHeader 和 Write 方法, 在向原来的 ReponseWriter 中写入后也会向 ResponseWriteRecoder.statusCode 和 ResponseWriteRecoder.body 写入对应的数据. 这样我们就可以在中间件里通过这两个字段访问响应码和响应数据了.
记录访问日志的中间件定义如下:
- func AccessLogging (f http.Handler) http.Handler {
- // 创建一个新的 handler 包装 http.HandlerFunc
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- buf := new(bytes.Buffer)
- buf.ReadFrom(r.Body)
- logEntry := vlog.AccessLog.WithFields(logrus.Fields{
- "ip": r.RemoteAddr,
- "method": r.Method,
- "path": r.RequestURI,
- "query": r.URL.RawQuery,
- "request_body": buf.String(),
- })
- wc := &ResponseWithRecorder{
- ResponseWriter: w,
- statusCode: http.StatusOK,
- body: bytes.Buffer{},
- }
- // 调用下一个中间件或者最终的 handler 处理程序
- f.ServeHTTP(wc, r)
- defer logEntry.WithFields(logrus.Fields{
- "status": wc.statusCode,
- "response_body": wc.body.String(),
- }).Info()
- })
- }
在 Router 上应用创建好的 AccessLogging 中间件后, 就可以正常的记录服务器的访问日志了.
- // router/router.go
- func RegisterRoutes(r *mux.Router) {
- ...
- // apply Logging middleware
- r.Use(middleware.Logging(), middleware.AccessLogging)
- ...
- }
不过有两点需要注意一下
这里为了演示获取响应数据记录了 response_body 字段, 如果是接口响应内容记录下还可以, 但是如果是 html 还是不记录的为好.
初始化
ResponseWithRecorder
时默认设置了 statusCode 时因为, 服务器正确返回响应时不会显式调用 WriteHeader 方法, 只有在返回 NOT_FOUND 之类的错误的时候才会调用 WriteHeader 方法, 针对这种情况需要在初始化的时候把 statusCode 的默认值设置为
200
.
现在再访问服务器后打开 access.log 会看到刚刚的访问日志, 就能看到刚刚请求的 url , method , 客户端 IP 等信息了.
{"ip":"......","level":"info","method":"GET","msg":"","path":"/index/","query":"","request_body":"","response_body":"Hello World1","status":200,"time":"2020-03-26T04:21:46Z"}
注意: 文章只为说明演示方便, 获取 IP 的方法无法获取代理后的真实 IP, 请悉知.
来源: http://www.tuicool.com/articles/vABbYj3