14 minute read

개요

Go 1.21부터 추가된 log/slog 패키지는 구조화된 로깅을 위한 표준 라이브러리입니다.

주요 특징:

  • 구조화된 로그: 키-값 쌍으로 로그 기록
  • 레벨 기반: Debug, Info, Warn, Error 레벨 지원
  • 핸들러 패턴: TextHandler, JSONHandler, 커스텀 핸들러
  • 성능: 고성능 로깅 (할당 최소화)
  • 컨텍스트 지원: context.Context 전파
  • 타입 안전: 정적 타입 체크
  • 확장 가능: Handler 인터페이스로 커스터마이징

기본 사용법

1. 로그 레벨

package main

import (
    "log/slog"
)

func main() {
    // 기본 핸들러 (TextHandler, INFO 레벨)
    slog.Debug("This won't appear")  // DEBUG < INFO
    slog.Info("Application started")
    slog.Warn("Low disk space")
    slog.Error("Database connection failed")
    
    // 출력:
    // 2024/01/01 10:00:00 INFO Application started
    // 2024/01/01 10:00:00 WARN Low disk space
    // 2024/01/01 10:00:00 ERROR Database connection failed
}

2. 속성 추가

func main() {
    // 키-값 쌍으로 속성 추가
    slog.Info("User logged in", "user_id", 123, "username", "alice")
    // 2024/01/01 10:00:00 INFO User logged in user_id=123 username=alice
    
    // 여러 속성
    slog.Error("Request failed",
        "method", "POST",
        "path", "/api/users",
        "status", 500,
        "duration_ms", 1234,
    )
}

3. 타입 안전 속성

func main() {
    // slog.Attr 사용 (권장)
    slog.Info("Server starting",
        slog.String("host", "localhost"),
        slog.Int("port", 8080),
        slog.Bool("tls_enabled", true),
        slog.Duration("timeout", 30*time.Second),
    )
    
    // Any 타입
    slog.Info("User data",
        slog.Any("user", map[string]interface{}{
            "id":   123,
            "name": "Alice",
        }),
    )
}

Handler

1. TextHandler

import (
    "log/slog"
    "os"
)

func main() {
    // TextHandler 생성
    handler := slog.NewTextHandler(os.Stdout, nil)
    logger := slog.New(handler)
    
    logger.Info("Server started", "port", 8080)
    // time=2024-01-01T10:00:00.000Z level=INFO msg="Server started" port=8080
}

2. JSONHandler

func main() {
    // JSONHandler 생성
    handler := slog.NewJSONHandler(os.Stdout, nil)
    logger := slog.New(handler)
    
    logger.Info("Server started", "port", 8080)
    // {"time":"2024-01-01T10:00:00.000Z","level":"INFO","msg":"Server started","port":8080}
}

3. 기본 Logger 설정

func main() {
    // 전역 기본 Logger 설정
    handler := slog.NewJSONHandler(os.Stdout, nil)
    logger := slog.New(handler)
    slog.SetDefault(logger)
    
    // 이후 slog.Info 등은 새 핸들러 사용
    slog.Info("Using JSON handler")
}

4. 파일 출력

func main() {
    // 파일로 로그 출력
    file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        panic(err)
    }
    defer file.Close()
    
    handler := slog.NewJSONHandler(file, nil)
    logger := slog.New(handler)
    
    logger.Info("Log to file", "timestamp", time.Now())
}

5. MultiWriter

import "io"

func main() {
    // stdout과 파일에 동시 출력
    file, _ := os.Create("app.log")
    defer file.Close()
    
    multi := io.MultiWriter(os.Stdout, file)
    handler := slog.NewJSONHandler(multi, nil)
    logger := slog.New(handler)
    
    logger.Info("Logged to both stdout and file")
}

HandlerOptions

1. 로그 레벨 설정

func main() {
    // DEBUG 레벨부터 출력
    opts := &slog.HandlerOptions{
        Level: slog.LevelDebug,
    }
    
    handler := slog.NewJSONHandler(os.Stdout, opts)
    logger := slog.New(handler)
    
    logger.Debug("Debug message")  // 이제 출력됨
    logger.Info("Info message")
}

2. 동적 레벨

