본문 바로가기

프로그래밍/golang

[Golang] slog : Context Handler 패턴

 

 

Go 1.21 버전부터 추가된 slog 는 구조화된 로깅을 지원하며,
특히 Handler 기반 구조를 통해 로깅 로직을 유연하게 확장 할 수 있는데요.

 

실무에서는 로그 추적을 위해서 다음과 같은 메타데이터를 남기는 경우가 많이 있습니다.

  • request_id
  • trace_id
  • user_id
  • service
  • ip
  • path

위 값들은 고유 값들로 각 요청과 종료의 범위, 흐름을 파악할 때 유용하게 사용이 됩니다.
이 값을 매번 수동으로 넣는 방식을 사용하면 번거럽고, 누락하는 문제가 발생하기도 합니다.

logger.Info("payment completed",
	slog.String("request_id", requestID),
	slog.String("user_id", userID),
)

 

위 로그만 본다면 단순하고 어려울 것도 없지만, 서비의 규모가 커질 수록 동일한 코드가 이곳 저곳 반복 작성이 될 것입니다.

 

 

1. Context Handler 패턴이란?

Request -> Context Save -> slog Handler 추출 -> add log

 

Context Handler 패턴은 어플리케이션의 실행 흐름을 저장하고 있는 context.Context와 로깅 시스템을 연결하는 역할을 합니다.

일반적으로 요청이 들어오면 미들웨어에서 request_id 등과 같은 고유 키를 만들어 context에 저장합니다.

이를 로그로 남기려면 매번 로깅 함수에 인자로 넘겨주어야하는데, context Handler는 로그가 출력되기 직전에 context 내부에 필요한 메타데이터 로그를 합쳐서 만들어 줍니다.

 

2. 이 방법을 쓰는 이유

  • 매 호출마다 반복적인 request_id 같은 동일 인자값을 로깅 함수에 인자 값으로 넘겨주지 않아도 됩니다.
  • 실수로 ID 값들을 누락하더라도 핸들러 차원에서 주입해서 누락 방지를 해줍니다
  • 비즈니스 로직에서는 비즈니스 함수가 로깅을 위한 파라미터를 들고 다니지 않아도 됩니다. (관심사 분리)

 

3. 예제 코드

package main

import (
	"context"
	"log/slog"
	"os"
)

type ctxKey string

const (
	RequestIDKey ctxKey = "request_id"
	TraceIDKey   ctxKey = "trace_id"
)

type ContextHandler struct {
	slog.Handler
}

func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error {
	if rid, ok := ctx.Value(RequestIDKey).(string); ok {
		r.AddAttrs(slog.String("request_id", rid))
	}

	if tid, ok := ctx.Value(TraceIDKey).(string); ok {
		r.AddAttrs(slog.String("trace_id", tid))
	}
	return h.Handler.Handle(ctx, r)
}

func (h *ContextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	return &ContextHandler{Handler: h.Handler.WithAttrs(attrs)}
}

func (h *ContextHandler) WithGroup(name string) slog.Handler {
	return &ContextHandler{Handler: h.Handler.WithGroup(name)}
}

func AppendCtx(parent context.Context, rid, tid string) context.Context {
	ctx := context.WithValue(parent, RequestIDKey, rid)
	return context.WithValue(ctx, TraceIDKey, tid)
}

func main() {
	innerHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelDebug,
	})

	logger := slog.New(&ContextHandler{innerHandler})

	// 요청 컨텍스트 생성
	ctx := AppendCtx(context.Background(), "REQ-12345", "TRACE-67890")

	logger.InfoContext(ctx, "user login attempt", slog.String("user_email", "reo@example.com"))
	performBusinessLogic(ctx, logger)
}

func performBusinessLogic(ctx context.Context, log *slog.Logger) {
	// 비즈니스 로직 함수는 내부에서 request_id나 trace_id를 전혀 몰라도 된다.
	log.DebugContext(ctx, "database query completed")
}