go の net/http の middleware として rs/zerolog を使う

はじめに

go の net/http の middleware として rs/zerolog を使う方法をまとめます。
rs/zerolog は構造化されたログを柔軟なフォーマットで高速に吐き出すことができる Logger です。

github.com

middleware として rs/zerolog を使う

go の net/http の middleware と書きましたが go-chi/chi という net/http に準拠した薄い router frameworkを利用します。

framework を使うことは必須ではないのですが、少し規模が大きくなるといつも似たような framework を自作していることに気付き、最近は最初から使うことが増えました。

go-chi/chi で middleware を stack する server を作る

go-chi/chi の readme にある通り NewRouter() で router のインスタンスを作り、そこに middleware を stack していきます。

ZeroLogger と書かれている middleware を自作します。

package main

import (
    "net/http"

    "github.com/go-chi/chi"
    "github.com/go-chi/chi/middleware"
)

func main() {
    r := chi.NewRouter()
    r.Use(middleware.RequestID)
    r.Use(middleware.RealIP)
    r.Use(ZeroLogger) // ← ZeroLogger を作る

    r.Get("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("welcome"))
    })
    http.ListenAndServe(":5050", r)
}

net/http の middleware として rs/zerolog を利用する

net/http の middleware を実装するには以下のような func(http.Handler) http.Handler 型を持っている必要があります。

func Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        next.ServeHTTP(w, r)
    })
}

今回はこのサイトを参考に io.MultiWriter を使って標準出力とファイルにログを吐きます。

ファイルは natefinch/lumberjack を使ってログローテートしています。

chi/middleware にある NewWrapResponseWriter() を使うと簡単に status code を取得できるので、少し雑ですが level を分けて Logging もできます。

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "time"

    "github.com/go-chi/chi/middleware"
    "github.com/rs/zerolog"

    "gopkg.in/natefinch/lumberjack.v2"
)

func ZeroLogger(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        zerolog.TimeFieldFormat = time.RFC3339Nano

        writer := &lumberjack.Logger{
            Filename:   "server.log",
            MaxSize:    10,
            MaxBackups: 5, // days
            MaxAge:     1, // days
            Compress:   true,
        }

        writers := io.MultiWriter(os.Stdout, writer)
        log := zerolog.New(writers).With().Timestamp().Logger()

        start := time.Now()
        var requestID string
        if reqID := r.Context().Value(middleware.RequestIDKey); reqID != nil {
            requestID = reqID.(string)
        }

        ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
        next.ServeHTTP(ww, r)

        latency := time.Since(start)

        scheme := "http"
        if r.TLS != nil {
            scheme = "https"
        }

        switch {
        case ww.Status() < 500:
            log.Info().
                Time("received_time", start).
                Int64(fmt.Sprint("latency_time"), latency.Nanoseconds()).
                Int("status", ww.Status()).
                Str("scheme", scheme).
                Str("host", r.Host).
                Str("protocol", r.Proto).
                Str("method", r.Method).
                Str("remoteIP", r.RemoteAddr).
                Str("request-id", requestID).
                Str("request", r.RequestURI).
                Str("agent", r.UserAgent()).
                Int("bytes", ww.BytesWritten())
        default:
            log.Error().
                Time("received_time", start).
                Int64(fmt.Sprint("latency_time"), latency.Nanoseconds()).
                Int("status", ww.Status()).
                Str("scheme", scheme).
                Str("host", r.Host).
                Str("protocol", r.Proto).
                Str("method", r.Method).
                Str("remoteIP", r.RemoteAddr).
                Str("request-id", requestID).
                Str("request", r.RequestURI).
                Str("agent", r.UserAgent()).
                Int("bytes", ww.BytesWritten()).
        }

    })
}

まとめ

go の net/http の middleware として rs/zerolog を使う方法をまとめました。
次回は NewWrapResponseWriter() で status code を取得できるまでの流れを調べてみます。