From 589172252615f80c2ab264a72ec5afba02698377 Mon Sep 17 00:00:00 2001 From: zenfun Date: Fri, 12 Dec 2025 22:39:10 +0800 Subject: [PATCH] feat(log): replace standard logger with zerolog - Integrate `rs/zerolog` for structured and leveled logging - Add `EZ_LOG_LEVEL` environment variable support (default: info) - Configure console output with timestamps and service fields - Migrate `main.go` and `LogWriter` to use the new logger instance - Update README with logging configuration and design details --- README.md | 6 ++++ cmd/server/main.go | 58 +++++++++++++++++++++++++++----------- go.mod | 2 ++ go.sum | 12 ++++++++ internal/service/logger.go | 4 +-- 5 files changed, 64 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ba50268..d169bb0 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ EZ-API 是"大脑"。它管理着事实的来源 (Source of Truth)。 | `EZ_REDIS_ADDR` | `localhost:6379` | Redis 地址。 | | `EZ_LOG_QUEUE` | `10000` | 日志缓冲通道的容量。 | | `EZ_LOG_BATCH_SIZE` | `10` | 单次 DB 事务写入的日志数量。 | +| `EZ_LOG_LEVEL` | `info` | 日志级别,支持 `debug`、`info`、`warn`、`error`。 | ## 运行方式 @@ -75,6 +76,11 @@ cd ez-api 脚本会拉起 `docker-compose.integration.yml` 中的服务,运行带 `integration` tag 的 Go 测试,并在完成后清理容器和卷。 +## 日志 + +- 使用 [zerolog](https://github.com/rs/zerolog) 输出结构化日志,默认 ConsoleWriter。 +- 通过 `EZ_LOG_LEVEL` 控制控制平面的日志等级,配合异步 DB 写入(LogWriter)一起使用。 + ## 设计决策 - **异步日志**: 日志不会立即写入 DB。它们被缓冲在内存中,并分批刷新,以减少 DB IOPS。 diff --git a/cmd/server/main.go b/cmd/server/main.go index 7b11c2e..050afc5 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,10 +2,10 @@ package main import ( "context" - "log" "net/http" "os" "os/signal" + "strings" "syscall" "time" @@ -17,6 +17,8 @@ import ( "github.com/ez-api/ez-api/internal/service" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" "gorm.io/driver/postgres" @@ -47,10 +49,12 @@ import ( // @name Authorization func main() { + logger := setupLogger() + // 1. Load Configuration cfg, err := config.Load() if err != nil { - log.Fatalf("Failed to load config: %v", err) + logger.Fatal().Err(err).Msg("Failed to load config") } // 2. Initialize Redis Client @@ -64,28 +68,28 @@ func main() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := rdb.Ping(ctx).Err(); err != nil { - log.Fatalf("Failed to connect to Redis: %v", err) + logger.Fatal().Err(err).Msg("Failed to connect to Redis") } - log.Println("Connected to Redis successfully") + logger.Info().Msg("Connected to Redis successfully") // 3. Initialize GORM (PostgreSQL) db, err := gorm.Open(postgres.Open(cfg.Postgres.DSN), &gorm.Config{}) if err != nil { - log.Fatalf("Failed to connect to PostgreSQL: %v", err) + logger.Fatal().Err(err).Msg("Failed to connect to PostgreSQL") } sqlDB, err := db.DB() if err != nil { - log.Fatalf("Failed to get generic database object: %v", err) + logger.Fatal().Err(err).Msg("Failed to get generic database object") } // Verify DB connection if err := sqlDB.Ping(); err != nil { - log.Fatalf("Failed to ping PostgreSQL: %v", err) + logger.Fatal().Err(err).Msg("Failed to ping PostgreSQL") } - log.Println("Connected to PostgreSQL successfully") + logger.Info().Msg("Connected to PostgreSQL successfully") // Auto Migrate if err := db.AutoMigrate(&model.Master{}, &model.Key{}, &model.Provider{}, &model.Model{}, &model.LogRecord{}); err != nil { - log.Fatalf("Failed to auto migrate: %v", err) + logger.Fatal().Err(err).Msg("Failed to auto migrate") } // 4. Setup Services and Handlers @@ -97,7 +101,7 @@ func main() { adminService, err := service.NewAdminService() if err != nil { - log.Fatalf("Failed to create admin service: %v", err) + logger.Fatal().Err(err).Msg("Failed to create admin service") } masterService := service.NewMasterService(db) healthService := service.NewHealthCheckService(db, rdb) @@ -108,7 +112,7 @@ func main() { // 4.1 Prime Redis snapshots so DP can start with data if err := syncService.SyncAll(db); err != nil { - log.Printf("Initial sync warning: %v", err) + logger.Warn().Err(err).Msg("Initial sync warning") } // 5. Setup Gin Router @@ -172,9 +176,9 @@ func main() { // 6. Start Server with Graceful Shutdown go func() { - log.Printf("Starting ez-api on port %s", cfg.Server.Port) + logger.Info().Str("port", cfg.Server.Port).Msg("Starting ez-api") if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("Server failed: %v", err) + logger.Fatal().Err(err).Msg("Server failed") } }() @@ -182,14 +186,36 @@ func main() { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit - log.Println("Shutting down server...") + logger.Info().Msg("Shutting down server...") // Shutdown with timeout ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { - log.Fatalf("Server forced to shutdown: %v", err) + logger.Fatal().Err(err).Msg("Server forced to shutdown") } - log.Println("Server exited properly") + logger.Info().Msg("Server exited properly") +} + +func setupLogger() zerolog.Logger { + level := zerolog.InfoLevel + if raw := strings.TrimSpace(os.Getenv("EZ_LOG_LEVEL")); raw != "" { + if parsed, err := zerolog.ParseLevel(strings.ToLower(raw)); err == nil { + level = parsed + } + } + + zerolog.SetGlobalLevel(level) + output := zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + } + + logger := zerolog.New(output).Level(level).With(). + Timestamp(). + Str("service", "ez-api"). + Logger() + log.Logger = logger + return logger } diff --git a/go.mod b/go.mod index 306c99f..a48b20d 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/bytedance/sonic v1.14.0 github.com/gin-gonic/gin v1.11.0 github.com/redis/go-redis/v9 v9.17.2 + github.com/rs/zerolog v1.34.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.1 github.com/swaggo/swag v1.16.6 @@ -47,6 +48,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/go.sum b/go.sum index c21b213..4eeb6e5 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,7 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -65,6 +66,7 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -90,6 +92,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -99,6 +105,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= @@ -109,6 +116,9 @@ github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4Vi github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= 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/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= @@ -158,8 +168,10 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/internal/service/logger.go b/internal/service/logger.go index f1b7bec..da22707 100644 --- a/internal/service/logger.go +++ b/internal/service/logger.go @@ -2,10 +2,10 @@ package service import ( "context" - "log" "time" "github.com/ez-api/ez-api/internal/model" + "github.com/rs/zerolog/log" "gorm.io/gorm" ) @@ -47,7 +47,7 @@ func (w *LogWriter) Start(ctx context.Context) { return } if err := w.db.Create(&buf).Error; err != nil { - log.Printf("log batch insert failed: %v", err) + log.Error().Err(err).Msg("log batch insert failed") } buf = buf[:0] }