Quick Flow
var level = new(slog.LevelVar) // 기본값: Info
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: level,
}))
logger.With("service", "billing").InfoContext(ctx,
"request completed",
"method", r.Method,
"path", r.URL.Path,
"status", http.StatusOK,
)운영 로그는 메시지, 안정적인 키(key), 값의 타입으로 남깁니다. InfoContext는 핸들러(handler)에 context를 전달할 뿐 request ID를 자동으로 기록하지 않으므로, 필요한 상관관계 값은 속성(attribute)으로 명시합니다.
handler와 level
간단한 로그는 log로 충분할 수 있다
log.Println("server started")작은 스크립트나 로컬 도구는 표준 log 패키지만으로도 충분합니다. 다만 서버나 배치처럼 로그를 수집 시스템에서 읽어야 하면 key-value 기반 구조화 로그가 더 잘 맞습니다.
구조화 로그는 slog를 쓴다
logger.Info("user created", "user_id", user.ID, "role", user.Role)slog는 메시지와 속성을 분리합니다. "user_id"처럼 안정적인 key를 쓰면 로그 검색, 알림, 대시보드 구성이 쉬워집니다.
출력 형식과 최소 level은 handler가 정한다
textLogger := slog.New(slog.NewTextHandler(os.Stdout, nil))
jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))로컬 개발에서는 text handler가 읽기 쉽고, 운영 수집 환경에서는 JSON handler가 기계 처리에 유리합니다. HandlerOptions.Level의 기본 최소 level은 Info이며, 고정 slog.LevelDebug/slog.LevelInfo를 주면 실행 중에는 바꿀 수 없습니다.
var programLevel = new(slog.LevelVar) // zero value는 Info
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: programLevel,
AddSource: true,
})
programLevel.Set(slog.LevelDebug)LevelVar은 여러 goroutine에서 안전하게 읽고 쓸 수 있어 장애 조사 중 level을 높일 때 씁니다. AddSource는 소스 파일과 줄 번호를 추가하지만, 호출 위치와 출력량을 늘리므로 모든 고빈도 로그에 무조건 켜기보다 운영 비용을 확인합니다.
attribute와 context
key는 안정적으로, 공통 값은 With로 묶는다
requestLog := logger.With(
"request_id", requestID,
"user_id", userID,
)
requestLog.Info("payment created", "payment_id", paymentID)With는 이후 모든 로그에 속성을 포함하는 새 logger를 돌려줍니다. service, component, request ID처럼 같은 경계에서 반복되는 값을 묶습니다. API와 대시보드가 user_id, duration_ms, status를 기준으로 검색한다면 키 이름, 값 타입, 단위를 중간에 바꾸지 않습니다.
WithGroup("http")는 method, path, status처럼 관련 attribute를 한 묶음으로 만듭니다. Text/JSON 출력에서 실제 key 표기는 handler에 따라 다를 수 있으므로, 수집 쿼리와 함께 한 번 확인합니다.
context는 자동 attribute가 아니다
logger.InfoContext(ctx, "db query completed", "duration_ms", duration.Milliseconds())InfoContext, ErrorContext 등은 context를 handler에게 전달합니다. 내장 handler는 context value를 자동으로 request ID나 trace ID로 기록하지 않으므로, 상관관계 값이 필요하면 middleware에서 logger에 With로 넣거나 사용자 정의 handler가 context를 읽도록 구현합니다. context 취소는 log record 처리를 취소하는 신호가 아닙니다.
민감 정보와 의존성
민감 attribute는 입력 지점에서 제거한다
func redact(_ []string, a slog.Attr) slog.Attr {
if a.Key == "authorization" || a.Key == "password" {
return slog.String(a.Key, "[REDACTED]")
}
return a
}
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: redact,
})ReplaceAttr는 출력 전에 그룹 밖 속성을 바꾸거나 제거합니다. 토큰(token), password, cookie, 전체 query string처럼 유입 지점에 민감 값이 섞일 수 있는 속성은 애초에 넘기지 않는 것이 우선이고, 공통 경계에는 위와 같은 최후 방어를 둡니다. 비싼 값을 debug 로그에 넣을 때는 LogValuer를 구현하면 로그가 비활성일 때 계산을 미룰 수 있습니다.
logger는 경계에 주입한다
type Server struct {
logger *slog.Logger
}전역 logger만 쓰면 테스트와 구성 변경이 어려워질 수 있습니다. 서버, repository, worker 같은 경계에 logger를 주입하면 출력 형식과 level을 환경에 맞춰 바꾸기 좋습니다. 애플리케이션 전체 기본 logger를 의도적으로 바꿀 때만 slog.SetDefault를 사용합니다.
주의할 점
로그 key는 나중에 검색 기준이 되므로 자주 바꾸지 않는 편이 좋습니다. 비밀번호, token, 개인정보는 로그에 남기지 않습니다. 오류 객체만 문자열로 흘려보내지 말고 실패 위치, 대상 ID, duration 같은 운영 단서를 안정적인 key로 같이 남기되, 오류의 원인 체인은 errors wrapping 규약으로 보존합니다.
참고 링크
2 sources