diff --git a/cmd/server/main.go b/cmd/server/main.go index b6036b2..04e9ba9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -18,19 +18,66 @@ import ( "aiaa-notification-service/internal/store" "github.com/gin-gonic/gin" + "github.com/logbull/logbull-go/logbull" ) func main() { - logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) - slog.SetDefault(logger) - - // Load config + // Load config first (need logbull settings for logger setup) cfg, err := config.Load("config/config.yaml") if err != nil { slog.Error("failed to load config", "error", err) os.Exit(1) } + // Setup dual logger: stdout JSON + Logbull remote + stdoutHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}) + + var logbullHandler slog.Handler + if cfg.Logbull.Host != "" { + lbCfg := logbull.Config{ + Host: cfg.Logbull.Host, + ProjectID: cfg.Logbull.ProjectID, + APIKey: cfg.Logbull.APIKey, + } + switch cfg.Logbull.LogLevel { + case "DEBUG": + lbCfg.LogLevel = logbull.DEBUG + case "WARNING": + lbCfg.LogLevel = logbull.WARNING + case "ERROR": + lbCfg.LogLevel = logbull.ERROR + default: + lbCfg.LogLevel = logbull.INFO + } + h, err := logbull.NewSlogHandler(lbCfg) + if err != nil { + slog.Warn("logbull init failed, using stdout only", "error", err) + } else { + logbullHandler = h + } + } + + var baseHandler slog.Handler + if logbullHandler != nil { + baseHandler = &teeHandler{handlers: []slog.Handler{stdoutHandler, logbullHandler}} + } else { + baseHandler = stdoutHandler + } + + logger := slog.New(baseHandler) + slog.SetDefault(logger) + + // Flush logbull on exit + defer func() { + if lbh, ok := logbullHandler.(interface{ Flush() }); ok { + lbh.Flush() + } + if lbh, ok := logbullHandler.(interface{ Shutdown() }); ok { + lbh.Shutdown() + } + time.Sleep(2 * time.Second) // allow async flush + }() + // Connect MySQL st, err := store.NewStore(cfg.Database) if err != nil { @@ -158,3 +205,44 @@ func main() { } slog.Info("server stopped") } + +// teeHandler fans out log records to multiple handlers (stdout + remote). +type teeHandler struct { + handlers []slog.Handler +} + +func (t *teeHandler) Enabled(ctx context.Context, level slog.Level) bool { + for _, h := range t.handlers { + if h.Enabled(ctx, level) { + return true + } + } + return false +} + +func (t *teeHandler) Handle(ctx context.Context, r slog.Record) error { + for _, h := range t.handlers { + if h.Enabled(ctx, r.Level) { + if err := h.Handle(ctx, r.Clone()); err != nil { + return err + } + } + } + return nil +} + +func (t *teeHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + handlers := make([]slog.Handler, len(t.handlers)) + for i, h := range t.handlers { + handlers[i] = h.WithAttrs(attrs) + } + return &teeHandler{handlers: handlers} +} + +func (t *teeHandler) WithGroup(name string) slog.Handler { + handlers := make([]slog.Handler, len(t.handlers)) + for i, h := range t.handlers { + handlers[i] = h.WithGroup(name) + } + return &teeHandler{handlers: handlers} +} diff --git a/config/config.yaml b/config/config.yaml index c7d3905..a6df5a8 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -24,3 +24,9 @@ smtp: rate_limit: default: 100 + +logbull: + host: "https://log.516886.xyz" + project_id: "42a3fef0-2fd6-4ce4-80c5-f6bb6ecc2013" + api_key: "lb_60701971723797ed0374aa3896078fe5" + log_level: "INFO" diff --git a/go.mod b/go.mod index bb606ed..408bb62 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/gin-gonic/gin v1.12.0 github.com/go-sql-driver/mysql v1.10.0 github.com/jmoiron/sqlx v1.4.0 + github.com/logbull/logbull-go v0.2.0 github.com/redis/go-redis/v9 v9.21.0 github.com/spf13/viper v1.21.0 ) @@ -36,6 +37,7 @@ require ( github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect @@ -45,6 +47,8 @@ require ( github.com/ugorji/go/codec v1.3.1 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.48.0 // indirect diff --git a/go.sum b/go.sum index 23d55b1..9baccee 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/logbull/logbull-go v0.2.0 h1:lzY7otH0cF7U4BqC8EJj7xipmzsCAIZS8Tmc1sHQ6rg= +github.com/logbull/logbull-go v0.2.0/go.mod h1:6uoWbECvFyvg3DSGUd7y9Ufn8CisBaM//sDTLAjfaJI= 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.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= @@ -85,6 +87,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -100,6 +104,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -118,8 +123,14 @@ go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= @@ -128,6 +139,7 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/internal/config/config.go b/internal/config/config.go index 2832b5c..6b602d5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,6 +14,7 @@ type Config struct { Redis RedisConfig `mapstructure:"redis"` SMTP SMTPConfig `mapstructure:"smtp"` RateLimit RateLimitConfig `mapstructure:"rate_limit"` + Logbull LogbullConfig `mapstructure:"logbull"` } type ServerConfig struct { @@ -57,6 +58,13 @@ type RateLimitConfig struct { Default int `mapstructure:"default"` } +type LogbullConfig struct { + Host string `mapstructure:"host"` + ProjectID string `mapstructure:"project_id"` + APIKey string `mapstructure:"api_key"` + LogLevel string `mapstructure:"log_level"` +} + func Load(path string) (*Config, error) { v := viper.New() v.SetConfigFile(path)