func main() {
    // 런타임에 레벨 변경 가능
    var level slog.LevelVar
    level.Set(slog.LevelInfo)
    
    opts := &slog.HandlerOptions{
        Level: &level,
    }
    
    handler := slog.NewJSONHandler(os.Stdout, opts)
    logger := slog.New(handler)
    
    logger.Debug("Not shown")  // INFO 레벨이므로 출력 안됨
    
    // 레벨 변경
    level.Set(slog.LevelDebug)
    
    logger.Debug("Now shown")  // 이제 출력됨
}

3. 소스 위치 추가

func main() {
    opts := &slog.HandlerOptions{
        AddSource: true,  // 파일명과 라인 번호 추가
    }
    
    handler := slog.NewJSONHandler(os.Stdout, opts)
    logger := slog.New(handler)
    
    logger.Info("With source location")
    // {"time":"...","level":"INFO","source":{"file":"main.go","line":15},"msg":"With source location"}
}

4. ReplaceAttr

func main() {
    opts := &slog.HandlerOptions{
        ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
            // 시간 포맷 변경
            if a.Key == slog.TimeKey {
                return slog.String("timestamp", time.Now().Format(time.RFC3339))
            }
            
            // 레벨을 소문자로
            if a.Key == slog.LevelKey {
                level := a.Value.Any().(slog.Level)
                return slog.String("severity", strings.ToLower(level.String()))
            }
            
            return a
        },
    }
    
    handler := slog.NewJSONHandler(os.Stdout, opts)
    logger := slog.New(handler)
    
    logger.Info("Custom attributes")
    // {"timestamp":"2024-01-01T10:00:00Z","severity":"info","msg":"Custom attributes"}
}

5. 민감 정보 마스킹

func main() {
    opts := &slog.HandlerOptions{
        ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
            // password 필드 마스킹
            if a.Key == "password" {
                return slog.String("password", "***REDACTED***")
            }
            
            // credit_card 필드 마스킹
            if a.Key == "credit_card" {
                return slog.String("credit_card", "****-****-****-****")
            }
            
            return a
        },
    }
    
    handler := slog.NewJSONHandler(os.Stdout, opts)
    logger := slog.New(handler)
    
    logger.Info("User login",
        "username", "alice",
        "password", "secret123",
    )
    // {"time":"...","level":"INFO","msg":"User login","username":"alice","password":"***REDACTED***"}
}

Logger 생성 및 사용

1. With 메서드

func main() {
    // 공통 속성을 가진 Logger 생성
    baseLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // 요청별 Logger
    requestLogger := baseLogger.With(
        "request_id", "req-123",
        "user_id", 456,
    )
    
    requestLogger.Info("Processing request")
    requestLogger.Info("Request completed")
    
    // 모든 로그에 request_id와 user_id가 포함됨
}

2. WithGroup

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // 그룹화된 속성
    serverLogger := logger.WithGroup("server")
    serverLogger.Info("Starting",
        "host", "localhost",
        "port", 8080,
    )
    
    // {"time":"...","level":"INFO","msg":"Starting","server":{"host":"localhost","port":8080}}
}

3. 중첩 그룹

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    logger = logger.WithGroup("app").WithGroup("database")
    logger.Info("Connected",
        "host", "localhost",
        "port", 5432,
    )
    
    // {"time":"...","level":"INFO","msg":"Connected","app":{"database":{"host":"localhost","port":5432}}}
}

4. 인라인 그룹

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    logger.Info("Server metrics",
        "cpu", 45.5,
        slog.Group("memory",
            "used", 1024,
            "total", 4096,
        ),
        slog.Group("disk",
            "read", 100,
            "write", 50,
        ),
    )
    
    // {"time":"...","level":"INFO","msg":"Server metrics","cpu":45.5,"memory":{"used":1024,"total":4096},"disk":{"read":100,"write":50}}
}

Context 사용

1. Context 전달

import "context"

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    ctx := context.Background()
    
    // Context와 함께 로깅
    logger.InfoContext(ctx, "Processing request")
    logger.ErrorContext(ctx, "Request failed", "error", "timeout")
}

2. Context에서 Logger 추출

type contextKey string

const loggerKey contextKey = "logger"

func WithLogger(ctx context.Context, logger *slog.Logger) context.Context {
    return context.WithValue(ctx, loggerKey, logger)
}

func LoggerFromContext(ctx context.Context) *slog.Logger {
    if logger, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
        return logger
    }
    return slog.Default()
}

