본문 바로가기

프로그래밍/golang

[Golang] Slog를 활용한 로그 샘플링 구현하기

golang slog sampling

지난 포스팅에서는 "slog"를 활용한 민감 정보를 가리는 마스킹 핸들러를 알아보았습니다.
이번에는 보안만큼 중요한 운영 비용과 시스템 과부화 관리에 대해서 함께 알아보겠습니다.

초당 수만 건씩 발생하는 단순 상태 로그들을 전부 저장하면 로그 서버(ELK,CloudWatch)의 비용이 계속 증가하게 됩니다.
중요한 에러 로그를 찾고, 검색하는 것에 방해가 되는 경우가 많습니다. 이때 필요한 방법이 "로그 샘플링" 입니다. 

 

1. 작동 원리

특정 범위의 로그는 설정한 비율로만 남기고 중요한 로그는 유실 없이 기록하는 전략입니다.

  • 특정 범위 로그 : 로그 레벨 (debug,info,warn.... 등)
  • 설정한 비율 : N회당 1개 (10번 중 1번 출력하기)

 

2. 전체 코드

slog.handler 인터페이스를 구현합니다.

 

type samplingHandler struct {
	next   slog.Handler
	everyN uint64

	minLevel slog.Level
	maxLevel slog.Level

	keepAbove slog.Level
	seq       *atomic.Uint64
}

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

	h := &samplingHandler{
		next:      base,            // 최종 출력을 담당할 핸들러
		everyN:    10,              // 10번째 로그마다 1개씩 출력 (나머지는 드랍)
		minLevel:  slog.LevelInfo,  // 샘플링을 적용할 최소 레벨
		maxLevel:  slog.LevelInfo,  // 샘플링을 적용할 최대 레벨 (여기서는 INFO만 대상)
		keepAbove: slog.LevelError, // 이 레벨(ERROR) 이상은 샘플링 없이 무조건 통과
		seq:       new(atomic.Uint64),
	}

	log := slog.New(h)
	ctx := context.Background()
	for i := 0; i < 35; i++ {
		log.InfoContext(ctx, "heartbeat", "i", i)

		if i%17 == 0 {
			log.ErrorContext(ctx, "db error", "i", i, "code", "E_DB")
		}
		time.Sleep(5 * time.Millisecond)
	}
}

func (h *samplingHandler) Handle(ctx context.Context, r slog.Record) error {
	// 1. 중요한 로그는 샘플링 로직타지 않고 즉시 출력
	if r.Level >= h.keepAbove {
		return h.next.Handle(ctx, r)
	}

	// 2. 샘플링 대상 레벨 범위가 아니면 출력
	if r.Level < h.minLevel || r.Level > h.maxLevel || h.everyN == 0 {
		return h.next.Handle(ctx, r)
	}

	// 3. 카운터 증가
	n := h.seq.Add(1)

	// 4. n번째로그가 아니면 드랍
	if n%h.everyN != 0 {
		return nil
	}
	return h.next.Handle(ctx, r)
}

 

 

3. 운영 관점에서 이점

  • 비용 절감 : 불필요한 로그량을 줄여, 인프라 비용을 절감합니다.
  • 노이즈 제거 : 진짜 에러 로그를 찾는 시간이 감소합니다.
  • 성능 최적화 : 로그 직렬화 및 네트워크 전송 이전에 필터링이 이루어지므로 성능 저하를 줄여줍니다.

 

4. 주의 사항

  • 복사 : slog.Handler는 속성을 추가할 때 구조체 자체가 복사가 됩니다.
    그래서 atomic 값을 직접 들고 복사하게 되면, 복사본과 카운터가 따로 놀게 됩니다.
    반드시 포인터를 사용해서 하나의 카운터를 공유할 수 있도록 해야 합니다.
type samplingHandler struct {
	next   slog.Handler
	everyN uint64

	minLevel slog.Level
	maxLevel slog.Level

	keepAbove slog.Level
	seq       *atomic.Uint64
}

 

 

5. 전체 코드

https://github.com/reochoi109/go-handbook/blob/main/log/slog/advanced/sampling/main.go

 

go-handbook/log/slog/advanced/sampling/main.go at main · reochoi109/go-handbook

A personal handbook of Go patterns and best practices. Lightweight, practical code snippets for real-world backend development. - reochoi109/go-handbook

github.com