diff --git a/go.mod b/go.mod index c0ee8b1..6900174 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.17 require ( github.com/fasthttp/session/v2 v2.4.4 + github.com/go-logfmt/logfmt v0.5.1 github.com/lestrrat-go/jwx v1.2.14 github.com/valyala/fasthttp v1.31.0 ) @@ -11,7 +12,7 @@ require ( require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect - github.com/goccy/go-json v0.8.1 // indirect + github.com/goccy/go-json v0.9.0 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/blackmagic v1.0.0 // indirect diff --git a/go.sum b/go.sum index b632042..3123a45 100644 --- a/go.sum +++ b/go.sum @@ -15,11 +15,14 @@ github.com/fasthttp/session/v2 v2.4.4 h1:xJOyPT8oIAeDXwCDnDyz52x44BxVRTCRNhYeMNj github.com/fasthttp/session/v2 v2.4.4/go.mod h1:qemCSBS6ozHzh5RVPMQzAFklcAhLqLtsoGTz1OPT5kM= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI= github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.9.0 h1:2flW7bkbrRgU8VuDi0WXDqTmPimjv1thfxkPe8sug+8= +github.com/goccy/go-json v0.9.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= diff --git a/logfmt.go b/logfmt.go new file mode 100644 index 0000000..a2512a5 --- /dev/null +++ b/logfmt.go @@ -0,0 +1,84 @@ +package middleware + +import ( + "io" + "os" + "time" + + "github.com/go-logfmt/logfmt" + http "github.com/valyala/fasthttp" +) + +type LogFmtConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // TODO(toby3d): allow select some tags + + // Output is a writer where logs in JSON format are written. + // Optional. Default value os.Stdout. + Output io.Writer +} + +var DefaultLogFmtConfig = LogFmtConfig{ + Skipper: DefaultSkipper, + Output: os.Stdout, +} + +func LogFmt() Interceptor { + c := DefaultLogFmtConfig + + return LogFmtWithConfig(c) +} + +func LogFmtWithConfig(config LogFmtConfig) Interceptor { + if config.Skipper == nil { + config.Skipper = DefaultLogFmtConfig.Skipper + } + + if config.Output == nil { + config.Output = DefaultLogFmtConfig.Output + } + + encoder := logfmt.NewEncoder(config.Output) + + return func(ctx *http.RequestCtx, next http.RequestHandler) { + next(ctx) + + encoder.EncodeKeyvals( + "bytes_in", len(ctx.Request.Body()), + "bytes_out", len(ctx.Response.Body()), + "error", ctx.Err(), + "host", ctx.Host(), + "id", ctx.ID(), + "latency", ctx.Time().Sub(ctx.ConnTime()).Nanoseconds(), + "latency_human", ctx.Time().Sub(ctx.ConnTime()).String(), + "method", ctx.Method(), + "path", ctx.Path(), + "protocol", ctx.Request.Header.Protocol(), + "referer", ctx.Referer(), + "remote_ip", ctx.RemoteIP(), + "status", ctx.Response.StatusCode(), + "time_rfc3339", ctx.Time().Format(time.RFC3339), + "time_rfc3339_nano", ctx.Time().Format(time.RFC3339Nano), + "time_unix", ctx.Time().Unix(), + "time_unix_nano", ctx.Time().UnixNano(), + "uri", ctx.URI(), + "user_agent", ctx.UserAgent(), + ) + ctx.Request.Header.VisitAllInOrder(func(key, value []byte) { + encoder.EncodeKeyval("header_"+string(key), value) + }) + ctx.QueryArgs().VisitAll(func(key, value []byte) { + encoder.EncodeKeyval("query_"+string(key), value) + }) + + if form, err := ctx.MultipartForm(); err == nil { + for k, v := range form.Value { + encoder.EncodeKeyval("form_"+k, v) + } + } + + encoder.EndRecord() + } +}