func processRequest(ctx context.Context) {
    logger := LoggerFromContext(ctx)
    logger.Info("Processing...")
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    logger = logger.With("request_id", "req-123")
    
    ctx := WithLogger(context.Background(), logger)
    processRequest(ctx)
}

3. Trace ID 전파

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // Trace ID를 Context에 저장
    ctx := context.WithValue(context.Background(), "trace_id", "trace-abc-123")
    
    // Context에서 Trace ID 추출하여 로깅
    traceID := ctx.Value("trace_id").(string)
    logger = logger.With("trace_id", traceID)
    
    logger.InfoContext(ctx, "Request received")
    logger.InfoContext(ctx, "Request processed")
    
    // 모든 로그에 trace_id가 포함됨
}

성능 최적화

1. Enabled 체크

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    }))
    
    // 비효율적: Debug가 비활성화되어도 문자열 생성
    logger.Debug("Expensive operation: " + expensiveOperation())
    
    // 효율적: Enabled 체크
    if logger.Enabled(context.Background(), slog.LevelDebug) {
        logger.Debug("Expensive operation: " + expensiveOperation())
    }
}

func expensiveOperation() string {
    // 무거운 작업
    return "result"
}

2. LogAttrs

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // 일반적인 방법 (가변 인자)
    logger.Info("Message", "key1", "value1", "key2", "value2")
    
    // LogAttrs 사용 (더 효율적)
    logger.LogAttrs(context.Background(), slog.LevelInfo, "Message",
        slog.String("key1", "value1"),
        slog.String("key2", "value2"),
    )
}

3. 사전 할당된 Attributes

var (
    // 자주 사용하는 속성 미리 생성
    serviceAttr = slog.String("service", "my-app")
    versionAttr = slog.String("version", "1.0.0")
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // 사전 할당된 속성 재사용
    logger.LogAttrs(context.Background(), slog.LevelInfo, "Application started",
        serviceAttr,
        versionAttr,
    )
}

4. 조건부 로깅

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelWarn,
    }))
    
    // Debug와 Info는 무시됨 (할당 최소화)
    logger.Debug("Debug message")
    logger.Info("Info message")
    logger.Warn("Warn message")  // 출력됨
}

커스텀 Handler

1. Handler 인터페이스

type Handler interface {
    Enabled(context.Context, Level) bool
    Handle(context.Context, Record) error
    WithAttrs(attrs []Attr) Handler
    WithGroup(name string) Handler
}

2. 간단한 커스텀 Handler

type CustomHandler struct {
    slog.Handler
    prefix string
}

func NewCustomHandler(w io.Writer, prefix string, opts *slog.HandlerOptions) *CustomHandler {
    return &CustomHandler{
        Handler: slog.NewTextHandler(w, opts),
        prefix:  prefix,
    }
}

func (h *CustomHandler) Handle(ctx context.Context, r slog.Record) error {
    // 모든 메시지에 prefix 추가
    r.Message = h.prefix + r.Message
    return h.Handler.Handle(ctx, r)
}

func main() {
    handler := NewCustomHandler(os.Stdout, "[APP] ", nil)
    logger := slog.New(handler)
    
    logger.Info("Server started")
    // time=... level=INFO msg="[APP] Server started"
}

3. 색상 출력 Handler

type ColorHandler struct {
    slog.Handler
}

func NewColorHandler(w io.Writer, opts *slog.HandlerOptions) *ColorHandler {
    return &ColorHandler{
        Handler: slog.NewTextHandler(w, opts),
    }
}

func (h *ColorHandler) Handle(ctx context.Context, r slog.Record) error {
    // 레벨별 색상
    var color string
    switch r.Level {
    case slog.LevelDebug:
        color = "\033[36m" // Cyan
    case slog.LevelInfo:
        color = "\033[32m" // Green
    case slog.LevelWarn:
        color = "\033[33m" // Yellow
    case slog.LevelError:
        color = "\033[31m" // Red
    default:
        color = "\033[0m" // Reset
    }
    
    reset := "\033[0m"
    
    // 색상 추가
    r.Message = color + r.Message + reset
    
    return h.Handler.Handle(ctx, r)
}

func main() {
    handler := NewColorHandler(os.Stdout, nil)
    logger := slog.New(handler)
    
    logger.Debug("Debug message")  // Cyan
    logger.Info("Info message")    // Green
    logger.Warn("Warn message")    // Yellow
    logger.Error("Error message")  // Red
}

