feat: add Logbull remote logging via slog handler, tee to stdout

This commit is contained in:
2026-06-27 15:17:59 +08:00
parent 835b92ec00
commit c5225d8c97
5 changed files with 122 additions and 4 deletions
+92 -4
View File
@@ -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}
}
+6
View File
@@ -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"
+4
View File
@@ -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
+12
View File
@@ -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=
+8
View File
@@ -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)