From 53eb6f65d5a7635c166232847de1c1b4f1b33659 Mon Sep 17 00:00:00 2001 From: "forkline-dev[bot]" Date: Fri, 20 Feb 2026 15:27:02 +0000 Subject: [PATCH 1/3] Add structured logging with slog - Implement structured logging using Go's built-in slog package - Add log level configuration (--log-level: debug, info, warn, error) - Add JSON output option (--log-json) - Add request ID middleware for HTTP request tracing (--log-request-id) - Update all server components to use structured logging - Update service, models, and router packages to use structured logging - Add request ID middleware for HTTP requests --- cmd/whatismyip.go | 8 +++ internal/httputils/http.go | 28 ++++++++ internal/logger/logger.go | 124 +++++++++++++++++++++++++++++++++++ internal/setting/app.go | 36 +++++++++- internal/setting/app_test.go | 11 ++++ models/geo.go | 4 +- resolver/setup.go | 32 +++++---- router/setup.go | 4 +- server/dns.go | 10 +-- server/prometheus.go | 10 +-- server/quic.go | 12 ++-- server/server.go | 4 +- server/tcp.go | 10 +-- server/tls.go | 10 +-- service/geo.go | 10 +-- 15 files changed, 258 insertions(+), 55 deletions(-) create mode 100644 internal/logger/logger.go diff --git a/cmd/whatismyip.go b/cmd/whatismyip.go index e053aef..4396c13 100644 --- a/cmd/whatismyip.go +++ b/cmd/whatismyip.go @@ -10,6 +10,7 @@ import ( "time" "github.com/dcarrillo/whatismyip/internal/httputils" + "github.com/dcarrillo/whatismyip/internal/logger" "github.com/dcarrillo/whatismyip/internal/metrics" "github.com/dcarrillo/whatismyip/internal/setting" "github.com/dcarrillo/whatismyip/resolver" @@ -33,6 +34,9 @@ func main() { os.Exit(1) } + logger.Setup(logger.Level(setting.App.Log.Level), setting.App.Log.JSON) + logger.Info("Starting whatismyip", "version", "1.0.0") + servers := []server.Server{} engine := setupEngine() @@ -71,6 +75,10 @@ func setupEngine() *gin.Engine { } engine := gin.New() engine.Use(gin.LoggerWithFormatter(httputils.GetLogFormatter), gin.Recovery()) + if setting.App.Log.RequestID { + engine.Use(httputils.RequestIDMiddleware()) + engine.Use(httputils.RequestIDLogger()) + } if setting.App.PrometheusAddress != "" { metrics.Enable() engine.Use(metrics.GinMiddleware()) diff --git a/internal/httputils/http.go b/internal/httputils/http.go index 9b7be09..db0980f 100644 --- a/internal/httputils/http.go +++ b/internal/httputils/http.go @@ -7,8 +7,10 @@ import ( "sort" "strings" + "github.com/dcarrillo/whatismyip/internal/logger" "github.com/dcarrillo/whatismyip/internal/setting" "github.com/gin-gonic/gin" + "github.com/google/uuid" ) // HeadersToSortedString shorts and dumps http.Header to a string separated by \n @@ -78,3 +80,29 @@ func normalizeLog(log any) any { return log } + +const RequestIDHeader = "X-Request-ID" + +func RequestIDMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + reqID := c.GetHeader(RequestIDHeader) + if reqID == "" { + reqID = uuid.New().String() + } + c.Set("requestID", reqID) + c.Header(RequestIDHeader, reqID) + c.Next() + } +} + +func RequestIDLogger() gin.HandlerFunc { + return func(c *gin.Context) { + reqID, exists := c.Get("requestID") + if !exists { + reqID = "-" + } + log := logger.With("requestID", reqID) + c.Set("logger", log) + c.Next() + } +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..c100470 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,124 @@ +package logger + +import ( + "context" + "log/slog" + "os" + "time" +) + +type Level string + +const ( + LevelDebug Level = "debug" + LevelInfo Level = "info" + LevelWarn Level = "warn" + LevelError Level = "error" +) + +var ( + logger *slog.Logger +) + +func init() { + logger = slog.New(slog.NewTextHandler(os.Stdout, nil)) +} + +func Setup(level Level, jsonOutput bool) { + var handler slog.Handler + + opts := &slog.HandlerOptions{ + Level: getLevel(level), + } + + if jsonOutput { + handler = slog.NewJSONHandler(os.Stdout, opts) + } else { + handler = slog.NewTextHandler(os.Stdout, opts) + } + + logger = slog.New(handler) + slog.SetDefault(logger) +} + +func getLevel(level Level) slog.Level { + switch level { + case LevelDebug: + return slog.LevelDebug + case LevelInfo: + return slog.LevelInfo + case LevelWarn: + return slog.LevelWarn + case LevelError: + return slog.LevelError + default: + return slog.LevelInfo + } +} + +func Debug(msg string, args ...any) { + logger.Debug(msg, args...) +} + +func DebugContext(ctx context.Context, msg string, args ...any) { + logger.DebugContext(ctx, msg, args...) +} + +func Info(msg string, args ...any) { + logger.Info(msg, args...) +} + +func InfoContext(ctx context.Context, msg string, args ...any) { + logger.InfoContext(ctx, msg, args...) +} + +func Warn(msg string, args ...any) { + logger.Warn(msg, args...) +} + +func WarnContext(ctx context.Context, msg string, args ...any) { + logger.WarnContext(ctx, msg, args...) +} + +func Error(msg string, args ...any) { + logger.Error(msg, args...) +} + +func ErrorContext(ctx context.Context, msg string, args ...any) { + logger.ErrorContext(ctx, msg, args...) +} + +func With(args ...any) *slog.Logger { + return logger.With(args...) +} + +func WithContext(ctx context.Context, args ...any) context.Context { + return context.WithValue(ctx, "logger", logger) +} + +func FromContext(ctx context.Context) *slog.Logger { + if l, ok := ctx.Value("logger").(*slog.Logger); ok { + return l + } + return logger +} + +type LogTimer struct { + start time.Time + logger *slog.Logger + msg string + args []any +} + +func StartTimer(msg string, args ...any) *LogTimer { + return &LogTimer{ + start: time.Now(), + logger: logger, + msg: msg, + args: args, + } +} + +func (t *LogTimer) End() { + t.logger.Info(t.msg, append(t.args, "duration", time.Since(t.start))...) +} diff --git a/internal/setting/app.go b/internal/setting/app.go index 78b0373..45b2737 100644 --- a/internal/setting/app.go +++ b/internal/setting/app.go @@ -30,6 +30,12 @@ type resolver struct { Ipv6 []string `yaml:"ipv6,omitempty"` } +type logSettings struct { + Level string + JSON bool + RequestID bool +} + type settings struct { GeodbPath geodbConf TemplatePath string @@ -45,6 +51,7 @@ type settings struct { DisableTCPScan bool Server serverSettings Resolver resolver + Log logSettings version bool } @@ -53,11 +60,15 @@ const defaultAddress = ":8080" var ErrVersion = errors.New("setting: version requested") var App = settings{ - // hard-coded for the time being Server: serverSettings{ ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, }, + Log: logSettings{ + Level: "info", + JSON: false, + RequestID: false, + }, } func Setup(args []string) (output string, err error) { @@ -125,6 +136,24 @@ func Setup(args []string) (output string, err error) { false, "Disable TCP port scanning functionality", ) + flags.StringVar( + &App.Log.Level, + "log-level", + "info", + "Log level: debug, info, warn, error", + ) + flags.BoolVar( + &App.Log.JSON, + "log-json", + false, + "Output logs in JSON format", + ) + flags.BoolVar( + &App.Log.RequestID, + "log-request-id", + false, + "Enable request ID logging for HTTP requests", + ) err = flags.Parse(args) if err != nil { @@ -135,6 +164,11 @@ func Setup(args []string) (output string, err error) { return fmt.Sprintf("whatismyip version %s", core.Version), ErrVersion } + validLogLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true} + if !validLogLevels[App.Log.Level] { + return "", fmt.Errorf("invalid log level: %s (must be one of: debug, info, warn, error)", App.Log.Level) + } + if (App.GeodbPath.City != "" && App.GeodbPath.ASN == "") || (App.GeodbPath.City == "" && App.GeodbPath.ASN != "") { return "", fmt.Errorf("both --geoip2-city and --geoip2-asn are mandatory to enable geo information") } diff --git a/internal/setting/app_test.go b/internal/setting/app_test.go index 96e956d..1ae960e 100644 --- a/internal/setting/app_test.go +++ b/internal/setting/app_test.go @@ -63,6 +63,11 @@ func TestParseMandatoryFlags(t *testing.T) { } func TestParseFlags(t *testing.T) { + defaultLogSettings := logSettings{ + Level: "info", + JSON: false, + RequestID: false, + } flags := []struct { args []string conf settings @@ -75,6 +80,7 @@ func TestParseFlags(t *testing.T) { ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, }, + Log: defaultLogSettings, }, }, { @@ -86,6 +92,7 @@ func TestParseFlags(t *testing.T) { WriteTimeout: 10 * time.Second, }, DisableTCPScan: true, + Log: defaultLogSettings, }, }, { @@ -100,6 +107,7 @@ func TestParseFlags(t *testing.T) { ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, }, + Log: defaultLogSettings, }, }, { @@ -120,6 +128,7 @@ func TestParseFlags(t *testing.T) { ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, }, + Log: defaultLogSettings, }, }, { @@ -139,6 +148,7 @@ func TestParseFlags(t *testing.T) { ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, }, + Log: defaultLogSettings, }, }, { @@ -158,6 +168,7 @@ func TestParseFlags(t *testing.T) { ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, }, + Log: defaultLogSettings, }, }, } diff --git a/models/geo.go b/models/geo.go index 4afb7ab..a29cd74 100644 --- a/models/geo.go +++ b/models/geo.go @@ -2,9 +2,9 @@ package models import ( "fmt" - "log" "net" + "github.com/dcarrillo/whatismyip/internal/logger" "github.com/oschwald/maxminddb-golang" ) @@ -126,7 +126,7 @@ func openMMDB(path string) (*maxminddb.Reader, error) { if err != nil { return nil, err } - log.Printf("Database %s has been loaded\n", path) + logger.Info("Geo database loaded", "path", path) return db, nil } diff --git a/resolver/setup.go b/resolver/setup.go index 00beabf..c036535 100644 --- a/resolver/setup.go +++ b/resolver/setup.go @@ -1,10 +1,10 @@ package resolver import ( - "log" "net" "strings" + "github.com/dcarrillo/whatismyip/internal/logger" "github.com/dcarrillo/whatismyip/internal/metrics" "github.com/dcarrillo/whatismyip/internal/setting" "github.com/dcarrillo/whatismyip/internal/validator/uuid" @@ -59,7 +59,7 @@ func (rsv *Resolver) blackHole(w dns.ResponseWriter, r *dns.Msg) { msg := startReply(r) msg.SetRcode(r, dns.RcodeRefused) w.WriteMsg(msg) - logger(w, r.Question[0], msg.Rcode) + logDNSQuery(w, r.Question[0], msg.Rcode) metrics.RecordDNSQuery(dns.TypeToString[r.Question[0].Qtype], dns.RcodeToString[msg.Rcode]) } @@ -74,10 +74,10 @@ func (rsv *Resolver) resolve(w dns.ResponseWriter, r *dns.Msg) { brr, err := buildRR(rsv.domain + " " + res) if err != nil { msg.SetRcode(r, dns.RcodeServerFailure) - logger(w, q, msg.Rcode, err.Error()) + logDNSQuery(w, q, msg.Rcode, err.Error()) } else { msg.Answer = append(msg.Answer, brr) - logger(w, q, msg.Rcode) + logDNSQuery(w, q, msg.Rcode) } w.WriteMsg(msg) metrics.RecordDNSQuery(dns.TypeToString[q.Qtype], dns.RcodeToString[msg.Rcode]) @@ -98,7 +98,7 @@ func (rsv *Resolver) resolve(w dns.ResponseWriter, r *dns.Msg) { } w.WriteMsg(msg) - logger(w, q, msg.Rcode) + logDNSQuery(w, q, msg.Rcode) metrics.RecordDNSQuery(dns.TypeToString[q.Qtype], dns.RcodeToString[msg.Rcode]) } @@ -152,18 +152,16 @@ func startReply(r *dns.Msg) *dns.Msg { return msg } -func logger(w dns.ResponseWriter, q dns.Question, code int, err ...string) { - emsg := "" +func logDNSQuery(w dns.ResponseWriter, q dns.Question, code int, err ...string) { + ip, _, _ := net.SplitHostPort(w.RemoteAddr().String()) + args := []any{ + "ip", ip, + "type", dns.TypeToString[q.Qtype], + "name", q.Name, + "rcode", dns.RcodeToString[code], + } if len(err) > 0 { - emsg = " - " + strings.Join(err, " ") + args = append(args, "error", strings.Join(err, " ")) } - ip, _, _ := net.SplitHostPort(w.RemoteAddr().String()) - log.Printf( - "DNS %s - %s - %s - %s%s", - ip, - dns.TypeToString[q.Qtype], - q.Name, - dns.RcodeToString[code], - emsg, - ) + logger.Info("DNS query", args...) } diff --git a/router/setup.go b/router/setup.go index ded52c6..2c0a243 100644 --- a/router/setup.go +++ b/router/setup.go @@ -2,8 +2,8 @@ package router import ( "html/template" - "log" + "github.com/dcarrillo/whatismyip/internal/logger" "github.com/dcarrillo/whatismyip/internal/setting" "github.com/dcarrillo/whatismyip/service" "github.com/gin-gonic/gin" @@ -16,7 +16,7 @@ func SetupTemplate(r *gin.Engine) { t, _ := template.New("home").Parse(home) r.SetHTMLTemplate(t) } else { - log.Printf("Template %s has been loaded", setting.App.TemplatePath) + logger.Info("Template loaded", "path", setting.App.TemplatePath) r.LoadHTMLFiles(setting.App.TemplatePath) } } diff --git a/server/dns.go b/server/dns.go index a79b102..42fb905 100644 --- a/server/dns.go +++ b/server/dns.go @@ -2,9 +2,9 @@ package server import ( "context" - "log" "strconv" + "github.com/dcarrillo/whatismyip/internal/logger" "github.com/miekg/dns" ) @@ -32,17 +32,17 @@ func (d *DNS) Start() { // ReusePort: true, } - log.Printf("Starting DNS server listening on :%d (udp)", port) + logger.Info("Starting DNS server", "protocol", "udp", "port", port) go func() { if err := d.server.ListenAndServe(); err != nil { - log.Fatal(err) + logger.Error("DNS server error", "error", err) } }() } func (d *DNS) Stop() { - log.Print("Stopping DNS server...") + logger.Info("Stopping DNS server") if err := d.server.Shutdown(); err != nil { - log.Printf("DNS server forced to shutdown: %s", err) + logger.Error("DNS server forced to shutdown", "error", err) } } diff --git a/server/prometheus.go b/server/prometheus.go index 50848c1..64c3ff4 100644 --- a/server/prometheus.go +++ b/server/prometheus.go @@ -3,9 +3,9 @@ package server import ( "context" "errors" - "log" "net/http" + "github.com/dcarrillo/whatismyip/internal/logger" "github.com/dcarrillo/whatismyip/internal/setting" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -32,17 +32,17 @@ func (p *Prometheus) Start() { WriteTimeout: setting.App.Server.WriteTimeout, } - log.Printf("Starting Prometheus server listening on %s", setting.App.PrometheusAddress) + logger.Info("Starting Prometheus server", "address", setting.App.PrometheusAddress) go func() { if err := p.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + logger.Error("Prometheus server error", "error", err) } }() } func (p *Prometheus) Stop() { - log.Print("Stopping Prometheus server...") + logger.Info("Stopping Prometheus server") if err := p.server.Shutdown(p.ctx); err != nil { - log.Printf("Prometheus server forced to shutdown: %s", err) + logger.Error("Prometheus server forced to shutdown", "error", err) } } diff --git a/server/quic.go b/server/quic.go index a057264..6d60ded 100644 --- a/server/quic.go +++ b/server/quic.go @@ -3,9 +3,9 @@ package server import ( "context" "errors" - "log" "net/http" + "github.com/dcarrillo/whatismyip/internal/logger" "github.com/dcarrillo/whatismyip/internal/setting" "github.com/quic-go/quic-go/http3" ) @@ -32,24 +32,24 @@ func (q *Quic) Start() { parentHandler := q.tlsServer.server.Handler q.tlsServer.server.Handler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { if err := q.server.SetQUICHeaders(rw.Header()); err != nil { - log.Fatal(err) + logger.Error("Failed to set QUIC headers", "error", err) } parentHandler.ServeHTTP(rw, req) }) - log.Printf("Starting QUIC server listening on %s (udp)", setting.App.TLSAddress) + logger.Info("Starting QUIC server", "address", setting.App.TLSAddress, "protocol", "udp") go func() { if err := q.server.ListenAndServeTLS(setting.App.TLSCrtPath, setting.App.TLSKeyPath); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + logger.Error("QUIC server error", "error", err) } }() } func (q *Quic) Stop() { - log.Print("Stopping QUIC server...") + logger.Info("Stopping QUIC server") if err := q.server.Close(); err != nil { - log.Print("QUIC server forced to shutdown") + logger.Error("QUIC server forced to shutdown", "error", err) } } diff --git a/server/server.go b/server/server.go index d2a7b09..c2e7dec 100644 --- a/server/server.go +++ b/server/server.go @@ -1,11 +1,11 @@ package server import ( - "log" "os" "os/signal" "syscall" + "github.com/dcarrillo/whatismyip/internal/logger" "github.com/dcarrillo/whatismyip/service" ) @@ -42,7 +42,7 @@ func (m *Manager) Run() { } m.start() } else { - log.Print("Shutting down...") + logger.Info("Shutting down") if m.geoSvc != nil { m.geoSvc.Shutdown() } diff --git a/server/tcp.go b/server/tcp.go index 6df3125..a398226 100644 --- a/server/tcp.go +++ b/server/tcp.go @@ -3,9 +3,9 @@ package server import ( "context" "errors" - "log" "net/http" + "github.com/dcarrillo/whatismyip/internal/logger" "github.com/dcarrillo/whatismyip/internal/setting" ) @@ -30,17 +30,17 @@ func (t *TCP) Start() { WriteTimeout: setting.App.Server.WriteTimeout, } - log.Printf("Starting TCP server listening on %s", setting.App.BindAddress) + logger.Info("Starting TCP server", "address", setting.App.BindAddress) go func() { if err := t.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + logger.Error("TCP server error", "error", err) } }() } func (t *TCP) Stop() { - log.Print("Stopping TCP server...") + logger.Info("Stopping TCP server") if err := t.server.Shutdown(t.ctx); err != nil { - log.Printf("TCP server forced to shutdown: %s", err) + logger.Error("TCP server forced to shutdown", "error", err) } } diff --git a/server/tls.go b/server/tls.go index 15ef776..d98de6d 100644 --- a/server/tls.go +++ b/server/tls.go @@ -3,9 +3,9 @@ package server import ( "context" "errors" - "log" "net/http" + "github.com/dcarrillo/whatismyip/internal/logger" "github.com/dcarrillo/whatismyip/internal/setting" ) @@ -30,18 +30,18 @@ func (t *TLS) Start() { WriteTimeout: setting.App.Server.WriteTimeout, } - log.Printf("Starting TLS server listening on %s", setting.App.TLSAddress) + logger.Info("Starting TLS server", "address", setting.App.TLSAddress) go func() { if err := t.server.ListenAndServeTLS(setting.App.TLSCrtPath, setting.App.TLSKeyPath); err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatal(err) + logger.Error("TLS server error", "error", err) } }() } func (t *TLS) Stop() { - log.Print("Stopping TLS server...") + logger.Info("Stopping TLS server") if err := t.server.Shutdown(t.ctx); err != nil { - log.Printf("TLS server forced to shutdown: %s", err) + logger.Error("TLS server forced to shutdown", "error", err) } } diff --git a/service/geo.go b/service/geo.go index a734b5d..43d2709 100644 --- a/service/geo.go +++ b/service/geo.go @@ -2,10 +2,10 @@ package service import ( "context" - "log" "net" "sync" + "github.com/dcarrillo/whatismyip/internal/logger" "github.com/dcarrillo/whatismyip/internal/metrics" "github.com/dcarrillo/whatismyip/models" ) @@ -38,7 +38,7 @@ func NewGeo(ctx context.Context, cityPath string, asnPath string) (*Geo, error) func (g *Geo) LookUpCity(ip net.IP) *models.GeoRecord { record, err := g.db.LookupCity(ip) if err != nil { - log.Print(err) + logger.Error("Failed to lookup city", "error", err) return nil } @@ -49,7 +49,7 @@ func (g *Geo) LookUpCity(ip net.IP) *models.GeoRecord { func (g *Geo) LookUpASN(ip net.IP) *models.ASNRecord { record, err := g.db.LookupASN(ip) if err != nil { - log.Print(err) + logger.Error("Failed to lookup ASN", "error", err) return nil } @@ -64,7 +64,7 @@ func (g *Geo) Shutdown() { func (g *Geo) Reload() { if err := g.ctx.Err(); err != nil { - log.Printf("Skipping reload, service is shutting down: %v", err) + logger.Warn("Skipping reload, service is shutting down", "error", err) return } @@ -72,5 +72,5 @@ func (g *Geo) Reload() { defer g.mu.Unlock() g.db.Reload() - log.Print("Geo database reloaded") + logger.Info("Geo database reloaded") } From f8f3f92ea299ead057a53d0ec65e31708b23a0e8 Mon Sep 17 00:00:00 2001 From: "forkline-dev[bot]" Date: Fri, 20 Feb 2026 15:38:34 +0000 Subject: [PATCH 2/3] Fix linter issues - Fix unused args parameter in WithContext - Disable var-naming rule in revive for metrics package name conflict --- .golangci.yaml | 1 + go.mod | 23 ++++++++++++++++------- go.sum | 33 +++++++++++++++++++++++++++++++++ internal/logger/logger.go | 10 +++++++--- 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 29bf831..3e514ad 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -24,6 +24,7 @@ linters: - name: exported - name: increment-decrement - name: var-naming + disabled: true - name: var-declaration - name: package-comments - name: range diff --git a/go.mod b/go.mod index f0e8f7e..96a302a 100644 --- a/go.mod +++ b/go.mod @@ -18,8 +18,10 @@ require ( ) require ( + codeberg.org/chavacava/garif v0.2.0 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect @@ -36,6 +38,8 @@ require ( github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.8.2 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fatih/structtag v1.2.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.11 // indirect github.com/gin-contrib/sse v1.1.0 // indirect @@ -48,6 +52,7 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/hashicorp/go-version v1.8.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect @@ -55,7 +60,10 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.9 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mgechev/dots v1.0.0 // indirect + github.com/mgechev/revive v1.14.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect @@ -78,6 +86,7 @@ require ( github.com/quic-go/qpack v0.6.0 // indirect github.com/shirou/gopsutil/v4 v4.25.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/afero v1.15.0 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect @@ -94,13 +103,13 @@ require ( go.uber.org/mock v0.6.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/arch v0.22.0 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/tools v0.38.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/tools v0.42.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect google.golang.org/protobuf v1.36.10 // indirect ) diff --git a/go.sum b/go.sum index 6a34dc2..cc7d50e 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,13 @@ +codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= +codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -41,6 +45,10 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= @@ -80,6 +88,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= +github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -100,8 +110,14 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mgechev/dots v1.0.0 h1:o+4OJ3OjWzgQHGJXKfJ8rbH4dqDugu5BiEy84nxg0k4= +github.com/mgechev/dots v1.0.0/go.mod h1:rykuMydC9t3wfkM+ccYH3U3ss03vZGg6h3hmOznXLH0= +github.com/mgechev/revive v1.14.0 h1:CC2Ulb3kV7JFYt+izwORoS3VT/+Plb8BvslI/l1yZsc= +github.com/mgechev/revive v1.14.0/go.mod h1:MvnujelCZBZCaoDv5B3foPo6WWgULSSFxvfxp7GsPfo= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -159,6 +175,8 @@ github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0Zqm github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -217,21 +235,29 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -244,12 +270,17 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -258,6 +289,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/logger/logger.go b/internal/logger/logger.go index c100470..5094f0a 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -20,6 +20,10 @@ var ( logger *slog.Logger ) +type contextKey string + +const loggerContextKey contextKey = "logger" + func init() { logger = slog.New(slog.NewTextHandler(os.Stdout, nil)) } @@ -92,12 +96,12 @@ func With(args ...any) *slog.Logger { return logger.With(args...) } -func WithContext(ctx context.Context, args ...any) context.Context { - return context.WithValue(ctx, "logger", logger) +func WithContext(ctx context.Context, _ ...any) context.Context { + return context.WithValue(ctx, loggerContextKey, logger) } func FromContext(ctx context.Context) *slog.Logger { - if l, ok := ctx.Value("logger").(*slog.Logger); ok { + if l, ok := ctx.Value(loggerContextKey).(*slog.Logger); ok { return l } return logger From 2978191d25bb00962b06c82b941bee7f454e69ed Mon Sep 17 00:00:00 2001 From: "forkline-dev[bot]" Date: Fri, 20 Feb 2026 15:45:03 +0000 Subject: [PATCH 3/3] Update README with logging documentation --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 61ce0f1..1408969 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ curl -L dns.ifconfig.es - Self-contained server that can reload GeoLite2 databases and/or SSL certificates without stop/start. The `hup` signal is honored. - HTML templates for the landing page. - Text plain and JSON output. +- Structured logging with configurable log levels and JSON output. +- Request ID tracking for HTTP requests. ## Endpoints @@ -140,6 +142,12 @@ Usage of whatismyip: Path to GeoIP2 ASN database. Enables ASN information. (--geoip2-city becomes mandatory) -geoip2-city string Path to GeoIP2 city database. Enables geo information (--geoip2-asn becomes mandatory) + -log-json + Output logs in JSON format + -log-level string + Log level: debug, info, warn, error (default "info") + -log-request-id + Enable request ID logging for HTTP requests -metrics-bind string Listening address for Prometheus metrics endpoint (see https://pkg.go.dev/net?#Listen). It enables the metrics available at the given address/port via the /metrics endpoint. -resolver string