4. 필터링 Handler

type FilterHandler struct {
    slog.Handler
    filter func(slog.Record) bool
}

func NewFilterHandler(h slog.Handler, filter func(slog.Record) bool) *FilterHandler {
    return &FilterHandler{
        Handler: h,
        filter:  filter,
    }
}

func (h *FilterHandler) Handle(ctx context.Context, r slog.Record) error {
    if !h.filter(r) {
        return nil // 필터링
    }
    return h.Handler.Handle(ctx, r)
}

func main() {
    baseHandler := slog.NewJSONHandler(os.Stdout, nil)
    
    // "password" 속성이 있는 로그 필터링
    handler := NewFilterHandler(baseHandler, func(r slog.Record) bool {
        hasPassword := false
        r.Attrs(func(a slog.Attr) bool {
            if a.Key == "password" {
                hasPassword = true
                return false
            }
            return true
        })
        return !hasPassword
    })
    
    logger := slog.New(handler)
    
    logger.Info("Safe log", "user", "alice")          // 출력됨
    logger.Info("Unsafe log", "password", "secret")   // 필터링됨
}

5. 샘플링 Handler

type SamplingHandler struct {
    slog.Handler
    rate     int
    counter  atomic.Int64
}

func NewSamplingHandler(h slog.Handler, rate int) *SamplingHandler {
    return &SamplingHandler{
        Handler: h,
        rate:    rate,
    }
}

func (h *SamplingHandler) Handle(ctx context.Context, r slog.Record) error {
    count := h.counter.Add(1)
    if count%int64(h.rate) != 0 {
        return nil // 샘플링
    }
    return h.Handler.Handle(ctx, r)
}

func main() {
    baseHandler := slog.NewJSONHandler(os.Stdout, nil)
    
    // 10개 중 1개만 기록
    handler := NewSamplingHandler(baseHandler, 10)
    logger := slog.New(handler)
    
    for i := 0; i < 100; i++ {
        logger.Info("High frequency log", "iteration", i)
    }
    // 약 10개의 로그만 출력됨
}

실전 예제

1. HTTP 미들웨어

func LoggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            
            // 요청 정보 로깅
            logger.Info("Request started",
                "method", r.Method,
                "path", r.URL.Path,
                "remote_addr", r.RemoteAddr,
            )
            
            // Response Writer 래핑
            wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
            
            // 다음 핸들러 실행
            next.ServeHTTP(wrapped, r)
            
            // 응답 정보 로깅
            duration := time.Since(start)
            
            logFunc := logger.Info
            if wrapped.statusCode >= 500 {
                logFunc = logger.Error
            } else if wrapped.statusCode >= 400 {
                logFunc = logger.Warn
            }
            
            logFunc("Request completed",
                "method", r.Method,
                "path", r.URL.Path,
                "status", wrapped.statusCode,
                "duration_ms", duration.Milliseconds(),
                "bytes", wrapped.bytesWritten,
            )
        })
    }
}

type responseWriter struct {
    http.ResponseWriter
    statusCode   int
    bytesWritten int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

func (rw *responseWriter) Write(b []byte) (int, error) {
    n, err := rw.ResponseWriter.Write(b)
    rw.bytesWritten += n
    return n, err
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })
    
    handler := LoggingMiddleware(logger)(mux)
    http.ListenAndServe(":8080", handler)
}

2. 에러 로깅

type AppError struct {
    Code    string
    Message string
    Err     error
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("%s: %s", e.Code, e.Message)
}

func (e *AppError) LogValue() slog.Value {
    attrs := []slog.Attr{
        slog.String("code", e.Code),
        slog.String("message", e.Message),
    }
    
    if e.Err != nil {
        attrs = append(attrs, slog.String("cause", e.Err.Error()))
    }
    
    return slog.GroupValue(attrs...)
}

func processData(logger *slog.Logger) error {
    err := someOperation()
    if err != nil {
        appErr := &AppError{
            Code:    "DATA_PROCESSING_ERROR",
            Message: "Failed to process data",
            Err:     err,
        }
        
        logger.Error("Operation failed",
            "error", appErr,
            "operation", "processData",
        )
        
        return appErr
    }
    
    return nil
}

