
이번 포스팅에서는 'slog' 를 활용해 로그의 라우팅을 설계해보겠습니다.
실제 서비스에서 발생하는 로그들의 가치에도 차이가 있습니다.
서비스의 정상 흐름을 보여주는
'Info' 로그와 에러 정보를 알려주는 'Error' 로그는
전달되는 속도와 저장되는 장소, 그리고 읽는 방식이 달라야 합니다.
1. 왜 로그는 왜 라우팅해야 하는가?
대부분의 경우 로그들을 단순히 os.Stdout로 출력을 시키게 되는데요.
보통은 큰 문제가 없지만 로그가 많고 서비스의 규모가 커지면 다음과 같은 문제에 직면하게 됩니다.
- 가독성 : 수만 건의 로그 사이에 중요한 에러 로그가 묻혀버립니다.
- 효율성 : 수만 건의 정상 로그 사이 에러 로그가 섞여 있을 경우 필터링의 부하
- 비용 및 관리 : 분석용 로그와 시스템 에러의 채널을 서로 분리하여 관리하면 경우에 따라 비용과 관리 측면에서 유리합니다
2. 전체 코드
레벨에 따라 출력 스트림(stdout/stderr)이 나누어지며,
중간에 특정 태그를 가진 경우 제거하는 필터 계층도 함께 포함했습니다.
func main() {
opts := &slog.HandlerOptions{Level: slog.LevelDebug}
// 목적지 정의
out := slog.NewJSONHandler(os.Stdout, opts) // 정상 로그용
errOut := slog.NewJSONHandler(os.Stderr, opts) // 에러 로그용
// 핸들러 체이닝 및 라우팅 구성
h := &levelRouterHandler{
low: &filterHandler{
next: out,
drop: func(_ context.Context, r slog.Record) bool {
return hasAttr(r, "tag", "noise")
},
},
high: errOut,
cut: slog.LevelError,
}
log := slog.New(h)
log.Info("startup", "time", time.Now().Format(time.RFC3339Nano))
log.Debug("polling", "tag", "noise", "iteration", 1)
log.Debug("cache miss", "tag", "cache", "key", "u:123")
log.Error("db error", "code", "E_DB", "detail", "connection refused")
}
type filterHandler struct {
next slog.Handler
drop func(ctx context.Context, r slog.Record) bool
}
func (h *filterHandler) Handle(ctx context.Context, r slog.Record) error {
if h.drop != nil && h.drop(ctx, r) {
return nil // 출력 안함
}
return h.next.Handle(ctx, r)
}
type levelRouterHandler struct {
low slog.Handler
high slog.Handler
cut slog.Level
}
func (h *levelRouterHandler) Handle(ctx context.Context, r slog.Record) error {
if r.Level >= h.cut {
return h.high.Handle(ctx, r) // 중요 로그는 high
}
return h.low.Handle(ctx, r) // 일반 로그 low
}
func hasAttr(r slog.Record, key string, want string) bool {
found := false
r.Attrs(func(a slog.Attr) bool {
if a.Key == key && strings.Contains(a.Value.String(), want) {
found = true
return false // 속성 찾았음으로 반복 종료 (false)
}
return true // 계속 반속
})
return found
}
3. 핵심 정리
- Stdout(1) , Stderr(2) 분리
- 유닉스 계열 시스템에서는 이 둘은 물리적으로 구분된 통로입니다.
- Stdout : 서비스 정상 실행 결과이며 보통 로그 수집기를 통해 분석 서버로 이동합니다.
- Stderr : 서비스 에러 신호이며 버퍼링 없이 즉시 출력되는 특징이 있어서 실시간 모니터링 하기 좋습니다.
- 성능 최적화
- hasAttr 함수 내부에서 return false를 사용하는 점을 살펴보면, 일반적으로 for 루프에서 break와 동일합니다.
원하는 값을 찾는 즉시 순회를 멈춤으로써 오버헤드 방지합니다.
- hasAttr 함수 내부에서 return false를 사용하는 점을 살펴보면, 일반적으로 for 루프에서 break와 동일합니다.
4. 전체 코드
https://github.com/reochoi109/go-handbook/blob/main/log/slog/advanced/handler_chain/main.go
go-handbook/log/slog/advanced/handler_chain/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
'프로그래밍 > golang' 카테고리의 다른 글
| [Golang] 에러 분류를 통한 Retry 구현 (0) | 2026.03.18 |
|---|---|
| [Golang] errors.Is와 As를 활용한 에러 핸들링 (0) | 2026.03.18 |
| [Golang] Slog를 활용한 로그 샘플링 구현하기 (0) | 2026.03.18 |
| [Golang] Slog를 활용한 마스킹 핸들러 구현 (0) | 2026.03.18 |
| [Golang] 표준 로깅 라이브러리 Slog (0) | 2026.03.18 |