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}
}