func someOperation() error {
    return fmt.Errorf("database connection timeout")
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    if err := processData(logger); err != nil {
        os.Exit(1)
    }
}

3. 구조화된 애플리케이션 로거

type App struct {
    logger *slog.Logger
}

func NewApp() *App {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    logger = logger.With(
        "service", "my-app",
        "version", "1.0.0",
        "environment", os.Getenv("ENV"),
    )
    
    return &App{logger: logger}
}

func (a *App) Start() {
    a.logger.Info("Application starting")
    
    // HTTP 서버 로거
    httpLogger := a.logger.WithGroup("http")
    a.startHTTPServer(httpLogger)
    
    // Database 로거
    dbLogger := a.logger.WithGroup("database")
    a.connectDatabase(dbLogger)
    
    a.logger.Info("Application started successfully")
}

func (a *App) startHTTPServer(logger *slog.Logger) {
    logger.Info("Starting HTTP server",
        "host", "0.0.0.0",
        "port", 8080,
    )
}

func (a *App) connectDatabase(logger *slog.Logger) {
    logger.Info("Connecting to database",
        "host", "localhost",
        "port", 5432,
        "database", "mydb",
    )
}

func main() {
    app := NewApp()
    app.Start()
}

4. 메트릭과 함께 사용

type Metrics struct {
    logger        *slog.Logger
    requestCount  atomic.Int64
    errorCount    atomic.Int64
    totalDuration atomic.Int64
}

func NewMetrics(logger *slog.Logger) *Metrics {
    m := &Metrics{logger: logger}
    
    // 주기적으로 메트릭 로깅
    go m.logMetricsPeriodically()
    
    return m
}

func (m *Metrics) RecordRequest(duration time.Duration, err error) {
    m.requestCount.Add(1)
    m.totalDuration.Add(duration.Milliseconds())
    
    if err != nil {
        m.errorCount.Add(1)
    }
}

func (m *Metrics) logMetricsPeriodically() {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()
    
    for range ticker.C {
        requests := m.requestCount.Load()
        errors := m.errorCount.Load()
        totalDuration := m.totalDuration.Load()
        
        var avgDuration int64
        if requests > 0 {
            avgDuration = totalDuration / requests
        }
        
        m.logger.Info("Metrics snapshot",
            "requests", requests,
            "errors", errors,
            "error_rate", float64(errors)/float64(max(requests, 1)),
            "avg_duration_ms", avgDuration,
        )
    }
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    metrics := NewMetrics(logger)
    
    // 요청 시뮬레이션
    for i := 0; i < 100; i++ {
        start := time.Now()
        
        var err error
        if i%10 == 0 {
            err = fmt.Errorf("simulated error")
        }
        
        time.Sleep(time.Millisecond * time.Duration(rand.Intn(100)))
        metrics.RecordRequest(time.Since(start), err)
    }
    
    time.Sleep(15 * time.Second)
}

5. 요청 추적

func generateRequestID() string {
    b := make([]byte, 16)
    rand.Read(b)
    return fmt.Sprintf("%x", b)
}

func TraceMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Request ID 생성
            requestID := generateRequestID()
            
            // Context에 Request ID 추가
            ctx := context.WithValue(r.Context(), "request_id", requestID)
            
            // Logger에 Request ID 추가
            reqLogger := logger.With("request_id", requestID)
            
            // Context에 Logger 저장
            ctx = context.WithValue(ctx, "logger", reqLogger)
            
            reqLogger.Info("Request received",
                "method", r.Method,
                "path", r.URL.Path,
            )
            
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
    // Context에서 Logger 가져오기
    logger := r.Context().Value("logger").(*slog.Logger)
    
    logger.Info("Processing request")
    
    // 비즈니스 로직
    time.Sleep(100 * time.Millisecond)
    
    logger.Info("Request processed successfully")
    
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    mux := http.NewServeMux()
    mux.HandleFunc("/api/data", handleRequest)
    
    handler := TraceMiddleware(logger)(mux)
    http.ListenAndServe(":8080", handler)
}

6. 로그 집계

type LogAggregator struct {
    logger    *slog.Logger
    buffer    []slog.Record
    mu        sync.Mutex
    flushSize int
}

func NewLogAggregator(logger *slog.Logger, flushSize int) *LogAggregator {
    return &LogAggregator{
        logger:    logger,
        buffer:    make([]slog.Record, 0, flushSize),
        flushSize: flushSize,
    }
}

