feat(lyra)!: 添加结构化日志与请求追踪
- 配置 slog JSON 输出和日志级别 - 添加请求 ID、访问日志及上下文日志工具 - 拆分启动初始化并统一启动失败处理 BREAKING CHANGE: 配置文件必须提供 log.level,缺失或非法将导致启动失败
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package constant
|
||||
|
||||
type contextKey string
|
||||
|
||||
const RequestIDKey contextKey = "request_id"
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user