feat: log middleware

This commit is contained in:
Daniel G. Taylor 2020-03-13 23:01:13 -07:00
parent 401e4d59ea
commit 77a0bc9072
No known key found for this signature in database
GPG key ID: 7BD6DC99C9A87E22
5 changed files with 77 additions and 39 deletions

View file

@ -21,6 +21,7 @@ Features include:
- Default middleware
- Automatic recovery from panics
- Automatically handle CORS headers
- Structured logging middleware using [Zap](https://github.com/uber-go/zap)
- Annotated Go types for input and output models
- Automatic input model validation
- Dependency injection for loggers, datastores, etc

4
go.mod
View file

@ -11,10 +11,12 @@ require (
github.com/gin-contrib/cors v1.3.1
github.com/gin-gonic/gin v1.5.0
github.com/gosimple/slug v1.9.0
github.com/rs/zerolog v1.18.0 // indirect
github.com/mattn/go-isatty v0.0.9
github.com/rs/zerolog v1.18.0
github.com/sergi/go-diff v1.1.0 // indirect
github.com/spf13/cobra v0.0.6
github.com/spf13/viper v1.4.0
github.com/stretchr/testify v1.5.1
github.com/xeipuuv/gojsonschema v1.2.0
go.uber.org/zap v1.10.0
)

4
go.sum
View file

@ -97,6 +97,7 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -159,8 +160,11 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

66
middelware.go Normal file
View file

@ -0,0 +1,66 @@
package huma
import (
"fmt"
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/mattn/go-isatty"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// LogMiddleware creates a new middleware to set a tagged `*zap.SugarLogger` in the
// Gin context under the `log` key. It debug logs request info. If the current
// terminal is a TTY, it will try to use colored output automatically.
func LogMiddleware() func(*gin.Context) {
var l *zap.Logger
var err error
if isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) {
config := zap.NewDevelopmentConfig()
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
l, err = config.Build()
} else {
l, err = zap.NewProduction()
}
if err != nil {
panic(err)
}
return func(c *gin.Context) {
start := time.Now()
contextLog := l.With(
zap.String("method", c.Request.Method),
zap.String("template", c.FullPath()),
zap.String("path", c.Request.URL.RequestURI()),
zap.String("ip", c.ClientIP()),
)
c.Set("log", contextLog.Sugar())
c.Next()
contextLog.Debug("Request",
zap.Int("status", c.Writer.Status()),
zap.Duration("duration", time.Since(start)),
)
}
}
// LogDependency returns a dependency that resolves to a `*zap.SugaredLogger`
// for the current request. This dependency *requires* the use of
// `LogMiddleware` and will error if the logger is not in the request context.
func LogDependency() *Dependency {
return &Dependency{
Depends: []*Dependency{ContextDependency(), OperationDependency()},
Value: func(c *gin.Context, op *Operation) (*zap.SugaredLogger, error) {
l, ok := c.Get("log")
if !ok {
return nil, fmt.Errorf("missing logger in context")
}
sl := l.(*zap.SugaredLogger).With("operation", op.ID)
sl.Desugar()
return sl, nil
},
}
}

View file

@ -132,12 +132,13 @@ type Router struct {
// NewRouter creates a new Huma router for handling API requests with
// default middleware and routes attached. This is equivalent to calling
// `NewRouterWithGin` with a new Gin instance with just the recovery and
// CORS (allowing all origins) middlewares.
// `NewRouterWithGin` with a new Gin instance with just the recovery,
// CORS (allowing all origins), and log middlewares.
func NewRouter(api *OpenAPI) *Router {
g := gin.New()
g.Use(gin.Recovery())
g.Use(cors.Default())
g.Use(LogMiddleware())
return NewRouterWithGin(g, api)
}
@ -192,42 +193,6 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.engine.ServeHTTP(w, req)
}
// Dependency registers a new dependency type to be injected into handler
// functions, e.g. for loggers, metrics, datastores, etc. Provide a value
// or a function to return a contextual value/error. Dependency functions
// can take their own dependencies. To prevent circular dependency loops,
// a function can only depend on previously defined dependencies.
//
// Some dependency types are built in:
// - `*gin.Context` the current Gin request execution context
// - `*huma.Operation` the current Huma operation
//
// // Register a global dependency like a datastore
// router.Dependency(&MyDB{...})
//
// // Register a contextual dependency like a logger
// router.Dependency(func (c *gin.Context) (*MyLogger, error) {
// return &MyLogger{Tags: []string{c.Request.RemoteAddr}}, nil
// })
//
// Then use the dependency in a handler function:
//
// router.Register(&huma.Operation{
// ...
// Handler: func(db *MyDB, log *MyLogger) *MyItem {
// item := db.GetItem("some-id")
// log.Info("Got item!")
// return item
// }
// })
//
// Panics on invalid input to force stop execution on service startup.
// func (r *Router) Dependency(f interface{}) {
// if err := r.deps.Add(f); err != nil {
// panic(err)
// }
// }
// Register a new operation.
func (r *Router) Register(op *Operation) {
// First, make sure the operation and handler make sense, as well as pre-