func (la *LogAggregator) Log(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) {
    la.mu.Lock()
    defer la.mu.Unlock()
    
    r := slog.NewRecord(time.Now(), level, msg, 0)
    r.AddAttrs(attrs...)
    
    la.buffer = append(la.buffer, r)
    
    if len(la.buffer) >= la.flushSize {
        la.flush()
    }
}

func (la *LogAggregator) flush() {
    for _, r := range la.buffer {
        la.logger.Handler().Handle(context.Background(), r)
    }
    la.buffer = la.buffer[:0]
}

func (la *LogAggregator) Flush() {
    la.mu.Lock()
    defer la.mu.Unlock()
    la.flush()
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    aggregator := NewLogAggregator(logger, 10)
    
    // 많은 로그 생성
    for i := 0; i < 25; i++ {
        aggregator.Log(context.Background(), slog.LevelInfo, "Event occurred",
            slog.Int("event_id", i),
        )
    }
    
    // 남은 로그 플러시
    aggregator.Flush()
}

일반적인 실수

1. 짝이 맞지 않는 키-값

// ❌ 나쁜 예 (값 누락)
func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    logger.Info("User action",
        "user_id", 123,
        "action",  // 값이 없음!
    )
    // 출력: {"time":"...","level":"INFO","msg":"User action","user_id":123,"action":"!BADKEY"}
}

// ✅ 좋은 예
func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    logger.Info("User action",
        "user_id", 123,
        "action", "login",
    )
}

2. 민감 정보 로깅

// ❌ 나쁜 예
func login(logger *slog.Logger, username, password string) {
    logger.Info("User login attempt",
        "username", username,
        "password", password,  // 비밀번호 노출!
    )
}

// ✅ 좋은 예
func login(logger *slog.Logger, username, password string) {
    logger.Info("User login attempt",
        "username", username,
        // 비밀번호는 로그하지 않음
    )
}

// ✅ 또는 ReplaceAttr 사용

3. 비효율적인 로깅

// ❌ 나쁜 예 (불필요한 문자열 생성)
func processItems(logger *slog.Logger, items []Item) {
    logger.Debug("Processing items: " + fmt.Sprintf("%+v", items))
    // DEBUG가 비활성화되어도 fmt.Sprintf 실행됨
}

// ✅ 좋은 예
func processItems(logger *slog.Logger, items []Item) {
    if logger.Enabled(context.Background(), slog.LevelDebug) {
        logger.Debug("Processing items", "items", items)
    }
}

4. Logger를 값으로 전달

// ❌ 나쁜 예
func processData(logger slog.Logger) {  // 값 복사
    logger.Info("Processing")
}

// ✅ 좋은 예
func processData(logger *slog.Logger) {  // 포인터
    logger.Info("Processing")
}

5. 전역 Logger 과다 사용

// ❌ 나쁜 예
var globalLogger *slog.Logger

func init() {
    globalLogger = slog.New(slog.NewJSONHandler(os.Stdout, nil))
}

func processRequest() {
    globalLogger.Info("Processing")  // 테스트하기 어려움
}

// ✅ 좋은 예
type Processor struct {
    logger *slog.Logger
}

func NewProcessor(logger *slog.Logger) *Processor {
    return &Processor{logger: logger}
}

func (p *Processor) ProcessRequest() {
    p.logger.Info("Processing")  // 의존성 주입
}

6. Context 무시

// ❌ 나쁜 예
func handleRequest(ctx context.Context, logger *slog.Logger) {
    logger.Info("Handling request")  // Context 무시
}

// ✅ 좋은 예
func handleRequest(ctx context.Context, logger *slog.Logger) {
    logger.InfoContext(ctx, "Handling request")  // Context 전달
}

7. 레벨 혼동

// ❌ 나쁜 예
func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    // 이건 디버그 정보인데 Info로 로깅
    logger.Info("Variable value", "x", 123)
    
    // 이건 중요한 에러인데 Warn으로 로깅
    logger.Warn("Database connection failed")
}

// ✅ 좋은 예
func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    
    logger.Debug("Variable value", "x", 123)
    logger.Error("Database connection failed")
}

베스트 프랙티스

1. 구조화된 로깅

