diff --git a/.traefik.yml b/.traefik.yml index d2113f4..bb4d472 100644 --- a/.traefik.yml +++ b/.traefik.yml @@ -4,7 +4,7 @@ type: middleware import: github.com/juitde/traefik-plugin-fail2ban -summary: 'Block or allow IPs depending on various conditions' +summary: 'Block or allow IPs depending on various conditions (requires Traefik >= 2.10.0)' testData: enabled: true diff --git a/README.md b/README.md index a5ece05..c141bb1 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,13 @@ Inspirations taken from: Installation instructions are provided via the [traefik Plugin Catalog](https://plugins.traefik.io/plugins/). +### CAUTION: Breaking Changes + +#### Version 0.2.0 + +- traefik v2.10+ is required due to now having a vendored dependency which results + in go routine panics in previous traefik versions. + ## Configuration All configuration options may be specified either in config files or as CLI parameters. diff --git a/docker-compose.yml b/docker-compose.yml index fcd1e92..c72c774 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: traefik: - image: traefik:v2.9 + image: traefik:v2.10 ports: - target: 8080 published: 8080 diff --git a/fail2ban.go b/fail2ban.go index d27410c..e2caae9 100644 --- a/fail2ban.go +++ b/fail2ban.go @@ -11,6 +11,8 @@ import ( "strconv" "strings" "time" + + "github.com/zerodha/logf" ) type configIPSpec struct { @@ -64,6 +66,7 @@ type Fail2Ban struct { next http.Handler name string cache *Cache + logger *logf.Logger enabled bool staticAllowedIPNets []*net.IPNet staticDeniedIPNets []*net.IPNet @@ -147,11 +150,12 @@ func parseResponseRules(config configResponse) responseRules { } // New creates a Fail2Ban plugin instance. -func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { +func New(_ context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { return &Fail2Ban{ next: next, name: name, cache: NewCache(), + logger: NewLogger(config.LogLevel), enabled: config.Enabled, staticAllowedIPNets: parseConfigIPList(config.AlwaysAllowed.IP), staticDeniedIPNets: parseConfigIPList(config.AlwaysDenied.IP), @@ -164,25 +168,32 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h func (a *Fail2Ban) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { if !a.enabled { + a.logger.Debug("Handler is not enabled. Skipping.", "phase", "accept_request") a.next.ServeHTTP(responseWriter, request) return } + a.logger.Debug("Handler is enabled. Analyzing request.", "phase", "accept_request") remoteIP, _, err := net.SplitHostPort(request.RemoteAddr) if err != nil { + a.logger.Error("Failed to detect remoteIP from request.RemoteAddr.", "request.RemoteAddr", request.RemoteAddr, "phase", "accept_request") responseWriter.WriteHeader(http.StatusInternalServerError) return } for _, ipNet := range a.staticDeniedIPNets { + a.logger.Debug("Checking if remoteIP is in static denied Netmask.", "remoteIP", remoteIP, "netmask", ipNet, "phase", "check_request") if ipNet.Contains(net.ParseIP(remoteIP)) { + a.logger.Info("RemoteIP was found in staticDeniedIPNets. Access Denied.", "remoteIP", remoteIP, "staticDeniedIPNets", a.staticDeniedIPNets, "phase", "check_request", "status", "denied") responseWriter.WriteHeader(http.StatusForbidden) return } } for _, ipNet := range a.staticAllowedIPNets { + a.logger.Debug("Checking if remoteIP is in static allowed Netmask.", "remoteIP", remoteIP, "netmask", ipNet, "phase", "check_request") if ipNet.Contains(net.ParseIP(remoteIP)) { + a.logger.Info("RemoteIP was found in staticAllowedIPNets. Access Granted.", "remoteIP", remoteIP, "staticAllowedIPNets", a.staticAllowedIPNets, "phase", "check_request", "status", "granted") a.next.ServeHTTP(responseWriter, request) return } @@ -194,10 +205,12 @@ func (a *Fail2Ban) ServeHTTP(responseWriter http.ResponseWriter, request *http.R entry := a.cache.CreateEntry(remoteIP, requestTime) if entry.GetTimesSeen() >= a.maxRetries && !entry.IsBanned() { + a.logger.Info("Client has been banned.", "remoteIP", remoteIP, "maxRetries", a.maxRetries, "banTime", a.banTime, "findTime", a.findTime, "phase", "check_request", "status", "denied") entry.IssueBan() } if entry.IsBanned() { + a.logger.Debug("Client is still banned.", "remoteIP", remoteIP, "phase", "check_request", "status", "denied") responseWriter.WriteHeader(http.StatusForbidden) return } @@ -208,6 +221,7 @@ func (a *Fail2Ban) ServeHTTP(responseWriter http.ResponseWriter, request *http.R // Response rules will be checked in the wrapped response writer wrappedResponseWriter := &ResponseWriter{ ResponseWriter: responseWriter, + logger: a.logger, cacheEntry: entry, rules: a.responseRules, } diff --git a/go.mod b/go.mod index efbb412..1c0eea8 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/juitde/traefik-plugin-fail2ban go 1.19 + +require github.com/zerodha/logf v0.5.5 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..58a00ec --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/zerodha/logf v0.5.5 h1:AhxHlixHNYwhFjvlgTv6uO4VBKYKxx2I6SbHoHtWLBk= +github.com/zerodha/logf v0.5.5/go.mod h1:HWpfKsie+WFFpnUnUxelT6Z0FC6xu9+qt+oXNMPg6y8= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..6b068fc --- /dev/null +++ b/logger.go @@ -0,0 +1,28 @@ +package traefik_plugin_fail2ban //nolint:revive,stylecheck + +import ( + "fmt" + "strings" + "time" + + "github.com/zerodha/logf" +) + +// NewLogger creates new instance of logger. +func NewLogger(logLevel string) *logf.Logger { + parsedLogLevel, err := logf.LevelFromString(strings.ToLower(logLevel)) + if err != nil { + parsedLogLevel = logf.InfoLevel + } + logger := logf.New(logf.Opts{ + EnableColor: false, + Level: parsedLogLevel, + EnableCaller: false, + TimestampFormat: fmt.Sprintf("\"%s\"", time.RFC3339), + DefaultFields: []any{"plugin", "JUIT Fail2Ban"}, + }) + + logger.Debug(fmt.Sprintf("Setting log level to %s", strings.ToUpper(parsedLogLevel.String())), "phase", "initialize") + + return &logger +} diff --git a/response_writer.go b/response_writer.go index 1040f73..f5ddd78 100644 --- a/response_writer.go +++ b/response_writer.go @@ -3,12 +3,15 @@ package traefik_plugin_fail2ban //nolint:revive,stylecheck import ( "net/http" "strconv" + + "github.com/zerodha/logf" ) // ResponseWriter wrapping original ResponseWriter with response check handling. type ResponseWriter struct { http.ResponseWriter + logger *logf.Logger cacheEntry *CacheEntry rules responseRules } @@ -20,7 +23,10 @@ func (rw *ResponseWriter) WriteHeader(code int) { if rw.rules.StatusCode != nil { if rw.rules.StatusCode.MatchString(strconv.Itoa(code)) { + rw.logger.Debug("Response Status Code matched rule. Incrementing.", "statusCodeRegexp", rw.rules.StatusCode, "responseStatusCode", code, "phase", "check_response", "status", "denied") rw.cacheEntry.IncrementTimesSeen() + } else { + rw.logger.Debug("Response Status Code does not match rule. Skipping.", "statusCodeRegexp", rw.rules.StatusCode, "responseStatusCode", code, "phase", "check_response", "status", "granted") } } } diff --git a/vendor/github.com/zerodha/logf/.gitignore b/vendor/github.com/zerodha/logf/.gitignore new file mode 100644 index 0000000..a779d58 --- /dev/null +++ b/vendor/github.com/zerodha/logf/.gitignore @@ -0,0 +1,23 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +config.toml +.env +bin/ +data/ +dist/ +.vscode/ +coverage.txt \ No newline at end of file diff --git a/vendor/github.com/zerodha/logf/.goreleaser.yml b/vendor/github.com/zerodha/logf/.goreleaser.yml new file mode 100644 index 0000000..85b513a --- /dev/null +++ b/vendor/github.com/zerodha/logf/.goreleaser.yml @@ -0,0 +1,8 @@ +before: + hooks: + - go mod tidy +builds: +- skip: true + +snapshot: + name_template: '{{ incpatch .Version }}-next' diff --git a/vendor/github.com/zerodha/logf/LICENSE b/vendor/github.com/zerodha/logf/LICENSE new file mode 100644 index 0000000..e405b58 --- /dev/null +++ b/vendor/github.com/zerodha/logf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Karan Sharma + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/zerodha/logf/Makefile b/vendor/github.com/zerodha/logf/Makefile new file mode 100644 index 0000000..a79bc15 --- /dev/null +++ b/vendor/github.com/zerodha/logf/Makefile @@ -0,0 +1,6 @@ +.PHONY: test +test: + go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt + +benchmark: + go test -bench=. -benchmem diff --git a/vendor/github.com/zerodha/logf/README.md b/vendor/github.com/zerodha/logf/README.md new file mode 100644 index 0000000..ca88b52 --- /dev/null +++ b/vendor/github.com/zerodha/logf/README.md @@ -0,0 +1,102 @@ + + +# 💥 logf + +[![Go Reference](https://pkg.go.dev/badge/github.com/zerodha/logf.svg)](https://pkg.go.dev/github.com/zerodha/logf) +[![Go Report Card](https://goreportcard.com/badge/zerodha/logf)](https://goreportcard.com/report/zerodha/logf) +[![GitHub Actions](https://github.com/zerodha/logf/actions/workflows/build.yml/badge.svg)](https://github.com/zerodha/logf/actions/workflows/build.yml) + +`logf` is a high-performance, zero-alloc logging library for Go applications with a minimal API overhead. It's also the fastest logfmt logging library for Go. + +`logf` emits structured logs in [`logfmt`](https://brandur.org/logfmt) style. `logfmt` is a flexible format that involves `key=value` pairs to emit structured log lines. `logfmt` achieves the goal of generating logs that are not just machine-friendly but also readable by humans, unlike the clunky JSON lines. + +## Example + +```go +package main + +import ( + "time" + + "github.com/zerodha/logf" +) + +func main() { + logger := logf.New(logf.Opts{ + EnableColor: true, + Level: logf.DebugLevel, + CallerSkipFrameCount: 3, + EnableCaller: true, + TimestampFormat: time.RFC3339Nano, + DefaultFields: []any{"scope", "example"}, + }) + + // Basic logs. + logger.Info("starting app") + logger.Debug("meant for debugging app") + + // Add extra keys to the log. + logger.Info("logging with some extra metadata", "component", "api", "user", "karan") + + // Log with error key. + logger.Error("error fetching details", "error", "this is a dummy error") + + // Log the error and set exit code as 1. + logger.Fatal("goodbye world") +} +``` + +### Text Output + +```bash +timestamp=2022-07-07T12:09:10.221+05:30 level=info message="starting app" +timestamp=2022-07-07T12:09:10.221+05:30 level=info message="logging with some extra metadata" component=api user=karan +timestamp=2022-07-07T12:09:10.221+05:30 level=error message="error fetching details" error="this is a dummy error" +timestamp=2022-07-07T12:09:10.221+05:30 level=fatal message="goodbye world" +``` + +### Console Output + +![](examples/screenshot.png) + +## Why another lib + +There are several logging libraries, but the available options didn't meet our use case. + +`logf` meets our constraints of: + +- Clean API +- Minimal dependencies +- Structured logging but human-readable (`logfmt`!) +- Sane defaults out of the box + +## Benchmarks + +You can run benchmarks with `make bench`. + +### No Colors (Default) + +``` +BenchmarkNoField-8 7884771 144.2 ns/op 0 B/op 0 allocs/op +BenchmarkOneField-8 6251565 186.7 ns/op 0 B/op 0 allocs/op +BenchmarkThreeFields-8 6273717 188.2 ns/op 0 B/op 0 allocs/op +BenchmarkErrorField-8 6687260 174.8 ns/op 0 B/op 0 allocs/op +BenchmarkHugePayload-8 3395139 360.3 ns/op 0 B/op 0 allocs/op +BenchmarkThreeFields_WithCaller-8 2764860 437.9 ns/op 216 B/op 2 allocs/op +``` + +### With Colors + +``` +BenchmarkNoField_WithColor-8 6501867 186.6 ns/op 0 B/op 0 allocs/op +BenchmarkOneField_WithColor-8 5938155 205.7 ns/op 0 B/op 0 allocs/op +BenchmarkThreeFields_WithColor-8 4613145 379.4 ns/op 0 B/op 0 allocs/op +BenchmarkErrorField_WithColor-8 3512522 353.6 ns/op 0 B/op 0 allocs/op +BenchmarkHugePayload_WithColor-8 1520659 799.5 ns/op 0 B/op 0 allocs/op +``` + +For a comparison with existing popular libs, visit [uber-go/zap#performance](https://github.com/uber-go/zap#performance). + +## LICENSE + +[LICENSE](./LICENSE) diff --git a/vendor/github.com/zerodha/logf/buffer.go b/vendor/github.com/zerodha/logf/buffer.go new file mode 100644 index 0000000..45207b8 --- /dev/null +++ b/vendor/github.com/zerodha/logf/buffer.go @@ -0,0 +1,73 @@ +package logf + +import ( + "strconv" + "sync" + "time" +) + +// ref: https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/lib/bytesutil/bytebuffer.go +// byteBufferPool is a pool of byteBuffer +type byteBufferPool struct { + p sync.Pool +} + +// Get returns a new instance of byteBuffer or gets from the object pool +func (bbp *byteBufferPool) Get() *byteBuffer { + bbv := bbp.p.Get() + if bbv == nil { + return &byteBuffer{} + } + return bbv.(*byteBuffer) +} + +// Put puts back the ByteBuffer into the object pool +func (bbp *byteBufferPool) Put(bb *byteBuffer) { + bb.Reset() + bbp.p.Put(bb) +} + +// byteBuffer is a wrapper around byte array +type byteBuffer struct { + B []byte +} + +// AppendByte appends a single byte to the buffer. +func (bb *byteBuffer) AppendByte(b byte) { + bb.B = append(bb.B, b) +} + +// AppendString appends a string to the buffer. +func (bb *byteBuffer) AppendString(s string) { + bb.B = append(bb.B, s...) +} + +// AppendInt appends an integer to the underlying buffer (assuming base 10). +func (bb *byteBuffer) AppendInt(i int64) { + bb.B = strconv.AppendInt(bb.B, i, 10) +} + +// AppendTime appends the time formatted using the specified layout. +func (bb *byteBuffer) AppendTime(t time.Time, layout string) { + bb.B = t.AppendFormat(bb.B, layout) +} + +// AppendBool appends a bool to the underlying buffer. +func (bb *byteBuffer) AppendBool(v bool) { + bb.B = strconv.AppendBool(bb.B, v) +} + +// AppendFloat appends a float to the underlying buffer. +func (bb *byteBuffer) AppendFloat(f float64, bitSize int) { + bb.B = strconv.AppendFloat(bb.B, f, 'f', -1, bitSize) +} + +// Bytes returns a mutable reference to the underlying buffer. +func (bb *byteBuffer) Bytes() []byte { + return bb.B +} + +// Reset resets the underlying buffer. +func (bb *byteBuffer) Reset() { + bb.B = bb.B[:0] +} diff --git a/vendor/github.com/zerodha/logf/log.go b/vendor/github.com/zerodha/logf/log.go new file mode 100644 index 0000000..75313dd --- /dev/null +++ b/vendor/github.com/zerodha/logf/log.go @@ -0,0 +1,430 @@ +package logf + +import ( + "fmt" + "io" + stdlog "log" + "os" + "runtime" + "strings" + "sync" + "time" + "unicode/utf8" +) + +const ( + tsKey = "timestamp=" + defaultTSFormat = "2006-01-02T15:04:05.999Z07:00" +) + +var ( + hex = "0123456789abcdef" + bufPool byteBufferPool + exit = func() { os.Exit(1) } +) + +type Opts struct { + Writer io.Writer + Level Level + TimestampFormat string + EnableColor bool + EnableCaller bool + CallerSkipFrameCount int + + // These fields will be printed with every log. + DefaultFields []interface{} +} + +// Logger is the interface for all log operations +// related to emitting logs. +type Logger struct { + out io.Writer // Output destination. + Opts +} + +// Severity level of the log. +type Level int + +const ( + DebugLevel Level = iota + 1 // 1 + InfoLevel // 2 + WarnLevel // 3 + ErrorLevel // 4 + FatalLevel // 5 +) + +// ANSI escape codes for coloring text in console. +const ( + reset = "\033[0m" + purple = "\033[35m" + red = "\033[31m" + yellow = "\033[33m" + cyan = "\033[36m" +) + +// Map colors with log level. +var colorLvlMap = [...]string{ + DebugLevel: purple, + InfoLevel: cyan, + WarnLevel: yellow, + ErrorLevel: red, + FatalLevel: red, +} + +// New instantiates a logger object. +func New(opts Opts) Logger { + // Initialize fallbacks if unspecified by user. + if opts.Writer == nil { + opts.Writer = os.Stderr + } + if opts.TimestampFormat == "" { + opts.TimestampFormat = defaultTSFormat + } + if opts.Level == 0 { + opts.Level = InfoLevel + } + if opts.CallerSkipFrameCount == 0 { + opts.CallerSkipFrameCount = 3 + } + + if len(opts.DefaultFields)%2 != 0 { + opts.DefaultFields = opts.DefaultFields[0 : len(opts.DefaultFields)-1] + } + + return Logger{ + out: newSyncWriter(opts.Writer), + Opts: opts, + } +} + +// syncWriter is a wrapper around io.Writer that +// synchronizes writes using a mutex. +type syncWriter struct { + sync.Mutex + w io.Writer +} + +// Write synchronously to the underlying io.Writer. +func (w *syncWriter) Write(p []byte) (int, error) { + w.Lock() + n, err := w.w.Write(p) + w.Unlock() + return n, err +} + +// newSyncWriter wraps an io.Writer with syncWriter. It can +// be used as an io.Writer as syncWriter satisfies the io.Writer interface. +func newSyncWriter(in io.Writer) *syncWriter { + if in == nil { + return &syncWriter{w: os.Stderr} + } + + return &syncWriter{w: in} +} + +// String representation of the log severity. +func (l Level) String() string { + switch l { + case DebugLevel: + return "debug" + case InfoLevel: + return "info" + case WarnLevel: + return "warn" + case ErrorLevel: + return "error" + case FatalLevel: + return "fatal" + default: + return "invalid lvl" + } +} + +func LevelFromString(lvl string) (Level, error) { + switch lvl { + case "debug": + return DebugLevel, nil + case "info": + return InfoLevel, nil + case "warn": + return WarnLevel, nil + case "error": + return ErrorLevel, nil + case "fatal": + return FatalLevel, nil + default: + return 0, fmt.Errorf("invalid level") + } +} + +// Debug emits a debug log line. +func (l Logger) Debug(msg string, fields ...interface{}) { + l.handleLog(msg, DebugLevel, fields...) +} + +// Info emits a info log line. +func (l Logger) Info(msg string, fields ...interface{}) { + l.handleLog(msg, InfoLevel, fields...) +} + +// Warn emits a warning log line. +func (l Logger) Warn(msg string, fields ...interface{}) { + l.handleLog(msg, WarnLevel, fields...) +} + +// Error emits an error log line. +func (l Logger) Error(msg string, fields ...interface{}) { + l.handleLog(msg, ErrorLevel, fields...) +} + +// Fatal emits a fatal level log line. +// It aborts the current program with an exit code of 1. +func (l Logger) Fatal(msg string, fields ...interface{}) { + l.handleLog(msg, FatalLevel, fields...) + exit() +} + +// handleLog emits the log after filtering log level +// and applying formatting of the fields. +func (l Logger) handleLog(msg string, lvl Level, fields ...interface{}) { + // Discard the log if the verbosity is higher. + // For eg, if the lvl is `3` (error), but the incoming message is `0` (debug), skip it. + if lvl < l.Opts.Level { + return + } + + // Get a buffer from the pool. + buf := bufPool.Get() + + // Write fixed keys to the buffer before writing user provided ones. + writeTimeToBuf(buf, l.Opts.TimestampFormat, lvl, l.Opts.EnableColor) + writeToBuf(buf, "level", lvl, lvl, l.Opts.EnableColor, true) + writeStringToBuf(buf, "message", msg, lvl, l.Opts.EnableColor, true) + + if l.Opts.EnableCaller { + writeCallerToBuf(buf, "caller", l.Opts.CallerSkipFrameCount, lvl, l.EnableColor, true) + } + + // Format the line as logfmt. + var ( + count int // to find out if this is the last key in while itering fields. + fieldCount = len(l.DefaultFields) + len(fields) + key string + val interface{} + ) + + // If there are odd number of fields, ignore the last. + if fieldCount%2 != 0 { + fields = fields[0 : len(fields)-1] + } + + for i := range l.DefaultFields { + space := false + if count != fieldCount-1 { + space = true + } + + if i%2 == 0 { + key = l.DefaultFields[i].(string) + continue + } else { + val = l.DefaultFields[i] + } + + writeToBuf(buf, key, val, lvl, l.Opts.EnableColor, space) + count++ + } + + for i := range fields { + space := false + if count != fieldCount-1 { + space = true + } + + if i%2 == 0 { + key = fields[i].(string) + continue + } else { + val = fields[i] + } + + writeToBuf(buf, key, val, lvl, l.Opts.EnableColor, space) + count++ + } + buf.AppendString("\n") + + _, err := l.out.Write(buf.Bytes()) + if err != nil { + // Should ideally never happen. + stdlog.Printf("error logging: %v", err) + } + + // Put the writer back in the pool. It resets the underlying byte buffer. + bufPool.Put(buf) +} + +// writeTimeToBuf writes timestamp key + timestamp into buffer. +func writeTimeToBuf(buf *byteBuffer, format string, lvl Level, color bool) { + if color { + buf.AppendString(getColoredKey(tsKey, lvl)) + } else { + buf.AppendString(tsKey) + } + + buf.AppendTime(time.Now(), format) + buf.AppendByte(' ') +} + +// writeStringToBuf takes key, value and additional options to write to the buffer in logfmt. +func writeStringToBuf(buf *byteBuffer, key, val string, lvl Level, color, space bool) { + if color { + escapeAndWriteString(buf, getColoredKey(key, lvl)) + } else { + escapeAndWriteString(buf, key) + } + buf.AppendByte('=') + escapeAndWriteString(buf, val) + if space { + buf.AppendByte(' ') + } +} + +func writeCallerToBuf(buf *byteBuffer, key string, depth int, lvl Level, color, space bool) { + _, file, line, ok := runtime.Caller(depth) + if !ok { + file = "???" + line = 0 + } + if color { + buf.AppendString(getColoredKey(key, lvl)) + } else { + buf.AppendString(key) + } + buf.AppendByte('=') + escapeAndWriteString(buf, file) + buf.AppendByte(':') + buf.AppendInt(int64(line)) + if space { + buf.AppendByte(' ') + } +} + +// writeToBuf takes key, value and additional options to write to the buffer in logfmt. +func writeToBuf(buf *byteBuffer, key string, val interface{}, lvl Level, color, space bool) { + if color { + escapeAndWriteString(buf, getColoredKey(key, lvl)) + } else { + escapeAndWriteString(buf, key) + } + buf.AppendByte('=') + + switch v := val.(type) { + case nil: + buf.AppendString("null") + case []byte: + escapeAndWriteString(buf, string(v)) + case string: + escapeAndWriteString(buf, v) + case int: + buf.AppendInt(int64(v)) + case int8: + buf.AppendInt(int64(v)) + case int16: + buf.AppendInt(int64(v)) + case int32: + buf.AppendInt(int64(v)) + case int64: + buf.AppendInt(v) + case float32: + buf.AppendFloat(float64(v), 32) + case float64: + buf.AppendFloat(v, 64) + case bool: + buf.AppendBool(v) + case error: + escapeAndWriteString(buf, v.Error()) + case fmt.Stringer: + escapeAndWriteString(buf, v.String()) + default: + escapeAndWriteString(buf, fmt.Sprintf("%v", val)) + } + + if space { + buf.AppendByte(' ') + } +} + +// escapeAndWriteString escapes the string if interface{} unwanted chars are there. +func escapeAndWriteString(buf *byteBuffer, s string) { + idx := strings.IndexFunc(s, checkEscapingRune) + if idx != -1 || s == "null" { + writeQuotedString(buf, s) + return + } + buf.AppendString(s) +} + +// getColoredKey returns a color formatter key based on the log level. +func getColoredKey(k string, lvl Level) string { + return colorLvlMap[lvl] + k + reset +} + +// checkEscapingRune returns true if the rune is to be escaped. +func checkEscapingRune(r rune) bool { + return r == '=' || r == ' ' || r == '"' || r == utf8.RuneError +} + +// writeQuotedString quotes a string before writing to the buffer. +// Taken from: https://github.com/go-logfmt/logfmt/blob/99455b83edb21b32a1f1c0a32f5001b77487b721/jsonstring.go#L95 +func writeQuotedString(buf *byteBuffer, s string) { + buf.AppendByte('"') + start := 0 + for i := 0; i < len(s); { + if b := s[i]; b < utf8.RuneSelf { + if 0x20 <= b && b != '\\' && b != '"' { + i++ + continue + } + if start < i { + buf.AppendString(s[start:i]) + } + switch b { + case '\\', '"': + buf.AppendByte('\\') + buf.AppendByte(b) + case '\n': + buf.AppendByte('\\') + buf.AppendByte('n') + case '\r': + buf.AppendByte('\\') + buf.AppendByte('r') + case '\t': + buf.AppendByte('\\') + buf.AppendByte('t') + default: + // This encodes bytes < 0x20 except for \n, \r, and \t. + buf.AppendString(`\u00`) + buf.AppendByte(hex[b>>4]) + buf.AppendByte(hex[b&0xF]) + } + i++ + start = i + continue + } + c, size := utf8.DecodeRuneInString(s[i:]) + if c == utf8.RuneError { + if start < i { + buf.AppendString(s[start:i]) + } + buf.AppendString(`\ufffd`) + i += size + start = i + continue + } + i += size + } + if start < len(s) { + buf.AppendString(s[start:]) + } + buf.AppendByte('"') +} diff --git a/vendor/modules.txt b/vendor/modules.txt new file mode 100644 index 0000000..db09ca9 --- /dev/null +++ b/vendor/modules.txt @@ -0,0 +1,3 @@ +# github.com/zerodha/logf v0.5.5 +## explicit; go 1.17 +github.com/zerodha/logf