From cd0e13325ff3f00f5fcaa8f068f3733db0fd3197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=82=B2=E6=96=B0?= <2635131263@qq.com> Date: Tue, 20 Oct 2026 15:16:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(lyra)!:=20=E6=B7=BB=E5=8A=A0=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8C=96=E6=97=A5=E5=BF=97=E4=B8=8E=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E8=BF=BD=E8=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 配置 slog JSON 输出和日志级别 - 添加请求 ID、访问日志及上下文日志工具 - 拆分启动初始化并统一启动失败处理 BREAKING CHANGE: 配置文件必须提供 log.level,缺失或非法将导致启动失败 --- go/projects/lyra/cmd/lyra/bootstrap.go | 75 +++++++++++++++++++ go/projects/lyra/cmd/lyra/main.go | 56 +++++++------- go/projects/lyra/etc/lyra.toml | 3 + go/projects/lyra/go.mod | 1 + go/projects/lyra/go.sum | 2 + go/projects/lyra/internal/config/config.go | 4 + go/projects/lyra/internal/handler/test.go | 10 +-- .../lyra/internal/middleware/accessLog.go | 30 ++++++++ .../lyra/internal/middleware/requestID.go | 27 +++++++ .../lyra/internal/pkg/constant/contextKey.go | 5 ++ go/projects/lyra/internal/util/log.go | 17 +++++ 11 files changed, 197 insertions(+), 33 deletions(-) create mode 100644 go/projects/lyra/cmd/lyra/bootstrap.go create mode 100644 go/projects/lyra/internal/middleware/accessLog.go create mode 100644 go/projects/lyra/internal/middleware/requestID.go create mode 100644 go/projects/lyra/internal/pkg/constant/contextKey.go create mode 100644 go/projects/lyra/internal/util/log.go diff --git a/go/projects/lyra/cmd/lyra/bootstrap.go b/go/projects/lyra/cmd/lyra/bootstrap.go new file mode 100644 index 0000000..6933ea2 --- /dev/null +++ b/go/projects/lyra/cmd/lyra/bootstrap.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log/slog" + "lyra/internal/config" + "lyra/internal/middleware" + "net" + "os" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/jmoiron/sqlx" + "github.com/redis/go-redis/v9" + + // 注册 pgx 驱动,供 sqlx 以 "pgx" 驱动名使用。 + _ "github.com/jackc/pgx/v5/stdlib" +) + +func setLogger(cfg *config.Config) error { + var level slog.Level + switch cfg.Log.Level { + case "debug": + level = slog.LevelDebug + case "info": + level = slog.LevelInfo + case "warn": + level = slog.LevelWarn + case "error": + level = slog.LevelError + default: + return fmt.Errorf("不支持的日志级别: %s", cfg.Log.Level) + } + logger := slog.New( + slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: level, + }), + ) + slog.SetDefault(logger) + return nil +} + +func newHTTPRouter(mode string) (*gin.Engine, error) { + gin.SetMode(mode) + r := gin.New() + r.Use(middleware.RequestIDMiddleware()) + r.Use(middleware.AccessLogMiddleware()) + r.Use(gin.Recovery()) + + // 当前没有反向代理,不信任客户端转发的 IP 头。 + if err := r.SetTrustedProxies(nil); err != nil { + return nil, fmt.Errorf("设置代理信任失败: %w", err) + } + return r, nil +} + +func newDatabase(ctx context.Context, cfg config.DatabaseConfig) (*sqlx.DB, error) { + dataSourceName := fmt.Sprintf( + "host=%s port=%d user=%s dbname=%s password=%s sslmode=disable", + cfg.Host, cfg.Port, cfg.User, cfg.Name, cfg.Password, + ) + db, err := sqlx.ConnectContext(ctx, cfg.Driver, dataSourceName) + if err != nil { + return nil, errors.New("连接 PostgreSQL 失败,请检查连接配置、认证和网络") + } + return db, nil +} +func newCache(cfg config.CacheConfig) *redis.Client { + return redis.NewClient(&redis.Options{ + Addr: net.JoinHostPort(cfg.Host, strconv.Itoa(cfg.Port)), + DB: cfg.DB, + }) +} diff --git a/go/projects/lyra/cmd/lyra/main.go b/go/projects/lyra/cmd/lyra/main.go index 7d16383..75c4f6e 100644 --- a/go/projects/lyra/cmd/lyra/main.go +++ b/go/projects/lyra/cmd/lyra/main.go @@ -3,57 +3,57 @@ package main import ( "context" - "fmt" - "log" + "log/slog" "lyra/internal/config" "lyra/internal/router" "net" + "os" "strconv" - "github.com/gin-gonic/gin" - "github.com/jmoiron/sqlx" "github.com/joho/godotenv" - "github.com/redis/go-redis/v9" - - // 注册 pgx 驱动,供 sqlx 以 "pgx" 驱动名使用。 - _ "github.com/jackc/pgx/v5/stdlib" ) func main() { - // 加载 .env.local 文件,覆盖环境变量 if err := godotenv.Load(".env.local"); err != nil { - log.Fatal("加载环境变量失败,请检查文件是否存在") + slog.Error("加载环境变量失败,请检查文件是否存在及格式是否正确") + os.Exit(1) } - // 加载配置(internal/config) + cfg, err := config.Load("etc/lyra.toml") if err != nil { - log.Fatal("加载配置失败,请检查配置文件和环境变量") + slog.Error("加载配置失败,请检查配置文件和环境变量") + os.Exit(1) } - gin.SetMode(cfg.App.Env) - ginEngine := gin.Default() - //没有反向代理,在启动前关闭代理信任: - if err := ginEngine.SetTrustedProxies(nil); err != nil { - log.Fatal("设置代理信任失败,请检查配置") + if err := setLogger(cfg); err != nil { + slog.Error("设置日志失败", "error", err) + os.Exit(1) + } + r, err := newHTTPRouter(cfg.App.Env) + if err != nil { + slog.Error("初始化 HTTP 路由失败", "error", err) + os.Exit(1) } - dataSourceName := fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s sslmode=disable", cfg.Database.Host, cfg.Database.Port, cfg.Database.User, cfg.Database.Name, cfg.Database.Password) ctx := context.Background() - db, err := sqlx.ConnectContext(ctx, cfg.Database.Driver, dataSourceName) + + db, err := newDatabase(ctx, cfg.Database) if err != nil { - log.Fatal("连接 PostgreSQL 失败,请检查连接配置、认证和网络") + slog.Error("初始化数据库失败", "error", err) + os.Exit(1) } defer db.Close() - cache := redis.NewClient(&redis.Options{ - Addr: net.JoinHostPort(cfg.Cache.Host, strconv.Itoa(cfg.Cache.Port)), - DB: cfg.Cache.DB, - }) + cache := newCache(cfg.Cache) defer cache.Close() + deps := buildDependencies(cfg, db, cache) + handlers := buildHandlers(deps) - router.RegisterRoutes(ginEngine, handlers) - addr := fmt.Sprintf("%s:%d", cfg.HTTP.Host, cfg.HTTP.Port) - if err := ginEngine.Run(addr); err != nil { - log.Fatal("启动 HTTP 服务失败,请检查端口是否被占用") + + router.RegisterRoutes(r, handlers) + + if err := r.Run(net.JoinHostPort(cfg.HTTP.Host, strconv.Itoa(cfg.HTTP.Port))); err != nil { + slog.Error("启动 HTTP 服务失败", "error", err) + os.Exit(1) } } diff --git a/go/projects/lyra/etc/lyra.toml b/go/projects/lyra/etc/lyra.toml index 342dbc0..6152fb3 100644 --- a/go/projects/lyra/etc/lyra.toml +++ b/go/projects/lyra/etc/lyra.toml @@ -2,6 +2,9 @@ name = "lyra" env = "debug" #gin支持 release | test | debug 3种mode,如果需要自定义,需要映射到这3种mode中去,gin的默认模式是debug模式 +[log] +level = "info" #日志级别,支持 debug | info | warn | error | + [http] host = "0.0.0.0" port = 7777 diff --git a/go/projects/lyra/go.mod b/go/projects/lyra/go.mod index c0f0b46..279de10 100644 --- a/go/projects/lyra/go.mod +++ b/go/projects/lyra/go.mod @@ -5,6 +5,7 @@ go 1.27.1 require ( github.com/Masterminds/squirrel v1.5.4 github.com/gin-gonic/gin v1.12.0 + github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.11.0 github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 diff --git a/go/projects/lyra/go.sum b/go/projects/lyra/go.sum index 672d9d9..52f9c74 100644 --- a/go/projects/lyra/go.sum +++ b/go/projects/lyra/go.sum @@ -48,6 +48,8 @@ github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7Lk 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= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= diff --git a/go/projects/lyra/internal/config/config.go b/go/projects/lyra/internal/config/config.go index be90f7a..35d3d26 100644 --- a/go/projects/lyra/internal/config/config.go +++ b/go/projects/lyra/internal/config/config.go @@ -8,6 +8,7 @@ import ( type Config struct { App AppConfig `mapstructure:"app"` + Log LogConfig `mapstructure:"log"` HTTP HTTPConfig `mapstructure:"http"` Database DatabaseConfig `mapstructure:"database"` Cache CacheConfig `mapstructure:"cache"` @@ -18,6 +19,9 @@ type AppConfig struct { Env string `mapstructure:"env"` } +type LogConfig struct { + Level string `mapstructure:"level"` +} type HTTPConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` diff --git a/go/projects/lyra/internal/handler/test.go b/go/projects/lyra/internal/handler/test.go index 33b2bcb..7f95b61 100644 --- a/go/projects/lyra/internal/handler/test.go +++ b/go/projects/lyra/internal/handler/test.go @@ -2,11 +2,11 @@ package handler import ( "fmt" - "log/slog" "lyra/internal/api" "lyra/internal/cache" "lyra/internal/httpx" "lyra/internal/model" + "lyra/internal/util" "net/http" "time" @@ -35,21 +35,21 @@ func (h *TestHandler) Hello() gin.HandlerFunc { Value: categories, TTL: 5 * time.Minute, }); err != nil { - slog.ErrorContext(ctx.Request.Context(), "缓存分类列表失败") + util.Logger(ctx.Request.Context()).Error("设置缓存分类列表失败") return } testResult, _, err := h.cache.Get[[]*model.Category](ctx.Request.Context(), "test:categories") if err != nil { - slog.ErrorContext(ctx.Request.Context(), "获取缓存分类列表失败") + util.Logger(ctx.Request.Context()).Error("获取缓存分类列表失败") } if len(testResult) == 0 { - slog.ErrorContext(ctx.Request.Context(), "缓存分类列表为空") + util.Logger(ctx.Request.Context()).Error("缓存分类列表为空") } else { for _, v := range testResult { fmt.Printf("%+v\n", v) } } - + util.Logger(ctx.Request.Context()).Info("success") httpx.OK(ctx, toCategoryResponses(categories)) } } diff --git a/go/projects/lyra/internal/middleware/accessLog.go b/go/projects/lyra/internal/middleware/accessLog.go new file mode 100644 index 0000000..4751c62 --- /dev/null +++ b/go/projects/lyra/internal/middleware/accessLog.go @@ -0,0 +1,30 @@ +package middleware + +import ( + "lyra/internal/util" + "time" + + "github.com/gin-gonic/gin" +) + +func AccessLogMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + + c.Next() + + ctx := c.Request.Context() + + util.Logger(ctx).InfoContext( + ctx, + "http request", + "method", c.Request.Method, + "path", c.Request.URL.Path, + "status", c.Writer.Status(), + "latency_ms", time.Since(start).Milliseconds(), + "client_ip", c.ClientIP(), + "user_agent", c.Request.UserAgent(), + "response_size", c.Writer.Size(), + ) + } +} diff --git a/go/projects/lyra/internal/middleware/requestID.go b/go/projects/lyra/internal/middleware/requestID.go new file mode 100644 index 0000000..c0ebeff --- /dev/null +++ b/go/projects/lyra/internal/middleware/requestID.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "context" + "lyra/internal/pkg/constant" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func RequestIDMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + requestID := c.GetHeader("X-Request-ID") + + if requestID == "" { + requestID = uuid.NewString() + } + + c.Header("X-Request-ID", requestID) + + ctx := context.WithValue(c.Request.Context(), constant.RequestIDKey, requestID) + + c.Request = c.Request.WithContext(ctx) + + c.Next() + } +} diff --git a/go/projects/lyra/internal/pkg/constant/contextKey.go b/go/projects/lyra/internal/pkg/constant/contextKey.go new file mode 100644 index 0000000..07c825c --- /dev/null +++ b/go/projects/lyra/internal/pkg/constant/contextKey.go @@ -0,0 +1,5 @@ +package constant + +type contextKey string + +const RequestIDKey contextKey = "request_id" diff --git a/go/projects/lyra/internal/util/log.go b/go/projects/lyra/internal/util/log.go new file mode 100644 index 0000000..79fbab4 --- /dev/null +++ b/go/projects/lyra/internal/util/log.go @@ -0,0 +1,17 @@ +package util + +import ( + "context" + "log/slog" + "lyra/internal/pkg/constant" +) + +func Logger(ctx context.Context) *slog.Logger { + logger := slog.Default() + + if requestID, ok := ctx.Value(constant.RequestIDKey).(string); ok { + logger = logger.With(string(constant.RequestIDKey), requestID) + } + + return logger +}