// ✅ 항상 구조화된 로그 사용
logger.Info("User registered",
    "user_id", user.ID,
    "username", user.Name,
    "email", user.Email,
    "registration_date", time.Now(),
)

// ❌ 문자열 연결 사용하지 않기
logger.Info(fmt.Sprintf("User %s registered with ID %d", user.Name, user.ID))

2. 일관된 키 이름

// ✅ 일관된 명명 규칙
const (
    KeyUserID     = "user_id"
    KeyRequestID  = "request_id"
    KeyDurationMS = "duration_ms"
    KeyError      = "error"
)

logger.Info("Request completed",
    KeyRequestID, "req-123",
    KeyUserID, 456,
    KeyDurationMS, 150,
)

3. 적절한 레벨 사용

// Debug: 개발/디버깅 정보
logger.Debug("Cache lookup", "key", key, "found", found)

// Info: 일반적인 정보
logger.Info("Server started", "port", 8080)

// Warn: 주의가 필요한 상황
logger.Warn("High memory usage", "percent", 85)

// Error: 에러 상황
logger.Error("Database query failed", "error", err, "query", sql)

4. Context 활용

func (s *Service) ProcessRequest(ctx context.Context, req Request) error {
    logger := s.logger.With(
        "request_id", req.ID,
        "user_id", req.UserID,
    )
    
    logger.InfoContext(ctx, "Processing request")
    
    // 비즈니스 로직
    
    logger.InfoContext(ctx, "Request processed successfully")
    return nil
}

5. 에러 로깅

// ✅ 에러 정보를 풍부하게
func processData(logger *slog.Logger) error {
    err := someOperation()
    if err != nil {
        logger.Error("Operation failed",
            "error", err,
            "operation", "processData",
            "retry_count", retryCount,
            slog.Group("context",
                "timestamp", time.Now(),
                "environment", os.Getenv("ENV"),
            ),
        )
        return err
    }
    return nil
}

6. 성능 고려

// ✅ 고빈도 로깅에서는 Enabled 체크
func processBatch(logger *slog.Logger, items []Item) {
    debugEnabled := logger.Enabled(context.Background(), slog.LevelDebug)
    
    for _, item := range items {
        // 비즈니스 로직
        
        if debugEnabled {
            logger.Debug("Item processed",
                "item_id", item.ID,
                "details", item.String(),  // 비싼 연산
            )
        }
    }
}

7. 테스트 가능한 로깅

// ✅ 의존성 주입으로 테스트 용이
type Service struct {
    logger *slog.Logger
    db     *Database
}

func NewService(logger *slog.Logger, db *Database) *Service {
    return &Service{
        logger: logger,
        db:     db,
    }
}

// 테스트
func TestService(t *testing.T) {
    // 테스트용 Logger
    var buf bytes.Buffer
    logger := slog.New(slog.NewJSONHandler(&buf, nil))
    
    service := NewService(logger, mockDB)
    service.Process()
    
    // 로그 검증
    if !strings.Contains(buf.String(), "Processing") {
        t.Error("Expected log not found")
    }
}

8. 그룹화와 네임스페이스

// ✅ 관련 정보 그룹화
func (s *Service) LogRequestMetrics(r *http.Request, duration time.Duration, status int) {
    s.logger.Info("Request completed",
        slog.Group("request",
            "method", r.Method,
            "path", r.URL.Path,
            "remote_addr", r.RemoteAddr,
        ),
        slog.Group("response",
            "status", status,
            "duration_ms", duration.Milliseconds(),
        ),
    )
}

정리

  • 기본: Debug, Info, Warn, Error 레벨, 키-값 쌍
  • Handler: TextHandler, JSONHandler, 커스텀 핸들러
  • Options: Level, AddSource, ReplaceAttr (마스킹)
  • Logger: With, WithGroup, 중첩 그룹
  • Context: InfoContext, 추적 ID 전파
  • 성능: Enabled 체크, LogAttrs, 조건부 로깅
  • 커스텀: Handler 인터페이스, 색상, 필터, 샘플링
  • 실전: HTTP 미들웨어, 에러, 메트릭, 추적, 집계
  • 실수: 짝 불일치, 민감 정보, 비효율, 값 전달, 전역, Context 무시
  • 베스트: 구조화, 일관성, 적절한 레벨, Context, 에러, 성능, 테스트, 그룹화