Gin 프레임워크에서 Zap 로깅 시스템 통합 및 로그 로테이션 설정하기

Gin 프레임워크를 사용하여 웹 서비스를 개발할 때, 기본적으로 제공되는 로거 대신 Uber의 고성능 로깅 라이브러리인 zap을 사용하면 더 빠르고 구조화된 로그를 남길 수 있습니다. 이 글에서는 Gin의 기본 로그를 Zap으로 대체하고, 대용량 로그 관리를 위한 로그 로테이션(Lumberjack) 설정 방법을 설명합니다.

Gin의 기본 로거 이해

Gin 애플리케이션을 시작할 때 흔히 사용하는 gin.Default() 함수는 내부적으로 두 개의 미들웨어를 활성화합니다.

func Default() *Engine {
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}

Logger() 미들웨어는 표준 출력(stdout)에 요청 로그를 기록하고, Recovery()는 예기치 못한 패닉(panic)으로부터 서버를 복구하고 500 에러를 반환합니다. 하지만 운영 환경에서는 이러한 로그를 파일에 기록하거나 JSON 형태로 구조화하여 분석 도구와 연동해야 할 필요가 있습니다.

Zap을 활용한 커스텀 미들웨어 구현

Gin의 기본 LoggerRecovery를 대체할 Zap 기반의 미들웨어를 직접 구현할 수 있습니다. 아래 코드는 요청 경로, 상태 코드, 지연 시간 등을 Zap의 구조화된 데이터 필드로 기록하는 예제입니다.

// ZapRequestLogger는 Gin의 HTTP 요청을 기록하는 미들웨어입니다.
func ZapRequestLogger(z *zap.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        entryTime := time.Now()
        reqPath := c.Request.URL.Path
        rawQuery := c.Request.URL.RawQuery

        c.Next()

        latency := time.Since(entryTime)
        z.Info(reqPath,
            zap.Int("status", c.Writer.Status()),
            zap.String("method", c.Request.Method),
            zap.String("path", reqPath),
            zap.String("query", rawQuery),
            zap.String("ip", c.ClientIP()),
            zap.String("user-agent", c.Request.UserAgent()),
            zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
            zap.Duration("latency", latency),
        )
    }
}

// ZapPanicRecovery는 패닉 발생 시 이를 포착하고 Zap을 통해 스택 트레이스를 기록합니다.
func ZapPanicRecovery(z *zap.Logger, showStack bool) gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                // 연결이 끊긴 경우(broken pipe) 확인
                var isBrokenPipe bool
                if ne, ok := err.(*net.OpError); ok {
                    if se, ok := ne.Err.(*os.SyscallError); ok {
                        errMsg := strings.ToLower(se.Error())
                        if strings.Contains(errMsg, "broken pipe") || strings.Contains(errMsg, "connection reset by peer") {
                            isBrokenPipe = true
                        }
                    }
                }

                reqInfo, _ := httputil.DumpRequest(c.Request, false)
                if isBrokenPipe {
                    z.Error(c.Request.URL.Path,
                        zap.Any("error", err),
                        zap.String("request", string(reqInfo)),
                    )
                    c.Error(err.(error))
                    c.Abort()
                    return
                }

                fields := []zap.Field{
                    zap.Any("error", err),
                    zap.String("request", string(reqInfo)),
                }
                if showStack {
                    fields = append(fields, zap.String("stack", string(debug.Stack())))
                }

                z.Error("[Panic Recovered]", fields...)
                c.AbortWithStatus(http.StatusInternalServerError)
            }
        }()
        c.Next()
    }
}

로그 로테이션 적용 및 초기화

로그 파일이 무한정 커지는 것을 방지하기 위해 lumberjack 라이브러리를 연동하여 크기 기반 로테이션을 설정합니다. 다음은 설정 정보를 바탕으로 Zap 로거를 초기화하는 통합 코드입니다.

package logger

import (
    "github.com/gin-gonic/gin"
    "github.com/natefinch/lumberjack"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "net"
    "net/http"
    "net/http/httputil"
    "os"
    "runtime/debug"
    "strings"
    "time"
)

var AppLogger *zap.Logger

type LogSettings struct {
    FilePath   string
    Level      string
    MaxSize    int
    MaxBackups int
    MaxAge     int
}

func SetupLogger(cfg LogSettings) error {
    // 로그 파일 자동 관리 설정
    writer := zapcore.AddSync(&lumberjack.Logger{
        Filename:   cfg.FilePath,
        MaxSize:    cfg.MaxSize,
        MaxBackups: cfg.MaxBackups,
        MaxAge:     cfg.MaxAge,
        Compress:   true,
    })

    // 인코더 설정 (JSON 형식)
    encCfg := zap.NewProductionEncoderConfig()
    encCfg.EncodeTime = zapcore.ISO8601TimeEncoder
    encCfg.TimeKey = "timestamp"
    encCfg.EncodeLevel = zapcore.CapitalLevelEncoder
    encoder := zapcore.NewJSONEncoder(encCfg)

    // 로그 레벨 파싱
    var level zapcore.Level
    if err := level.UnmarshalText([]byte(cfg.Level)); err != nil {
        return err
    }

    core := zapcore.NewCore(encoder, writer, level)
    AppLogger = zap.New(core, zap.AddCaller())
    zap.ReplaceGlobals(AppLogger)
    return nil
}

프로젝트 적용 예시

이제 메인 함수에서 로거를 초기화하고 Gin 엔진에 미들웨어를 등록하여 사용합니다. gin.New()를 사용하여 기본 미들웨어 없이 엔진을 생성한 뒤, 구현한 커스텀 미들웨어를 주입합니다.

func main() {
    // 1. 로거 설정 로드 및 초기화
    conf := logger.LogSettings{
        FilePath:   "./logs/app.log",
        Level:      "info",
        MaxSize:    100, // MB
        MaxBackups: 5,
        MaxAge:     30, // days
    }
    
    if err := logger.SetupLogger(conf); err != nil {
        panic(err)
    }

    // 2. Gin 엔진 생성 (기본 미들웨어 제외)
    router := gin.New()

    // 3. 커스텀 Zap 미들웨어 적용
    router.Use(logger.ZapRequestLogger(zap.L()), logger.ZapPanicRecovery(zap.L(), true))

    router.GET("/ping", func(c *gin.Context) {
        zap.L().Debug("Health check endpoint accessed")
        c.String(http.StatusOK, "pong")
    })

    router.Run(":8080")
}

이 구성을 통해 Gin의 모든 내부 로그가 Zap으로 전달되며, 설정한 규칙에 따라 로그 파일이 자동으로 분할 및 관리됩니다. 또한 구조화된 JSON 로그를 통해 ELK 스택이나 클라우드 로깅 서비스와의 연동이 훨씬 용이해집니다.

태그: Golang gin-gonic zap logging lumberjack

7월 31일 15:02에 게시됨