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의 기본 Logger와 Recovery를 대체할 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 스택이나 클라우드 로깅 서비스와의 연동이 훨씬 용이해집니다.