switch to chi v4

This commit is contained in:
Umputun
2019-04-04 02:17:12 -05:00
parent 0f433722da
commit 70134578e1
32 changed files with 558 additions and 490 deletions
+1 -1
View File
@@ -262,7 +262,7 @@ func (s *Rest) routes() chi.Router {
rauth.Put("/comment/{id}", s.updateCommentCtrl)
rauth.Post("/comment", s.createCommentCtrl)
rauth.With(rejectAnonUser).Put("/vote/{id}", s.voteCtrl)
rauth.Post("/deleteme", s.deleteMeCtrl)
rauth.With(rejectAnonUser).Post("/deleteme", s.deleteMeCtrl)
})
rapi.Group(func(rauth chi.Router) {
+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/didip/tollbooth v4.0.0+incompatible
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8
github.com/go-chi/chi v3.3.2+incompatible
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-chi/cors v1.0.0
github.com/go-chi/render v1.0.0
github.com/go-pkgz/auth v0.5.0
+2
View File
@@ -22,6 +22,8 @@ github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7a
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-chi/chi v3.3.2+incompatible h1:uQNcQN3NsV1j4ANsPh42P4ew4t6rnRbJb8frvpp31qQ=
github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/cors v1.0.0 h1:e6x8k7uWbUwYs+aXDoiUzeQFT6l0cygBYyNhD7/1Tg0=
github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
github.com/go-chi/render v1.0.0 h1:cLJlkaTB4xfx5rWhtoB0BSXsXVJKWFqv08Y3cR1bZKA=
+9 -9
View File
@@ -1,18 +1,18 @@
language: go
go:
- 1.7.x
- 1.8.x
- 1.9.x
install:
- go get -u golang.org/x/tools/cmd/goimports
- go get -u github.com/golang/lint/golint
- 1.10.x
- 1.11.x
- 1.12.x
script:
- go get -d -t ./...
- go vet ./...
- golint ./...
- go test ./...
- >
goimports -d -e ./ | grep '.*' && { echo; echo "Aborting due to non-empty goimports output."; exit 1; } || :
go_version=$(go version);
if [ ${go_version:13:4} = "1.12" ]; then
go get -u golang.org/x/tools/cmd/goimports;
goimports -d -e ./ | grep '.*' && { echo; echo "Aborting due to non-empty goimports output."; exit 1; } || :;
fi
+23
View File
@@ -1,5 +1,28 @@
# Changelog
## v4.0.0 (2019-01-10)
- chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8
- router: respond with 404 on router with no routes (#362)
- router: additional check to ensure wildcard is at the end of a url pattern (#333)
- middleware: deprecate use of http.CloseNotifier (#347)
- middleware: fix RedirectSlashes to include query params on redirect (#334)
- History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0
## v3.3.4 (2019-01-07)
- Minor middleware improvements. No changes to core library/router. Moving v3 into its
- own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11
- History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4
## v3.3.3 (2018-08-27)
- Minor release
- See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3
## v3.3.2 (2017-12-22)
- Support to route trailing slashes on mounted sub-routers (#281)
+27 -32
View File
@@ -3,7 +3,7 @@
[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis]
`chi` is a lightweight, idiomatic and composable router for building Go 1.7+ HTTP services. It's
`chi` is a lightweight, idiomatic and composable router for building Go HTTP services. It's
especially good at helping you write large REST API services that are kept maintainable as your
project grows and changes. `chi` is built on the new `context` package introduced in Go 1.7 to
handle signaling, cancelation and request-scoped values across a handler chain.
@@ -31,18 +31,12 @@ included some useful/optional subpackages: [middleware](/middleware), [render](h
* **Context control** - built on new `context` package, providing value chaining, cancelations and timeouts
* **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91))
* **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown
* **No external dependencies** - plain ol' Go 1.7+ stdlib + net/http
* **No external dependencies** - plain ol' Go stdlib + net/http
## Examples
* [rest](https://github.com/go-chi/chi/blob/master/_examples/rest/main.go) - REST APIs made easy, productive and maintainable
* [logging](https://github.com/go-chi/chi/blob/master/_examples/logging/main.go) - Easy structured logging for any backend
* [limits](https://github.com/go-chi/chi/blob/master/_examples/limits/main.go) - Timeouts and Throttling
* [todos-resource](https://github.com/go-chi/chi/blob/master/_examples/todos-resource/main.go) - Struct routers/handlers, an example of another code layout style
* [versions](https://github.com/go-chi/chi/blob/master/_examples/versions/main.go) - Demo of `chi/render` subpkg
* [fileserver](https://github.com/go-chi/chi/blob/master/_examples/fileserver/main.go) - Easily serve static files
* [graceful](https://github.com/go-chi/chi/blob/master/_examples/graceful/main.go) - Graceful context signaling and server shutdown
See [_examples/](https://github.com/go-chi/chi/blob/master/_examples/) for a variety of examples.
**As easy as:**
@@ -70,8 +64,8 @@ Here is a little preview of how routing looks like with chi. Also take a look at
in JSON ([routes.json](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.json)) and in
Markdown ([routes.md](https://github.com/go-chi/chi/blob/master/_examples/rest/routes.md)).
I highly recommend reading the source of the [examples](#examples) listed above, they will show you all the features
of chi and serve as a good form of documentation.
I highly recommend reading the source of the [examples](https://github.com/go-chi/chi/blob/master/_examples/) listed
above, they will show you all the features of chi and serve as a good form of documentation.
```go
import (
@@ -232,7 +226,7 @@ type Router interface {
}
// Routes interface adds two methods for router traversal, which is also
// used by the `docgen` subpackage to generation documentation for Routers.
// used by the github.com/go-chi/docgen package to generate documentation for Routers.
type Routes interface {
// Routes returns the routing tree in an easily traversable structure.
Routes() []Route
@@ -261,7 +255,7 @@ friendly with any middleware in the community. This offers much better extensibi
of packages and is at the heart of chi's purpose.
Here is an example of a standard net/http middleware handler using the new request context
available in Go 1.7+. This middleware sets a hypothetical user identifier on the request
available in Go. This middleware sets a hypothetical user identifier on the request
context and calls the next handler in the chain.
```go
@@ -347,6 +341,7 @@ Please see https://github.com/go-chi for additional packages.
| package | description |
|:---------------------------------------------------|:-------------------------------------------------------------
| [cors](https://github.com/go-chi/cors) | Cross-origin resource sharing (CORS) |
| [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime |
| [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication |
| [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing |
| [httpcoala](https://github.com/go-chi/httpcoala) | HTTP request coalescer |
@@ -374,33 +369,33 @@ and..
The benchmark suite: https://github.com/pkieltyka/go-http-routing-benchmark
Results as of Aug 31, 2017 on Go 1.9.0
Results as of Jan 9, 2019 with Go 1.11.4 on Linux X1 Carbon laptop
```shell
BenchmarkChi_Param 3000000 607 ns/op 432 B/op 3 allocs/op
BenchmarkChi_Param5 2000000 935 ns/op 432 B/op 3 allocs/op
BenchmarkChi_Param20 1000000 1944 ns/op 432 B/op 3 allocs/op
BenchmarkChi_ParamWrite 2000000 664 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GithubStatic 2000000 627 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GithubParam 2000000 847 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GithubAll 10000 175556 ns/op 87700 B/op 609 allocs/op
BenchmarkChi_GPlusStatic 3000000 566 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GPlusParam 2000000 652 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GPlus2Params 2000000 767 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GPlusAll 200000 9794 ns/op 5616 B/op 39 allocs/op
BenchmarkChi_ParseStatic 3000000 590 ns/op 432 B/op 3 allocs/op
BenchmarkChi_ParseParam 2000000 656 ns/op 432 B/op 3 allocs/op
BenchmarkChi_Parse2Params 2000000 715 ns/op 432 B/op 3 allocs/op
BenchmarkChi_ParseAll 100000 18045 ns/op 11232 B/op 78 allocs/op
BenchmarkChi_StaticAll 10000 108871 ns/op 67827 B/op 471 allocs/op
BenchmarkChi_Param 3000000 475 ns/op 432 B/op 3 allocs/op
BenchmarkChi_Param5 2000000 696 ns/op 432 B/op 3 allocs/op
BenchmarkChi_Param20 1000000 1275 ns/op 432 B/op 3 allocs/op
BenchmarkChi_ParamWrite 3000000 505 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GithubStatic 3000000 508 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GithubParam 2000000 669 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GithubAll 10000 134627 ns/op 87699 B/op 609 allocs/op
BenchmarkChi_GPlusStatic 3000000 402 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GPlusParam 3000000 500 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GPlus2Params 3000000 586 ns/op 432 B/op 3 allocs/op
BenchmarkChi_GPlusAll 200000 7237 ns/op 5616 B/op 39 allocs/op
BenchmarkChi_ParseStatic 3000000 408 ns/op 432 B/op 3 allocs/op
BenchmarkChi_ParseParam 3000000 488 ns/op 432 B/op 3 allocs/op
BenchmarkChi_Parse2Params 3000000 551 ns/op 432 B/op 3 allocs/op
BenchmarkChi_ParseAll 100000 13508 ns/op 11232 B/op 78 allocs/op
BenchmarkChi_StaticAll 20000 81933 ns/op 67826 B/op 471 allocs/op
```
Comparison with other routers: https://gist.github.com/pkieltyka/c089f309abeb179cfc4deaa519956d8c
Comparison with other routers: https://gist.github.com/pkieltyka/123032f12052520aaccab752bd3e78cc
NOTE: the allocs in the benchmark above are from the calls to http.Request's
`WithContext(context.Context)` method that clones the http.Request, sets the `Context()`
on the duplicated (alloc'd) request and returns it the new request object. This is just
how setting context on a request in Go 1.7+ works.
how setting context on a request in Go works.
## Credits
+7 -7
View File
@@ -84,13 +84,13 @@ func (x *Context) URLParam(key string) string {
//
// For example,
//
// func Instrument(next http.Handler) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// next.ServeHTTP(w, r)
// routePattern := chi.RouteContext(r.Context()).RoutePattern()
// measure(w, r, routePattern)
// })
// }
// func Instrument(next http.Handler) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// next.ServeHTTP(w, r)
// routePattern := chi.RouteContext(r.Context()).RoutePattern()
// measure(w, r, routePattern)
// })
// }
func (x *Context) RoutePattern() string {
routePattern := strings.Join(x.RoutePatterns, "")
return strings.Replace(routePattern, "/*/", "/", -1)
-42
View File
@@ -1,42 +0,0 @@
// +build go1.7,!go1.8
package middleware
import (
"context"
"net/http"
)
// CloseNotify is a middleware that cancels ctx when the underlying
// connection has gone away. It can be used to cancel long operations
// on the server when the client disconnects before the response is ready.
//
// Note: this behaviour is standard in Go 1.8+, so the middleware does nothing
// on 1.8+ and exists just for backwards compatibility.
func CloseNotify(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
cn, ok := w.(http.CloseNotifier)
if !ok {
panic("chi/middleware: CloseNotify expects http.ResponseWriter to implement http.CloseNotifier interface")
}
closeNotifyCh := cn.CloseNotify()
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
go func() {
select {
case <-ctx.Done():
return
case <-closeNotifyCh:
cancel()
return
}
}()
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
-17
View File
@@ -1,17 +0,0 @@
// +build go1.8 appengine
package middleware
import (
"net/http"
)
// CloseNotify is a middleware that cancels ctx when the underlying
// connection has gone away. It can be used to cancel long operations
// on the server when the client disconnects before the response is ready.
//
// Note: this behaviour is standard in Go 1.8+, so the middleware does nothing
// on 1.8+ and exists just for backwards compatibility.
func CloseNotify(next http.Handler) http.Handler {
return next
}
+167 -104
View File
@@ -11,24 +11,98 @@ import (
"strings"
)
type encoding int
var encoders = map[string]EncoderFunc{}
const (
encodingNone encoding = iota
encodingGzip
encodingDeflate
)
var encodingPrecedence = []string{"br", "gzip", "deflate"}
func init() {
// TODO:
// lzma: Opera.
// sdch: Chrome, Android. Gzip output + dictionary header.
// br: Brotli, see https://github.com/go-chi/chi/pull/326
// TODO: Exception for old MSIE browsers that can't handle non-HTML?
// https://zoompf.com/blog/2012/02/lose-the-wait-http-compression
SetEncoder("gzip", encoderGzip)
// HTTP 1.1 "deflate" (RFC 2616) stands for DEFLATE data (RFC 1951)
// wrapped with zlib (RFC 1950). The zlib wrapper uses Adler-32
// checksum compared to CRC-32 used in "gzip" and thus is faster.
//
// But.. some old browsers (MSIE, Safari 5.1) incorrectly expect
// raw DEFLATE data only, without the mentioned zlib wrapper.
// Because of this major confusion, most modern browsers try it
// both ways, first looking for zlib headers.
// Quote by Mark Adler: http://stackoverflow.com/a/9186091/385548
//
// The list of browsers having problems is quite big, see:
// http://zoompf.com/blog/2012/02/lose-the-wait-http-compression
// https://web.archive.org/web/20120321182910/http://www.vervestudios.co/projects/compression-tests/results
//
// That's why we prefer gzip over deflate. It's just more reliable
// and not significantly slower than gzip.
SetEncoder("deflate", encoderDeflate)
// NOTE: Not implemented, intentionally:
// case "compress": // LZW. Deprecated.
// case "bzip2": // Too slow on-the-fly.
// case "zopfli": // Too slow on-the-fly.
// case "xz": // Too slow on-the-fly.
}
// An EncoderFunc is a function that wraps the provided ResponseWriter with a
// streaming compression algorithm and returns it.
//
// In case of failure, the function should return nil.
type EncoderFunc func(w http.ResponseWriter, level int) io.Writer
// SetEncoder can be used to set the implementation of a compression algorithm.
//
// The encoding should be a standardised identifier. See:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
//
// For example, add the Brotli algortithm:
//
// import brotli_enc "gopkg.in/kothar/brotli-go.v0/enc"
//
// middleware.SetEncoder("br", func(w http.ResponseWriter, level int) io.Writer {
// params := brotli_enc.NewBrotliParams()
// params.SetQuality(level)
// return brotli_enc.NewBrotliWriter(params, w)
// })
func SetEncoder(encoding string, fn EncoderFunc) {
encoding = strings.ToLower(encoding)
if encoding == "" {
panic("the encoding can not be empty")
}
if fn == nil {
panic("attempted to set a nil encoder function")
}
encoders[encoding] = fn
var e string
for _, v := range encodingPrecedence {
if v == encoding {
e = v
}
}
if e == "" {
encodingPrecedence = append([]string{e}, encodingPrecedence...)
}
}
var defaultContentTypes = map[string]struct{}{
"text/html": struct{}{},
"text/css": struct{}{},
"text/plain": struct{}{},
"text/javascript": struct{}{},
"application/javascript": struct{}{},
"application/x-javascript": struct{}{},
"application/json": struct{}{},
"application/atom+xml": struct{}{},
"application/rss+xml": struct{}{},
"text/html": {},
"text/css": {},
"text/plain": {},
"text/javascript": {},
"application/javascript": {},
"application/x-javascript": {},
"application/json": {},
"application/atom+xml": {},
"application/rss+xml": {},
"image/svg+xml": {},
}
// DefaultCompress is a middleware that compresses response
@@ -43,6 +117,11 @@ func DefaultCompress(next http.Handler) http.Handler {
// body of a given content types to a data format based
// on Accept-Encoding request header. It uses a given
// compression level.
//
// NOTE: make sure to set the Content-Type header on your response
// otherwise this middleware will not compress the response body. For ex, in
// your handler you should set w.Header().Set("Content-Type", http.DetectContentType(yourBody))
// or set it manually.
func Compress(level int, types ...string) func(next http.Handler) http.Handler {
contentTypes := defaultContentTypes
if len(types) > 0 {
@@ -54,159 +133,143 @@ func Compress(level int, types ...string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
mcw := &maybeCompressResponseWriter{
encoder, encoding := selectEncoder(r.Header)
cw := &compressResponseWriter{
ResponseWriter: w,
w: w,
contentTypes: contentTypes,
encoding: selectEncoding(r.Header),
encoder: encoder,
encoding: encoding,
level: level,
}
defer mcw.Close()
defer cw.Close()
next.ServeHTTP(mcw, r)
next.ServeHTTP(cw, r)
}
return http.HandlerFunc(fn)
}
}
func selectEncoding(h http.Header) encoding {
enc := h.Get("Accept-Encoding")
func selectEncoder(h http.Header) (EncoderFunc, string) {
header := h.Get("Accept-Encoding")
switch {
// TODO:
// case "br": // Brotli, experimental. Firefox 2016, to-be-in Chromium.
// case "lzma": // Opera.
// case "sdch": // Chrome, Android. Gzip output + dictionary header.
// Parse the names of all accepted algorithms from the header.
accepted := strings.Split(strings.ToLower(header), ",")
case strings.Contains(enc, "gzip"):
// TODO: Exception for old MSIE browsers that can't handle non-HTML?
// https://zoompf.com/blog/2012/02/lose-the-wait-http-compression
return encodingGzip
case strings.Contains(enc, "deflate"):
// HTTP 1.1 "deflate" (RFC 2616) stands for DEFLATE data (RFC 1951)
// wrapped with zlib (RFC 1950). The zlib wrapper uses Adler-32
// checksum compared to CRC-32 used in "gzip" and thus is faster.
//
// But.. some old browsers (MSIE, Safari 5.1) incorrectly expect
// raw DEFLATE data only, without the mentioned zlib wrapper.
// Because of this major confusion, most modern browsers try it
// both ways, first looking for zlib headers.
// Quote by Mark Adler: http://stackoverflow.com/a/9186091/385548
//
// The list of browsers having problems is quite big, see:
// http://zoompf.com/blog/2012/02/lose-the-wait-http-compression
// https://web.archive.org/web/20120321182910/http://www.vervestudios.co/projects/compression-tests/results
//
// That's why we prefer gzip over deflate. It's just more reliable
// and not significantly slower than gzip.
return encodingDeflate
// NOTE: Not implemented, intentionally:
// case "compress": // LZW. Deprecated.
// case "bzip2": // Too slow on-the-fly.
// case "zopfli": // Too slow on-the-fly.
// case "xz": // Too slow on-the-fly.
// Find supported encoder by accepted list by precedence
for _, name := range encodingPrecedence {
if fn, ok := encoders[name]; ok && matchAcceptEncoding(accepted, name) {
return fn, name
}
}
return encodingNone
// No encoder found to match the accepted encoding
return nil, ""
}
type maybeCompressResponseWriter struct {
func matchAcceptEncoding(accepted []string, encoding string) bool {
for _, v := range accepted {
if strings.Index(v, encoding) >= 0 {
return true
}
}
return false
}
type compressResponseWriter struct {
http.ResponseWriter
w io.Writer
encoding encoding
encoder EncoderFunc
encoding string
contentTypes map[string]struct{}
level int
wroteHeader bool
}
func (w *maybeCompressResponseWriter) WriteHeader(code int) {
if w.wroteHeader {
func (cw *compressResponseWriter) WriteHeader(code int) {
if cw.wroteHeader {
return
}
w.wroteHeader = true
defer w.ResponseWriter.WriteHeader(code)
cw.wroteHeader = true
defer cw.ResponseWriter.WriteHeader(code)
// Already compressed data?
if w.ResponseWriter.Header().Get("Content-Encoding") != "" {
if cw.Header().Get("Content-Encoding") != "" {
return
}
// The content-length after compression is unknown
w.ResponseWriter.Header().Del("Content-Length")
// Parse the first part of the Content-Type response header.
contentType := ""
parts := strings.Split(w.ResponseWriter.Header().Get("Content-Type"), ";")
parts := strings.Split(cw.Header().Get("Content-Type"), ";")
if len(parts) > 0 {
contentType = parts[0]
}
// Is the content type compressable?
if _, ok := w.contentTypes[contentType]; !ok {
if _, ok := cw.contentTypes[contentType]; !ok {
return
}
// Select the compress writer.
switch w.encoding {
case encodingGzip:
gw, err := gzip.NewWriterLevel(w.ResponseWriter, w.level)
if err != nil {
w.w = w.ResponseWriter
return
}
w.w = gw
w.ResponseWriter.Header().Set("Content-Encoding", "gzip")
if cw.encoder != nil && cw.encoding != "" {
if wr := cw.encoder(cw.ResponseWriter, cw.level); wr != nil {
cw.w = wr
cw.Header().Set("Content-Encoding", cw.encoding)
case encodingDeflate:
dw, err := flate.NewWriter(w.ResponseWriter, w.level)
if err != nil {
w.w = w.ResponseWriter
return
// The content-length after compression is unknown
cw.Header().Del("Content-Length")
}
w.w = dw
w.ResponseWriter.Header().Set("Content-Encoding", "deflate")
}
}
func (w *maybeCompressResponseWriter) Write(p []byte) (int, error) {
if !w.wroteHeader {
w.WriteHeader(http.StatusOK)
func (cw *compressResponseWriter) Write(p []byte) (int, error) {
if !cw.wroteHeader {
cw.WriteHeader(http.StatusOK)
}
return w.w.Write(p)
return cw.w.Write(p)
}
func (w *maybeCompressResponseWriter) Flush() {
if f, ok := w.w.(http.Flusher); ok {
func (cw *compressResponseWriter) Flush() {
if f, ok := cw.w.(http.Flusher); ok {
f.Flush()
}
}
func (w *maybeCompressResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := w.w.(http.Hijacker); ok {
func (cw *compressResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := cw.w.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, errors.New("chi/middleware: http.Hijacker is unavailable on the writer")
}
func (w *maybeCompressResponseWriter) CloseNotify() <-chan bool {
if cn, ok := w.w.(http.CloseNotifier); ok {
return cn.CloseNotify()
func (cw *compressResponseWriter) Push(target string, opts *http.PushOptions) error {
if ps, ok := cw.w.(http.Pusher); ok {
return ps.Push(target, opts)
}
// If the underlying writer does not implement http.CloseNotifier, return
// a channel that never receives a value. The semantics here is that the
// client never disconnnects before the request is processed by the
// http.Handler, which is close enough to the default behavior (when
// CloseNotify() is not even called).
return make(chan bool, 1)
return errors.New("chi/middleware: http.Pusher is unavailable on the writer")
}
func (w *maybeCompressResponseWriter) Close() error {
if c, ok := w.w.(io.WriteCloser); ok {
func (cw *compressResponseWriter) Close() error {
if c, ok := cw.w.(io.WriteCloser); ok {
return c.Close()
}
return errors.New("chi/middleware: io.WriteCloser is unavailable on the writer")
}
func encoderGzip(w http.ResponseWriter, level int) io.Writer {
gw, err := gzip.NewWriterLevel(w, level)
if err != nil {
return nil
}
return gw
}
func encoderDeflate(w http.ResponseWriter, level int) io.Writer {
dw, err := flate.NewWriter(w, level)
if err != nil {
return nil
}
return dw
}
-15
View File
@@ -1,15 +0,0 @@
// +build go1.8 appengine
package middleware
import (
"errors"
"net/http"
)
func (w *maybeCompressResponseWriter) Push(target string, opts *http.PushOptions) error {
if ps, ok := w.w.(http.Pusher); ok {
return ps.Push(target, opts)
}
return errors.New("chi/middleware: http.Pusher is unavailable on the writer")
}
+6
View File
@@ -26,6 +26,12 @@ func AllowContentType(contentTypes ...string) func(next http.Handler) http.Handl
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if r.ContentLength == 0 {
// skip check for empty content body
next.ServeHTTP(w, r)
return
}
s := strings.ToLower(strings.TrimSpace(r.Header.Get("Content-Type")))
if i := strings.Index(s, ";"); i > -1 {
s = s[0:i]
+22 -18
View File
@@ -16,7 +16,7 @@ var (
// DefaultLogger is called by the Logger middleware handler to log each request.
// Its made a package-level variable so that it can be reconfigured for custom
// logging configurations.
DefaultLogger = RequestLogger(&DefaultLogFormatter{Logger: log.New(os.Stdout, "", log.LstdFlags)})
DefaultLogger = RequestLogger(&DefaultLogFormatter{Logger: log.New(os.Stdout, "", log.LstdFlags), NoColor: false})
)
// Logger is a middleware that logs the start and end of each request, along
@@ -81,29 +81,32 @@ type LoggerInterface interface {
// DefaultLogFormatter is a simple logger that implements a LogFormatter.
type DefaultLogFormatter struct {
Logger LoggerInterface
Logger LoggerInterface
NoColor bool
}
// NewLogEntry creates a new LogEntry for the request.
func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry {
useColor := !l.NoColor
entry := &defaultLogEntry{
DefaultLogFormatter: l,
request: r,
buf: &bytes.Buffer{},
useColor: useColor,
}
reqID := GetReqID(r.Context())
if reqID != "" {
cW(entry.buf, nYellow, "[%s] ", reqID)
cW(entry.buf, useColor, nYellow, "[%s] ", reqID)
}
cW(entry.buf, nCyan, "\"")
cW(entry.buf, bMagenta, "%s ", r.Method)
cW(entry.buf, useColor, nCyan, "\"")
cW(entry.buf, useColor, bMagenta, "%s ", r.Method)
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
cW(entry.buf, nCyan, "%s://%s%s %s\" ", scheme, r.Host, r.RequestURI, r.Proto)
cW(entry.buf, useColor, nCyan, "%s://%s%s %s\" ", scheme, r.Host, r.RequestURI, r.Proto)
entry.buf.WriteString("from ")
entry.buf.WriteString(r.RemoteAddr)
@@ -114,33 +117,34 @@ func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry {
type defaultLogEntry struct {
*DefaultLogFormatter
request *http.Request
buf *bytes.Buffer
request *http.Request
buf *bytes.Buffer
useColor bool
}
func (l *defaultLogEntry) Write(status, bytes int, elapsed time.Duration) {
switch {
case status < 200:
cW(l.buf, bBlue, "%03d", status)
cW(l.buf, l.useColor, bBlue, "%03d", status)
case status < 300:
cW(l.buf, bGreen, "%03d", status)
cW(l.buf, l.useColor, bGreen, "%03d", status)
case status < 400:
cW(l.buf, bCyan, "%03d", status)
cW(l.buf, l.useColor, bCyan, "%03d", status)
case status < 500:
cW(l.buf, bYellow, "%03d", status)
cW(l.buf, l.useColor, bYellow, "%03d", status)
default:
cW(l.buf, bRed, "%03d", status)
cW(l.buf, l.useColor, bRed, "%03d", status)
}
cW(l.buf, bBlue, " %dB", bytes)
cW(l.buf, l.useColor, bBlue, " %dB", bytes)
l.buf.WriteString(" in ")
if elapsed < 500*time.Millisecond {
cW(l.buf, nGreen, "%s", elapsed)
cW(l.buf, l.useColor, nGreen, "%s", elapsed)
} else if elapsed < 5*time.Second {
cW(l.buf, nYellow, "%s", elapsed)
cW(l.buf, l.useColor, nYellow, "%s", elapsed)
} else {
cW(l.buf, nRed, "%s", elapsed)
cW(l.buf, l.useColor, nRed, "%s", elapsed)
}
l.Logger.Print(l.buf.String())
@@ -148,7 +152,7 @@ func (l *defaultLogEntry) Write(status, bytes int, elapsed time.Duration) {
func (l *defaultLogEntry) Panic(v interface{}, stack []byte) {
panicEntry := l.NewLogEntry(l.request).(*defaultLogEntry)
cW(panicEntry.buf, bRed, "panic: %+v", v)
cW(panicEntry.buf, l.useColor, bRed, "panic: %+v", v)
l.Logger.Print(panicEntry.buf.String())
l.Logger.Print(string(stack))
}
+1 -1
View File
@@ -14,7 +14,7 @@ var epoch = time.Unix(0, 0).Format(time.RFC1123)
// Taken from https://github.com/mytrile/nocache
var noCacheHeaders = map[string]string{
"Expires": epoch,
"Cache-Control": "no-cache, no-store, must-revalidate, private, max-age=0",
"Cache-Control": "no-cache, no-store, no-transform, must-revalidate, private, max-age=0",
"Pragma": "no-cache",
"X-Accel-Expires": "0",
}
+1 -1
View File
@@ -22,7 +22,7 @@ var xRealIP = http.CanonicalHeaderKey("X-Real-IP")
// You should only use this middleware if you can trust the headers passed to
// you (in particular, the two headers this middleware uses), for example
// because you have placed a reverse proxy like HAProxy or nginx in front of
// Goji. If your reverse proxies are configured to pass along arbitrary header
// chi. If your reverse proxies are configured to pass along arbitrary header
// values from the client, or if you use this middleware without a reverse
// proxy, malicious clients will be able to make you very sad (or, depending on
// how you're using RemoteAddr, vulnerable to an attack of some sort).
+7 -3
View File
@@ -17,7 +17,7 @@ import (
// Key to use when setting the request ID.
type ctxKeyRequestID int
// RequestIDKey is the key that holds th unique request ID in a request context.
// RequestIDKey is the key that holds the unique request ID in a request context.
const RequestIDKey ctxKeyRequestID = 0
var prefix string
@@ -62,9 +62,13 @@ func init() {
// counter.
func RequestID(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
myid := atomic.AddUint64(&reqid, 1)
ctx := r.Context()
ctx = context.WithValue(ctx, RequestIDKey, fmt.Sprintf("%s-%06d", prefix, myid))
requestID := r.Header.Get("X-Request-Id")
if requestID == "" {
myid := atomic.AddUint64(&reqid, 1)
requestID = fmt.Sprintf("%s-%06d", prefix, myid)
}
ctx = context.WithValue(ctx, RequestIDKey, requestID)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
+9 -1
View File
@@ -1,6 +1,7 @@
package middleware
import (
"fmt"
"net/http"
"github.com/go-chi/chi"
@@ -28,6 +29,9 @@ func StripSlashes(next http.Handler) http.Handler {
// RedirectSlashes is a middleware that will match request paths with a trailing
// slash and redirect to the same path, less the trailing slash.
//
// NOTE: RedirectSlashes middleware is *incompatible* with http.FileServer,
// see https://github.com/go-chi/chi/issues/343
func RedirectSlashes(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var path string
@@ -38,7 +42,11 @@ func RedirectSlashes(next http.Handler) http.Handler {
path = r.URL.Path
}
if len(path) > 1 && path[len(path)-1] == '/' {
path = path[:len(path)-1]
if r.URL.RawQuery != "" {
path = fmt.Sprintf("%s?%s", path[:len(path)-1], r.URL.RawQuery)
} else {
path = path[:len(path)-1]
}
http.Redirect(w, r, path, 301)
return
}
+3 -3
View File
@@ -52,12 +52,12 @@ func init() {
}
// colorWrite
func cW(w io.Writer, color []byte, s string, args ...interface{}) {
if isTTY {
func cW(w io.Writer, useColor bool, color []byte, s string, args ...interface{}) {
if isTTY && useColor {
w.Write(color)
}
fmt.Fprintf(w, s, args...)
if isTTY {
if isTTY && useColor {
w.Write(reset)
}
}
+2 -1
View File
@@ -15,7 +15,8 @@ import (
//
// ie. a route/handler may look like:
//
// r.Get("/long", func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
// r.Get("/long", func(w http.ResponseWriter, r *http.Request) {
// ctx := r.Context()
// processTime := time.Duration(rand.Intn(4)+1) * time.Second
//
// select {
+47 -12
View File
@@ -10,6 +10,32 @@ import (
"net/http"
)
// NewWrapResponseWriter wraps an http.ResponseWriter, returning a proxy that allows you to
// hook into various parts of the response process.
func NewWrapResponseWriter(w http.ResponseWriter, protoMajor int) WrapResponseWriter {
_, fl := w.(http.Flusher)
bw := basicWriter{ResponseWriter: w}
if protoMajor == 2 {
_, ps := w.(http.Pusher)
if fl && ps {
return &http2FancyWriter{bw}
}
} else {
_, hj := w.(http.Hijacker)
_, rf := w.(io.ReaderFrom)
if fl && hj && rf {
return &httpFancyWriter{bw}
}
}
if fl {
return &flushWriter{bw}
}
return &bw
}
// WrapResponseWriter is a proxy around an http.ResponseWriter that allows you to hook
// into various parts of the response process.
type WrapResponseWriter interface {
@@ -47,6 +73,7 @@ func (b *basicWriter) WriteHeader(code int) {
b.ResponseWriter.WriteHeader(code)
}
}
func (b *basicWriter) Write(buf []byte) (int, error) {
b.WriteHeader(http.StatusOK)
n, err := b.ResponseWriter.Write(buf)
@@ -60,20 +87,25 @@ func (b *basicWriter) Write(buf []byte) (int, error) {
b.bytes += n
return n, err
}
func (b *basicWriter) maybeWriteHeader() {
if !b.wroteHeader {
b.WriteHeader(http.StatusOK)
}
}
func (b *basicWriter) Status() int {
return b.code
}
func (b *basicWriter) BytesWritten() int {
return b.bytes
}
func (b *basicWriter) Tee(w io.Writer) {
b.tee = w
}
func (b *basicWriter) Unwrap() http.ResponseWriter {
return b.ResponseWriter
}
@@ -83,13 +115,15 @@ type flushWriter struct {
}
func (f *flushWriter) Flush() {
f.wroteHeader = true
fl := f.basicWriter.ResponseWriter.(http.Flusher)
fl.Flush()
}
var _ http.Flusher = &flushWriter{}
// httpFancyWriter is a HTTP writer that additionally satisfies http.CloseNotifier,
// httpFancyWriter is a HTTP writer that additionally satisfies
// http.Flusher, http.Hijacker, and io.ReaderFrom. It exists for the common case
// of wrapping the http.ResponseWriter that package http gives you, in order to
// make the proxied object support the full method set of the proxied object.
@@ -97,18 +131,22 @@ type httpFancyWriter struct {
basicWriter
}
func (f *httpFancyWriter) CloseNotify() <-chan bool {
cn := f.basicWriter.ResponseWriter.(http.CloseNotifier)
return cn.CloseNotify()
}
func (f *httpFancyWriter) Flush() {
f.wroteHeader = true
fl := f.basicWriter.ResponseWriter.(http.Flusher)
fl.Flush()
}
func (f *httpFancyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hj := f.basicWriter.ResponseWriter.(http.Hijacker)
return hj.Hijack()
}
func (f *http2FancyWriter) Push(target string, opts *http.PushOptions) error {
return f.basicWriter.ResponseWriter.(http.Pusher).Push(target, opts)
}
func (f *httpFancyWriter) ReadFrom(r io.Reader) (int64, error) {
if f.basicWriter.tee != nil {
n, err := io.Copy(&f.basicWriter, r)
@@ -122,12 +160,12 @@ func (f *httpFancyWriter) ReadFrom(r io.Reader) (int64, error) {
return n, err
}
var _ http.CloseNotifier = &httpFancyWriter{}
var _ http.Flusher = &httpFancyWriter{}
var _ http.Hijacker = &httpFancyWriter{}
var _ http.Pusher = &http2FancyWriter{}
var _ io.ReaderFrom = &httpFancyWriter{}
// http2FancyWriter is a HTTP2 writer that additionally satisfies http.CloseNotifier,
// http2FancyWriter is a HTTP2 writer that additionally satisfies
// http.Flusher, and io.ReaderFrom. It exists for the common case
// of wrapping the http.ResponseWriter that package http gives you, in order to
// make the proxied object support the full method set of the proxied object.
@@ -135,14 +173,11 @@ type http2FancyWriter struct {
basicWriter
}
func (f *http2FancyWriter) CloseNotify() <-chan bool {
cn := f.basicWriter.ResponseWriter.(http.CloseNotifier)
return cn.CloseNotify()
}
func (f *http2FancyWriter) Flush() {
f.wroteHeader = true
fl := f.basicWriter.ResponseWriter.(http.Flusher)
fl.Flush()
}
var _ http.CloseNotifier = &http2FancyWriter{}
var _ http.Flusher = &http2FancyWriter{}
-34
View File
@@ -1,34 +0,0 @@
// +build go1.7,!go1.8
package middleware
import (
"io"
"net/http"
)
// NewWrapResponseWriter wraps an http.ResponseWriter, returning a proxy that allows you to
// hook into various parts of the response process.
func NewWrapResponseWriter(w http.ResponseWriter, protoMajor int) WrapResponseWriter {
_, cn := w.(http.CloseNotifier)
_, fl := w.(http.Flusher)
bw := basicWriter{ResponseWriter: w}
if protoMajor == 2 {
if cn && fl {
return &http2FancyWriter{bw}
}
} else {
_, hj := w.(http.Hijacker)
_, rf := w.(io.ReaderFrom)
if cn && fl && hj && rf {
return &httpFancyWriter{bw}
}
}
if fl {
return &flushWriter{bw}
}
return &bw
}
-41
View File
@@ -1,41 +0,0 @@
// +build go1.8 appengine
package middleware
import (
"io"
"net/http"
)
// NewWrapResponseWriter wraps an http.ResponseWriter, returning a proxy that allows you to
// hook into various parts of the response process.
func NewWrapResponseWriter(w http.ResponseWriter, protoMajor int) WrapResponseWriter {
_, cn := w.(http.CloseNotifier)
_, fl := w.(http.Flusher)
bw := basicWriter{ResponseWriter: w}
if protoMajor == 2 {
_, ps := w.(http.Pusher)
if cn && fl && ps {
return &http2FancyWriter{bw}
}
} else {
_, hj := w.(http.Hijacker)
_, rf := w.(io.ReaderFrom)
if cn && fl && hj && rf {
return &httpFancyWriter{bw}
}
}
if fl {
return &flushWriter{bw}
}
return &bw
}
func (f *http2FancyWriter) Push(target string, opts *http.PushOptions) error {
return f.basicWriter.ResponseWriter.(http.Pusher).Push(target, opts)
}
var _ http.Pusher = &http2FancyWriter{}
+2 -1
View File
@@ -60,7 +60,8 @@ func NewMux() *Mux {
func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Ensure the mux has some routes defined on the mux
if mx.handler == nil {
panic("chi: attempting to route to a mux with no handlers.")
mx.NotFoundHandler().ServeHTTP(w, r)
return
}
// Check if a routing context already exists from a parent router.
+12 -10
View File
@@ -33,15 +33,15 @@ var mALL = mCONNECT | mDELETE | mGET | mHEAD |
mOPTIONS | mPATCH | mPOST | mPUT | mTRACE
var methodMap = map[string]methodTyp{
"CONNECT": mCONNECT,
"DELETE": mDELETE,
"GET": mGET,
"HEAD": mHEAD,
"OPTIONS": mOPTIONS,
"PATCH": mPATCH,
"POST": mPOST,
"PUT": mPUT,
"TRACE": mTRACE,
http.MethodConnect: mCONNECT,
http.MethodDelete: mDELETE,
http.MethodGet: mGET,
http.MethodHead: mHEAD,
http.MethodOptions: mOPTIONS,
http.MethodPatch: mPATCH,
http.MethodPost: mPOST,
http.MethodPut: mPUT,
http.MethodTrace: mTRACE,
}
// RegisterMethod adds support for custom HTTP method handlers, available
@@ -706,7 +706,9 @@ func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) {
}
// Wildcard pattern as finale
// TODO: should we panic if there is stuff after the * ???
if ws < len(pattern)-1 {
panic("chi: wildcard '*' must be the last value in a route. trim trailing text or use a '{param}' instead")
}
return ntCatchAll, "*", "", 0, ws, len(pattern)
}
+60
View File
@@ -0,0 +1,60 @@
linters-settings:
govet:
check-shadowing: true
golint:
min-confidence: 0
gocyclo:
min-complexity: 15
maligned:
suggest-new: true
dupl:
threshold: 100
goconst:
min-len: 2
min-occurrences: 2
misspell:
locale: US
lll:
line-length: 140
gocritic:
enabled-tags:
- performance
- style
- experimental
disabled-checks:
- wrapperFunc
linters:
disable-all: true
enable:
- megacheck
- govet
- unconvert
- megacheck
- structcheck
- gas
- gocyclo
- dupl
- misspell
- unparam
- varcheck
- deadcode
- typecheck
- ineffassign
- varcheck
fast: false
run:
# modules-download-mode: vendor
skip-dirs:
- vendor
issues:
exclude-rules:
- text: "weak cryptographic primitive"
linters:
- gosec
service:
golangci-lint-version: 1.16.x
+2 -3
View File
@@ -13,7 +13,6 @@ before_install:
script:
- GO111MODULE=on go get ./...
- GO111MODULE=on go mod vendor
- GO111MODULE=on go test -v -mod=vendor -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1;
- golangci-lint run || travis_terminate 1;
- GO111MODULE=on go test -v -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1;
- golangci-lint run || travis_terminate 1;
- $GOPATH/bin/goveralls -coverprofile=profile.cov -service=travis-ci
+11 -7
View File
@@ -32,7 +32,7 @@ _Without `lgr.Caller*` it will drop `{caller}` part_
`lgr.New` call accepts functional options:
- `lgr.Debug` - turn debug mode on to allow messages with "DEBUG" level (filtered overwise)
- `lgr.Debug` - turn debug mode on to allow messages with "DEBUG" level (filtered otherwise)
- `lgr.Out(io.Writer)` - sets the output writer, default `os.Stdout`
- `lgr.Err(io.Writer)` - sets the error writer, default `os.Stderr`
- `lgr.CallerFile` - adds the caller file info
@@ -42,6 +42,8 @@ _Without `lgr.Caller*` it will drop `{caller}` part_
- `lgr.Msec` - adds milliseconds to timestamp
- `lgr.Format` - sets custom template, overwrite all other formatting modifiers.
example: `l := lgr.New(lgr.Debug, lgr.Msec)`
#### formatting templates:
Several predefined templates provided and can be passed directly to `lgr.Format`, i.e. `lgr.Format(lgr.WithMsec)`
@@ -58,9 +60,11 @@ Several predefined templates provided and can be passed directly to `lgr.Format`
User can make a custom template and pass it directly to `lgr.Format`. For example:
```go
lgr.Format(`{{.Level}} - {{.DT.Format "2006-01-02T15:04:05Z07:00") - {{.CallerPkg}} - {{.Message}}`)
lgr.Format(`{{.Level}} - {{.DT.Format "2006-01-02T15:04:05Z07:00"}} - {{.CallerPkg}} - {{.Message}}`)
```
)
_Note: formatter (predefined or custom) adds measurable overhead - the cost will depend on the version of Go, but is between 30
and 50% in recent tests with 1.12. You can validate this in your environment via benchmarks: `go test -bench=. -run=Bench`_
### levels
@@ -70,8 +74,9 @@ User can make a custom template and pass it directly to `lgr.Format`. For exampl
- `DEBUG` will be filtered unless `lgr.Debug` or `lgr.Trace` options defined
- `INFO` and `WARN` don't have any special behavior attached
- `ERROR` sends messages to both out and err writers
- `PANIC` and `FATAL` send messages to both out and err writers. In addition sends dump of callers and runtime info to err only, and calls `os.Exit(1)`.
- `FATAL` and send messages to both out and err writers and exit(1)
- `PANIC` does the same as `FATAL` but in addition sends dump of callers and runtime info to err.
### adaptors
`lgr` logger can be converted to `io.Writer` or `*log.Logger`
@@ -79,11 +84,10 @@ User can make a custom template and pass it directly to `lgr.Format`. For exampl
- `lgr.ToWriter(l lgr.L, level string) io.Writer` - makes io.Writer forwarding write ops to underlying `lgr.L`
- `lgr.ToStdLogger(l lgr.L, level string) *log.Logger` - makes standard logger on top of `lgr.L`
_`level` parameter is optional, if defined will enforce the level._
_`level` parameter is optional, if defined (non-empty) will enforce the level._
### global logger
Users **should avoid** global logger and pass the concrete logger as a dependency. However, in some cases a global logger may be needed, for example migration from stdlib `log` to `lgr`. For such cases `log "github.com/go-pkgz/lgr"` can be imported instead of `log` package.
Global logger provides `lgr.Printf`, `lgr.Print` and `lgr.Fatalf` functions. User can customize the logger by calling `lgr.Setup(options ...)`. The instance of this logger can be retrieved with `lgr.Default()`
+2 -2
View File
@@ -11,9 +11,9 @@ type Writer struct {
level string // if defined added to each message
}
// Write to lgr.L, trim EOL
// Write to lgr.L
func (w *Writer) Write(p []byte) (n int, err error) {
w.Logf(strings.TrimSuffix(w.level+string(p), "\n"))
w.Logf(w.level + string(p))
return len(p), nil
}
+1 -2
View File
@@ -2,7 +2,6 @@ package lgr
import (
stdlog "log"
"os"
)
var def = New() // default logger doesn't allow DEBUG and doesn't add caller info
@@ -37,7 +36,7 @@ func Print(line string) {
// Fatalf simplifies replacement of std logger
func Fatalf(format string, args ...interface{}) {
def.logf(format, args...)
os.Exit(1)
def.fatal()
}
// Setup default logger with options
+61 -120
View File
@@ -1,10 +1,11 @@
// Package lgr provides a simple logger with some extras. Primary way to log is Logf method.
// The logger's output can be customized in 2 ways:
// - by passing formatting template, i.e. lgr.New(lgr.Format(lgr.Short))
// - by setting individual formatting flags, i.e. lgr.New(lgr.Msec, lgr.CallerFunc)
// Leveled output works for messages based on level prefix, i.e. Logf("INFO some message") means INFO level.
// - by passing formatting template, i.e. lgr.New(lgr.Format(lgr.Short))
// Leveled output works for messages based on text prefix, i.e. Logf("INFO some message") means INFO level.
// Debug and trace levels can be filtered based on lgr.Trace and lgr.Debug options.
// ERROR, FATAL and PANIC levels send to err as well. Both FATAL and PANIC also print stack trace and terminate caller application with os.Exit(1)
// ERROR, FATAL and PANIC levels send to err as well. FATAL terminate caller application with os.Exit(1)
// and PANIC also prints stack trace.
package lgr
@@ -15,6 +16,7 @@ import (
"os"
"path"
"runtime"
"strconv"
"strings"
"sync"
"text/template"
@@ -59,7 +61,7 @@ type Logger struct {
type nowFn func() time.Time
type panicFn func()
// layout holds all parts to construct the final message with template
// layout holds all parts to construct the final message with template or with individual flags
type layout struct {
DT time.Time
Level string
@@ -85,27 +87,28 @@ func New(options ...Option) *Logger {
opt(&res)
}
var err error
if res.format == "" {
res.format = res.templateFromOptions()
if res.format != "" {
// formatter defined
var err error
res.templ, err = template.New("lgr").Parse(res.format)
if err != nil {
fmt.Printf("invalid template %s, error %v. switched to %s\n", res.format, err, Short)
res.format = Short
res.templ = template.Must(template.New("lgrDefault").Parse(Short))
}
buf := bytes.Buffer{}
if err = res.templ.Execute(&buf, layout{}); err != nil {
fmt.Printf("failed to execute template %s, error %v. switched to %s\n", res.format, err, Short)
res.format = Short
res.templ = template.Must(template.New("lgrDefault").Parse(Short))
}
}
res.templ, err = template.New("lgr").Parse(res.format)
if err != nil {
fmt.Printf("invalid template %s, error %v. switched to %s\n", res.format, err, Short)
res.format = Short
res.templ = template.Must(template.New("lgrDefault").Parse(Short))
}
// set *On flags once for optimization on multiple Logf calls
res.callerOn = strings.Contains(res.format, "{{.Caller") || res.callerFile || res.callerFunc || res.callerPkg
res.levelBracesOn = strings.Contains(res.format, "[{{.Level}}]") || res.levelBraces
buf := bytes.Buffer{}
if err = res.templ.Execute(&buf, layout{}); err != nil {
fmt.Printf("failed to execute template %s, error %v. switched to %s\n", res.format, err, Short)
res.format = Short
res.templ = template.Must(template.New("lgrDefault").Parse(Short))
}
res.callerOn = strings.Contains(res.format, "{{.Caller")
res.levelBracesOn = strings.Contains(res.format, "[{{.Level}}]")
return &res
}
@@ -128,7 +131,7 @@ func (l *Logger) logf(format string, args ...interface{}) {
return
}
ci := callerInfo{}
var ci callerInfo
if l.callerOn { // optimization to avoid expensive caller evaluation if caller info not in the template
ci = l.reportCaller(l.callerDepth)
}
@@ -136,21 +139,26 @@ func (l *Logger) logf(format string, args ...interface{}) {
elems := layout{
DT: l.now(),
Level: l.formatLevel(lv),
Message: strings.TrimSuffix(msg, "\n"),
Message: strings.TrimSuffix(msg, "\n"), // output adds EOL, trim from the message if passed
CallerFunc: ci.FuncName,
CallerFile: ci.File,
CallerPkg: ci.Pkg,
CallerLine: ci.Line,
}
buf := bytes.Buffer{}
err := l.templ.Execute(&buf, elems) // once constructed, a template may be executed safely in parallel.
if err != nil {
fmt.Printf("failed to execute template, %v\n", err)
var data []byte
if l.format == "" {
data = []byte(l.formatWithOptions(elems))
} else {
buf := bytes.Buffer{}
err := l.templ.Execute(&buf, elems) // once constructed, a template may be executed safely in parallel.
if err != nil {
fmt.Printf("failed to execute template, %v\n", err) // should never happen
}
data = buf.Bytes()
}
buf.WriteString("\n")
data = append(data, '\n')
data := buf.Bytes()
if l.levelBracesOn { // rearrange space in short levels
data = bytes.Replace(data, []byte("[WARN ]"), []byte("[WARN] "), 1)
data = bytes.Replace(data, []byte("[INFO ]"), []byte("[INFO] "), 1)
@@ -230,51 +238,51 @@ func (l *Logger) reportCaller(calldepth int) (res callerInfo) {
return res
}
// make template from option flags
func (l *Logger) templateFromOptions() (res string) {
// speed-optimized version of formatter, used with individual options only, i.e. without Format call
func (l *Logger) formatWithOptions(elems layout) (res string) {
const (
// escape { and } from templates to allow "{some/blah}" output for caller
openCallerBrace = `{{"{"}}`
closeCallerBrace = `{{"}"}}`
)
orElse := func(flag bool, value string, elseValue string) string {
orElse := func(flag bool, fnTrue func() string, fnFalse func() string) string {
if flag {
return value
return fnTrue()
}
return elseValue
return fnFalse()
}
nothing := func() string { return "" }
var parts []string
parts := make([]string, 0, 4)
parts = append(parts, orElse(l.msec, `{{.DT.Format "2006/01/02 15:04:05.000"}}`, `{{.DT.Format "2006/01/02 15:04:05"}}`))
parts = append(parts, orElse(l.levelBraces, `[{{.Level}}]`, `{{.Level}}`))
parts = append(parts, orElse(l.msec,
func() string { return elems.DT.Format("2006/01/02 15:04:05.000") },
func() string { return elems.DT.Format("2006/01/02 15:04:05") },
))
parts = append(parts, orElse(l.levelBraces,
func() string { return `[` + elems.Level + `]` },
func() string { return elems.Level },
))
if l.callerFile || l.callerFunc || l.callerPkg {
var callerParts []string
if v := orElse(l.callerFile, `{{.CallerFile}}:{{.CallerLine}}`, ""); v != "" {
v := orElse(l.callerFile, func() string { return elems.CallerFile + ":" + strconv.Itoa(elems.CallerLine) }, nothing)
if v != "" {
callerParts = append(callerParts, v)
}
if v := orElse(l.callerFunc, `{{.CallerFunc}}`, ""); v != "" {
if v := orElse(l.callerFunc, func() string { return elems.CallerFunc }, nothing); v != "" {
callerParts = append(callerParts, v)
}
if v := orElse(l.callerPkg, `{{.CallerPkg}}`, ""); v != "" {
if v := orElse(l.callerPkg, func() string { return elems.CallerPkg }, nothing); v != "" {
callerParts = append(callerParts, v)
}
parts = append(parts, openCallerBrace+strings.Join(callerParts, " ")+closeCallerBrace)
parts = append(parts, "{"+strings.Join(callerParts, " ")+"}")
}
parts = append(parts, "{{.Message}}")
parts = append(parts, elems.Message)
return strings.Join(parts, " ")
}
// formatLevel aligns level to 5 chars
func (l *Logger) formatLevel(lv string) string {
if lv == "" {
return ""
}
spaces := ""
if len(lv) == 4 {
spaces = " "
@@ -305,70 +313,3 @@ func getDump() []byte {
}
return stacktrace[:length]
}
// Option func type
type Option func(l *Logger)
// Out sets out writer, stdout by default
func Out(w io.Writer) Option {
return func(l *Logger) {
l.stdout = w
}
}
// Err sets error writer, stderr by default
func Err(w io.Writer) Option {
return func(l *Logger) {
l.stderr = w
}
}
// Debug turn on dbg mode
func Debug(l *Logger) {
l.dbg = true
}
// Trace turn on trace + dbg mode
func Trace(l *Logger) {
l.dbg = true
l.trace = true
}
// CallerDepth sets number of stack frame skipped for caller reporting, 0 by default
func CallerDepth(n int) Option {
return func(l *Logger) {
l.callerDepth = n
}
}
// Format sets output layout, overwrites all options for individual parts, i.e. Caller*, Msec and LevelBraces
func Format(f string) Option {
return func(l *Logger) {
l.format = f
}
}
// CallerFunc adds caller info with function name. Ignored if Format option used.
func CallerFunc(l *Logger) {
l.callerFunc = true
}
// CallerPkg adds caller's package name. Ignored if Format option used.
func CallerPkg(l *Logger) {
l.callerPkg = true
}
// LevelBraces surrounds level with [], i.e. [INFO]. Ignored if Format option used.
func LevelBraces(l *Logger) {
l.levelBraces = true
}
// CallerFile adds caller info with file, and line number. Ignored if Format option used.
func CallerFile(l *Logger) {
l.callerFile = true
}
// Msec adds .msec to timestamp. Ignored if Format option used.
func Msec(l *Logger) {
l.msec = true
}
+70
View File
@@ -0,0 +1,70 @@
package lgr
import "io"
// Option func type
type Option func(l *Logger)
// Out sets out writer, stdout by default
func Out(w io.Writer) Option {
return func(l *Logger) {
l.stdout = w
}
}
// Err sets error writer, stderr by default
func Err(w io.Writer) Option {
return func(l *Logger) {
l.stderr = w
}
}
// Debug turn on dbg mode
func Debug(l *Logger) {
l.dbg = true
}
// Trace turn on trace + dbg mode
func Trace(l *Logger) {
l.dbg = true
l.trace = true
}
// CallerDepth sets number of stack frame skipped for caller reporting, 0 by default
func CallerDepth(n int) Option {
return func(l *Logger) {
l.callerDepth = n
}
}
// Format sets output layout, overwrites all options for individual parts, i.e. Caller*, Msec and LevelBraces
func Format(f string) Option {
return func(l *Logger) {
l.format = f
}
}
// CallerFunc adds caller info with function name. Ignored if Format option used.
func CallerFunc(l *Logger) {
l.callerFunc = true
}
// CallerPkg adds caller's package name. Ignored if Format option used.
func CallerPkg(l *Logger) {
l.callerPkg = true
}
// LevelBraces surrounds level with [], i.e. [INFO]. Ignored if Format option used.
func LevelBraces(l *Logger) {
l.levelBraces = true
}
// CallerFile adds caller info with file, and line number. Ignored if Format option used.
func CallerFile(l *Logger) {
l.callerFile = true
}
// Msec adds .msec to timestamp. Ignored if Format option used.
func Msec(l *Logger) {
l.msec = true
}
+2 -2
View File
@@ -23,7 +23,7 @@ github.com/globalsign/mgo/bson
github.com/globalsign/mgo/internal/sasl
github.com/globalsign/mgo/internal/scram
github.com/globalsign/mgo/internal/json
# github.com/go-chi/chi v3.3.2+incompatible
# github.com/go-chi/chi v4.0.2+incompatible
github.com/go-chi/chi
github.com/go-chi/chi/middleware
# github.com/go-chi/cors v1.0.0
@@ -39,7 +39,7 @@ github.com/go-pkgz/auth/logger
github.com/go-pkgz/auth/middleware
# github.com/go-pkgz/lcw v0.2.0
github.com/go-pkgz/lcw
# github.com/go-pkgz/lgr v0.6.1
# github.com/go-pkgz/lgr v0.6.2
github.com/go-pkgz/lgr
# github.com/go-pkgz/mongo v1.1.2
github.com/go-pkgz/mongo