-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
351 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
package logger | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
"time" | ||
|
||
"github.com/go-chi/chi/v5" | ||
"github.com/go-chi/chi/v5/middleware" | ||
"github.com/rs/zerolog" | ||
|
||
logger "github.com/VATUSA/primary-api/pkg/logging" | ||
) | ||
|
||
func Logger(logger zerolog.Logger, skipPaths ...[]string) func(next http.Handler) http.Handler { | ||
return chi.Chain( | ||
middleware.RequestID, | ||
Handler(logger, skipPaths...), | ||
middleware.Recoverer, | ||
).Handler | ||
} | ||
|
||
func NewLogger() zerolog.Logger { | ||
return logger.ZL.With().Str("component", "access").Logger() | ||
} | ||
|
||
func Handler(logger zerolog.Logger, optSkipPaths ...[]string) func(next http.Handler) http.Handler { | ||
var f middleware.LogFormatter = &requestLogger{logger} | ||
|
||
skipPaths := map[string]struct{}{} | ||
if len(optSkipPaths) > 0 { | ||
for _, path := range optSkipPaths[0] { | ||
skipPaths[path] = struct{}{} | ||
} | ||
} | ||
|
||
return func(next http.Handler) http.Handler { | ||
fn := func(w http.ResponseWriter, r *http.Request) { | ||
// Skip the logger if the path is in the skip list | ||
if len(skipPaths) > 0 { | ||
_, skip := skipPaths[r.URL.Path] | ||
if skip { | ||
next.ServeHTTP(w, r) | ||
return | ||
} | ||
} | ||
|
||
// Log the request | ||
entry := f.NewLogEntry(r) | ||
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) | ||
|
||
buf := newLimitBuffer(512) | ||
ww.Tee(buf) | ||
|
||
t1 := time.Now() | ||
defer func() { | ||
var respBody []byte | ||
if ww.Status() >= 400 { | ||
respBody, _ = io.ReadAll(buf) | ||
} | ||
entry.Write(ww.Status(), ww.BytesWritten(), ww.Header(), time.Since(t1), respBody) | ||
}() | ||
|
||
next.ServeHTTP(ww, middleware.WithLogEntry(r, entry)) | ||
} | ||
return http.HandlerFunc(fn) | ||
} | ||
} | ||
|
||
type requestLogger struct { | ||
Logger zerolog.Logger | ||
} | ||
|
||
func (l *requestLogger) NewLogEntry(r *http.Request) middleware.LogEntry { | ||
entry := &RequestLoggerEntry{} | ||
entry.Logger = l.Logger.With().Fields(requestLogFields(r)).Logger() | ||
return entry | ||
} | ||
|
||
type RequestLoggerEntry struct { | ||
Logger zerolog.Logger | ||
msg string | ||
} | ||
|
||
func (l *RequestLoggerEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { | ||
responseLog := map[string]interface{}{ | ||
"status": status, | ||
"bytes": bytes, | ||
"elapsed": elapsed.String(), // in milliseconds | ||
"content-type": header.Get("Content-Type"), | ||
"body": string(extra.([]byte)), | ||
} | ||
|
||
l.Logger.WithLevel(statusLevel(status)).Fields(map[string]interface{}{ | ||
"httpResponse": responseLog, | ||
}).Msgf("") | ||
} | ||
|
||
func (l *RequestLoggerEntry) Panic(v interface{}, stack []byte) { | ||
stacktrace := "#" | ||
|
||
l.Logger = l.Logger.With(). | ||
Str("stacktrace", stacktrace). | ||
Str("panic", fmt.Sprintf("%+v", v)). | ||
Logger() | ||
|
||
l.msg = fmt.Sprintf("%+v", v) | ||
|
||
middleware.PrintPrettyStack(v) | ||
} | ||
|
||
func requestLogFields(r *http.Request) map[string]interface{} { | ||
scheme := "http" | ||
if r.TLS != nil { | ||
scheme = "https" | ||
} | ||
requestURL := fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI) | ||
|
||
requestFields := map[string]interface{}{ | ||
"requestURL": requestURL, | ||
"requestMethod": r.Method, | ||
"requestPath": r.URL.Path, | ||
"remoteIP": r.RemoteAddr, | ||
"proto": r.Proto, | ||
} | ||
if reqID := middleware.GetReqID(r.Context()); reqID != "" { | ||
requestFields["requestID"] = reqID | ||
} | ||
|
||
requestFields["scheme"] = scheme | ||
|
||
if len(r.Header) > 0 { | ||
requestFields["header"] = headerLogField(r.Header) | ||
} | ||
|
||
return map[string]interface{}{ | ||
"httpRequest": requestFields, | ||
} | ||
} | ||
|
||
func headerLogField(header http.Header) map[string]string { | ||
headerField := map[string]string{} | ||
for k, v := range header { | ||
k = strings.ToLower(k) | ||
switch { | ||
case len(v) == 0: | ||
continue | ||
case len(v) == 1: | ||
headerField[k] = v[0] | ||
default: | ||
headerField[k] = fmt.Sprintf("[%s]", strings.Join(v, "], [")) | ||
} | ||
if k == "authorization" || k == "cookie" || k == "set-cookie" { | ||
headerField[k] = "***" | ||
} | ||
} | ||
return headerField | ||
} | ||
|
||
func statusLevel(status int) zerolog.Level { | ||
switch { | ||
case status <= 0: | ||
return zerolog.WarnLevel | ||
case status < 500: // for codes in 100-499 | ||
return zerolog.InfoLevel | ||
case status >= 500: | ||
return zerolog.ErrorLevel | ||
default: | ||
return zerolog.InfoLevel | ||
} | ||
} | ||
|
||
func statusLabel(status int) string { | ||
switch { | ||
case status >= 100 && status < 300: | ||
return "OK" | ||
case status >= 300 && status < 400: | ||
return "Redirect" | ||
case status >= 400 && status < 500: | ||
return "Client Error" | ||
case status >= 500: | ||
return "Server Error" | ||
default: | ||
return "Unknown" | ||
} | ||
} | ||
|
||
func LogEntry(ctx context.Context) zerolog.Logger { | ||
entry, ok := ctx.Value(middleware.LogEntryCtxKey).(*RequestLoggerEntry) | ||
if !ok || entry == nil { | ||
return zerolog.Nop() | ||
} | ||
return entry.Logger | ||
} | ||
|
||
func LogEntrySetField(ctx context.Context, key, value string) { | ||
if entry, ok := ctx.Value(middleware.LogEntryCtxKey).(*RequestLoggerEntry); ok { | ||
entry.Logger = entry.Logger.With().Str(key, value).Logger() | ||
} | ||
} | ||
|
||
func LogEntrySetFields(ctx context.Context, fields map[string]interface{}) { | ||
if entry, ok := ctx.Value(middleware.LogEntryCtxKey).(*RequestLoggerEntry); ok { | ||
entry.Logger = entry.Logger.With().Fields(fields).Logger() | ||
} | ||
} | ||
|
||
type limitBuffer struct { | ||
*bytes.Buffer | ||
limit int | ||
} | ||
|
||
func newLimitBuffer(size int) io.ReadWriter { | ||
return limitBuffer{ | ||
Buffer: bytes.NewBuffer(make([]byte, 0, size)), | ||
limit: size, | ||
} | ||
} | ||
|
||
func (b limitBuffer) Write(p []byte) (n int, err error) { | ||
if b.Buffer.Len() >= b.limit { | ||
return len(p), nil | ||
} | ||
limit := b.limit | ||
if len(p) < limit { | ||
limit = len(p) | ||
} | ||
return b.Buffer.Write(p[:limit]) | ||
} | ||
|
||
func (b limitBuffer) Read(p []byte) (n int, err error) { | ||
return b.Buffer.Read(p) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.