From a7ace9099fa5be5abf38f255696058c26b3ef04d Mon Sep 17 00:00:00 2001 From: Ryan Hamamura <58859899+ryanhamamura@users.noreply.github.com> Date: Sat, 31 Jan 2026 08:18:24 -1000 Subject: [PATCH] feat: replace log with rs/zerolog for structured logging Switch from the standard library log package to rs/zerolog with ConsoleWriter for colorful terminal output in dev mode and JSON output in production. Users can now provide their own logger via Options.Logger or set the level via Options.LogLevel. --- configuration.go | 27 ++++++---- context.go | 3 +- go.mod | 3 ++ go.sum | 15 ++++++ internal/examples/chatroom/main.go | 2 +- internal/examples/livereload/main.go | 2 +- internal/examples/nats-chatroom/main.go | 2 +- internal/examples/realtimechart/main.go | 2 +- internal/examples/shakespeare/main.go | 2 +- via.go | 69 +++++++++++++------------ 10 files changed, 75 insertions(+), 52 deletions(-) diff --git a/configuration.go b/configuration.go index 999da00..2358c7a 100644 --- a/configuration.go +++ b/configuration.go @@ -1,15 +1,17 @@ package via -import "github.com/alexedwards/scs/v2" +import ( + "github.com/alexedwards/scs/v2" + "github.com/rs/zerolog" +) -type LogLevel int +func ptr(l zerolog.Level) *zerolog.Level { return &l } -const ( - undefined LogLevel = iota - LogLevelError - LogLevelWarn - LogLevelInfo - LogLevelDebug +var ( + LogLevelDebug = ptr(zerolog.DebugLevel) + LogLevelInfo = ptr(zerolog.InfoLevel) + LogLevelWarn = ptr(zerolog.WarnLevel) + LogLevelError = ptr(zerolog.ErrorLevel) ) // Plugin is a func that can mutate the given *via.V app runtime. It is useful to integrate popular JS/CSS UI libraries or tools. @@ -23,9 +25,12 @@ type Options struct { // The http server address. e.g. ':3000' ServerAddress string - // Level of the logs to write to stdout. - // Options: Error, Warn, Info, Debug. - LogLvl LogLevel + // LogLevel sets the minimum log level. nil keeps the default (Info). + LogLevel *zerolog.Level + + // Logger overrides the default logger entirely. When set, LogLevel and + // DevMode have no effect on logging. + Logger *zerolog.Logger // The title of the HTML document. DocumentTitle string diff --git a/context.go b/context.go index 77b4d2b..934aca9 100644 --- a/context.go +++ b/context.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "log" "maps" "reflect" "sync" @@ -456,7 +455,7 @@ func (c *Context) unsubscribeAll() { func newContext(id string, route string, v *V) *Context { if v == nil { - log.Fatal("create context failed: app pointer is nil") + panic("create context failed: app pointer is nil") } return &Context{ diff --git a/go.mod b/go.mod index e744470..83072c8 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/delaneyj/toolbelt v0.9.1 github.com/mattn/go-sqlite3 v1.14.32 github.com/nats-io/nats.go v1.48.0 + github.com/rs/zerolog v1.34.0 github.com/starfederation/datastar-go v1.0.3 github.com/stretchr/testify v1.11.1 ) @@ -24,6 +25,8 @@ require ( github.com/google/go-tpm v0.9.7 // indirect github.com/klauspost/compress v1.18.2 // indirect github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect github.com/nats-io/jwt/v2 v2.8.0 // indirect github.com/nats-io/nats-server/v2 v2.12.2 // indirect diff --git a/go.sum b/go.sum index 05cc239..0285812 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,7 @@ github.com/antithesishq/antithesis-sdk-go v0.5.0 h1:cudCFF83pDDANcXFzkQPUHHedfnn github.com/antithesishq/antithesis-sdk-go v0.5.0/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -20,6 +21,7 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/delaneyj/toolbelt v0.9.1 h1:QJComn2qoaQ4azl5uRkGpdHSO9e+JtoxDTXCiQHvH8o= github.com/delaneyj/toolbelt v0.9.1/go.mod h1:eNXpPuThjTD4tpRNCBl4JEz9jdg9LpyzNuyG+stnIbs= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k= github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s= github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA= @@ -33,6 +35,12 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= @@ -50,11 +58,15 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/starfederation/datastar-go v1.0.3 h1:DnzgsJ6tDHDM6y5Nxsk0AGW/m8SyKch2vQg3P1xGTcU= github.com/starfederation/datastar-go v1.0.3/go.mod h1:stm83LQkhZkwa5GzzdPEN6dLuu8FVwxIv0w1DYkbD3w= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -74,6 +86,9 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/internal/examples/chatroom/main.go b/internal/examples/chatroom/main.go index 1c7ad03..fe083b8 100644 --- a/internal/examples/chatroom/main.go +++ b/internal/examples/chatroom/main.go @@ -22,7 +22,7 @@ func main() { v.Config(via.Options{ DevMode: true, DocumentTitle: "ViaChat", - LogLvl: via.LogLevelInfo, + LogLevel: via.LogLevelInfo, }) v.AppendToHead( diff --git a/internal/examples/livereload/main.go b/internal/examples/livereload/main.go index 2be847c..f1536df 100644 --- a/internal/examples/livereload/main.go +++ b/internal/examples/livereload/main.go @@ -14,7 +14,7 @@ func main() { v.Config(via.Options{ DocumentTitle: "Live Reload Demo", DevMode: true, - LogLvl: via.LogLevelDebug, + LogLevel: via.LogLevelDebug, Plugins: []via.Plugin{ // picocss.Default }, diff --git a/internal/examples/nats-chatroom/main.go b/internal/examples/nats-chatroom/main.go index 9bbdf19..f5c5879 100644 --- a/internal/examples/nats-chatroom/main.go +++ b/internal/examples/nats-chatroom/main.go @@ -60,7 +60,7 @@ func main() { v.Config(via.Options{ DevMode: true, DocumentTitle: "NATS Chat", - LogLvl: via.LogLevelInfo, + LogLevel: via.LogLevelInfo, ServerAddress: ":7331", PubSub: ps, }) diff --git a/internal/examples/realtimechart/main.go b/internal/examples/realtimechart/main.go index 54cf726..af794b0 100644 --- a/internal/examples/realtimechart/main.go +++ b/internal/examples/realtimechart/main.go @@ -14,7 +14,7 @@ func main() { v := via.New() v.Config(via.Options{ - LogLvl: via.LogLevelDebug, + LogLevel: via.LogLevelDebug, DevMode: true, Plugins: []via.Plugin{ // picocss.Default, diff --git a/internal/examples/shakespeare/main.go b/internal/examples/shakespeare/main.go index ff48cf3..02b6dcc 100644 --- a/internal/examples/shakespeare/main.go +++ b/internal/examples/shakespeare/main.go @@ -54,7 +54,7 @@ func main() { v.Config(via.Options{ DevMode: true, DocumentTitle: "Search", - LogLvl: via.LogLevelWarn, + LogLevel: via.LogLevelWarn, }) v.AppendToHead( diff --git a/via.go b/via.go index 2f03f08..ee488e3 100644 --- a/via.go +++ b/via.go @@ -13,7 +13,6 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" "net/url" "os" @@ -22,6 +21,7 @@ import ( "sync" "github.com/alexedwards/scs/v2" + "github.com/rs/zerolog" "github.com/ryanhamamura/via/h" "github.com/starfederation/datastar-go/datastar" ) @@ -34,6 +34,7 @@ var datastarJS []byte type V struct { cfg Options mux *http.ServeMux + logger zerolog.Logger contextRegistry map[string]*Context contextRegistryMutex sync.RWMutex documentHeadIncludes []h.H @@ -46,52 +47,52 @@ type V struct { datastarOnce sync.Once } +func (v *V) logEvent(evt *zerolog.Event, c *Context) *zerolog.Event { + if c != nil && c.id != "" { + evt = evt.Str("via-ctx", c.id) + } + return evt +} + func (v *V) logFatal(format string, a ...any) { - log.Printf("[fatal] msg=%q", fmt.Sprintf(format, a...)) + v.logEvent(v.logger.WithLevel(zerolog.FatalLevel), nil).Msgf(format, a...) } func (v *V) logErr(c *Context, format string, a ...any) { - cRef := "" - if c != nil && c.id != "" { - cRef = fmt.Sprintf("via-ctx=%q ", c.id) - } - log.Printf("[error] %smsg=%q", cRef, fmt.Sprintf(format, a...)) + v.logEvent(v.logger.Error(), c).Msgf(format, a...) } func (v *V) logWarn(c *Context, format string, a ...any) { - cRef := "" - if c != nil && c.id != "" { - cRef = fmt.Sprintf("via-ctx=%q ", c.id) - } - if v.cfg.LogLvl >= LogLevelWarn { - log.Printf("[warn] %smsg=%q", cRef, fmt.Sprintf(format, a...)) - } + v.logEvent(v.logger.Warn(), c).Msgf(format, a...) } func (v *V) logInfo(c *Context, format string, a ...any) { - cRef := "" - if c != nil && c.id != "" { - cRef = fmt.Sprintf("via-ctx=%q ", c.id) - } - if v.cfg.LogLvl >= LogLevelInfo { - log.Printf("[info] %smsg=%q", cRef, fmt.Sprintf(format, a...)) - } + v.logEvent(v.logger.Info(), c).Msgf(format, a...) } func (v *V) logDebug(c *Context, format string, a ...any) { - cRef := "" - if c != nil && c.id != "" { - cRef = fmt.Sprintf("via-ctx=%q ", c.id) - } - if v.cfg.LogLvl == LogLevelDebug { - log.Printf("[debug] %smsg=%q", cRef, fmt.Sprintf(format, a...)) - } + v.logEvent(v.logger.Debug(), c).Msgf(format, a...) +} + +func newConsoleLogger(level zerolog.Level) zerolog.Logger { + return zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"}). + With().Timestamp().Logger().Level(level) } // Config overrides the default configuration with the given options. func (v *V) Config(cfg Options) { - if cfg.LogLvl != undefined { - v.cfg.LogLvl = cfg.LogLvl + if cfg.Logger != nil { + v.logger = *cfg.Logger + } else if cfg.LogLevel != nil || cfg.DevMode != v.cfg.DevMode { + level := zerolog.InfoLevel + if cfg.LogLevel != nil { + level = *cfg.LogLevel + } + if cfg.DevMode { + v.logger = newConsoleLogger(level) + } else { + v.logger = zerolog.New(os.Stderr).With().Timestamp().Logger().Level(level) + } } if cfg.DocumentTitle != "" { v.cfg.DocumentTitle = cfg.DocumentTitle @@ -260,7 +261,7 @@ func (v *V) Start() { if v.sessionManager != nil { handler = v.sessionManager.LoadAndSave(v.mux) } - log.Fatalf("[fatal] %v", http.ListenAndServe(v.cfg.ServerAddress, handler)) + v.logger.Fatal().Err(http.ListenAndServe(v.cfg.ServerAddress, handler)).Msg("http server failed") } // HTTPServeMux returns the underlying HTTP request multiplexer to enable user extentions, middleware and @@ -284,7 +285,7 @@ func (v *V) ensureDatastarHandler() { func (v *V) devModePersist(c *Context) { p := filepath.Join(".via", "devmode", "ctx.json") if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil { - log.Fatalf("failed to create directory for devmode files: %v", err) + v.logFatal("failed to create directory for devmode files: %v", err) } // load persisted list from file, or empty list if file not found @@ -398,6 +399,7 @@ func New() *V { v := &V{ mux: mux, + logger: newConsoleLogger(zerolog.InfoLevel), contextRegistry: make(map[string]*Context), devModePageInitFnMap: make(map[string]func(*Context)), sessionManager: scs.New(), @@ -406,7 +408,6 @@ func New() *V { cfg: Options{ DevMode: false, ServerAddress: ":3000", - LogLvl: LogLevelInfo, DocumentTitle: "⚡ Via", }, } @@ -518,7 +519,7 @@ func New() *V { v.mux.HandleFunc("POST /_session/close", func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { - log.Printf("Error reading body: %v", err) + v.logErr(nil, "error reading body: %v", err) w.WriteHeader(http.StatusBadRequest) return }