Compare commits

..

3 Commits

Author SHA1 Message Date
27ce2d134b style: happy linting
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-02-24 09:22:51 +00:00
1d7e0dc701 style: happy linting
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-02-24 09:21:52 +00:00
d5b5c131e7 feat(requestid): the handler is request id aware
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
if the request has an header `X-Request-ID`, the middleware will extract
  it and use it as uuid on the logs.
2023-02-24 09:07:06 +00:00
4 changed files with 133 additions and 11 deletions

1
go.mod
View File

@ -6,6 +6,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/justinas/alice v1.2.0 github.com/justinas/alice v1.2.0
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/rs/xid v1.3.0
github.com/rs/zerolog v1.26.1 github.com/rs/zerolog v1.26.1
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect

View File

@ -8,11 +8,17 @@ import (
"time" "time"
"github.com/justinas/alice" "github.com/justinas/alice"
"github.com/rs/xid"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/hlog" "github.com/rs/zerolog/hlog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
const (
UUIDKey = "uuid"
UUIDHeader = "X-Request-ID"
)
func init() { //nolint:gochecknoinits func init() { //nolint:gochecknoinits
zerolog.DefaultContextLogger = &log.Logger zerolog.DefaultContextLogger = &log.Logger
} }
@ -39,6 +45,43 @@ func FromCtx(ctx context.Context) *zerolog.Logger {
return zerolog.Ctx(ctx) return zerolog.Ctx(ctx)
} }
// RequestIDHandler looks in the header for an existing request id. Else it will create one.
func RequestIDHandler() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get(UUIDHeader)
if id != "" {
ctx := r.Context()
log := zerolog.Ctx(ctx)
uuid, err := xid.FromString(id)
if err != nil {
log.Error().Err(err).Msg("couldnt parse uuid")
hlog.RequestIDHandler(UUIDKey, UUIDHeader)(next).ServeHTTP(w, r)
return
}
ctx = hlog.CtxWithID(ctx, uuid)
r = r.WithContext(ctx)
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str(UUIDKey, uuid.String())
})
w.Header().Set(UUIDHeader, uuid.String())
next.ServeHTTP(w, r)
} else {
hlog.RequestIDHandler(UUIDKey, UUIDHeader)(next).ServeHTTP(w, r)
}
})
}
}
func Handler(log zerolog.Logger) func(http.Handler) http.Handler { func Handler(log zerolog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
chain := alice.New( chain := alice.New(
@ -56,7 +99,7 @@ func Handler(log zerolog.Logger) func(http.Handler) http.Handler {
hlog.RemoteAddrHandler("remote"), hlog.RemoteAddrHandler("remote"),
hlog.UserAgentHandler("user-agent"), hlog.UserAgentHandler("user-agent"),
hlog.RefererHandler("referer"), hlog.RefererHandler("referer"),
hlog.RequestIDHandler("uuid", "X-Request-ID"), RequestIDHandler(),
).Then(next) ).Then(next)
return chain return chain

View File

@ -1,15 +1,18 @@
//nolint:funlen
package logginghandler_test package logginghandler_test
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"testing" "testing"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/hlog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.xsfx.dev/logginghandler" "go.xsfx.dev/logginghandler"
@ -18,13 +21,17 @@ import (
func Example() { func Example() {
logger := log.With().Logger() logger := log.With().Logger()
handler := logginghandler.Handler(logger)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := logginghandler.Handler(
logger,
)(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger := logginghandler.FromRequest(r) logger := logginghandler.FromRequest(r)
logger.Info().Msg("this is a request") logger.Info().Msg("this is a request")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
})) }),
)
http.Handle("/", handler) http.Handle("/", handler)
log.Fatal().Msg(http.ListenAndServe(":5000", nil).Error()) log.Fatal().Msg(http.ListenAndServe(":5000", nil).Error())
@ -45,7 +52,7 @@ func TestUUID(t *testing.T) {
handler.ServeHTTP(rr, req) handler.ServeHTTP(rr, req)
assert.NotEmpty(rr.Header().Get("X-Request-ID")) assert.NotEmpty(rr.Header().Get(logginghandler.UUIDHeader))
} }
func TestFromCtx(t *testing.T) { func TestFromCtx(t *testing.T) {
@ -58,10 +65,14 @@ func TestFromCtx(t *testing.T) {
var output bytes.Buffer var output bytes.Buffer
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
handler := logginghandler.Handler(zerolog.New(&output))(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := logginghandler.Handler(
zerolog.New(&output),
)(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := logginghandler.FromCtx(r.Context()) log := logginghandler.FromCtx(r.Context())
log.Info().Msg("hello world") log.Info().Msg("hello world")
})) }),
)
handler.ServeHTTP(rr, req) handler.ServeHTTP(rr, req)
@ -75,3 +86,69 @@ func TestFromCtx(t *testing.T) {
assert.NotEmpty(jOut) assert.NotEmpty(jOut)
} }
func TestRequestIDHandler(t *testing.T) {
t.Parallel()
assert := require.New(t)
handler := logginghandler.RequestIDHandler()(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := hlog.FromRequest(r)
log.Info().Msg("hello from TestRequestID")
}),
)
id := "cfrj1ro330reqgvfpgu0"
// Create buffer to store output.
var output bytes.Buffer
req, err := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
assert.NoError(err)
rr := httptest.NewRecorder()
h := hlog.NewHandler(zerolog.New(&output))(handler)
h.ServeHTTP(rr, req)
assert.NotEmpty(rr.Header().Get(logginghandler.UUIDHeader))
assert.NotEqual(rr.Header().Get(logginghandler.UUIDHeader), id)
// Now test with request id in header.
nr := httptest.NewRecorder()
nReq, err := http.NewRequestWithContext(context.Background(), "GET", "/", nil)
assert.NoError(err)
nReq.Header.Add(logginghandler.UUIDHeader, id)
h.ServeHTTP(nr, nReq)
assert.NotEmpty(nr.Header().Get(logginghandler.UUIDHeader))
assert.Equal(nr.Header().Get(logginghandler.UUIDHeader), id)
logs := strings.Split(output.String(), "\n")
assert.Len(logs, 3)
getUUID := func(l string) (string, error) {
var out struct{ UUID string }
err := json.Unmarshal([]byte(l), &out)
if err != nil {
return "", fmt.Errorf("failed to unmarshal log: %w", err)
}
return out.UUID, nil
}
uuid1, err := getUUID(logs[0])
assert.NoError(err)
assert.NotEqual(id, uuid1)
uuid2, err := getUUID(logs[1])
assert.NoError(err)
assert.Equal(id, uuid2)
}

1
vendor/modules.txt vendored
View File

@ -9,6 +9,7 @@ github.com/justinas/alice
# github.com/pmezard/go-difflib v1.0.0 # github.com/pmezard/go-difflib v1.0.0
github.com/pmezard/go-difflib/difflib github.com/pmezard/go-difflib/difflib
# github.com/rs/xid v1.3.0 # github.com/rs/xid v1.3.0
## explicit
github.com/rs/xid github.com/rs/xid
# github.com/rs/zerolog v1.26.1 # github.com/rs/zerolog v1.26.1
## explicit ## explicit