Menu

Golang slog와 log: Go의 구조화된 로깅

Go에서 로그를 남기는 방법: 플래그와 log.Fatal이 있는 전통적인 log 패키지, 그리고 레벨, 키-값 속성, 텍스트와 JSON 핸들러, With로 맥락을 담는 로거를 갖춘 구조화된 로깅용 log/slog(Go 1.21)를 다룹니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

예제 하나로 보는 slog

log/slog(Go 1.21)는 메시지, 레벨, 키-값 속성으로 된 구조화된 레코드를 씁니다.

각 줄은 time=... level=INFO msg="user logged in" user=ada attempts=1 형태로 나옵니다. 모든 값이 별도의 필드이므로 로그 수집기(Loki, Elasticsearch, CloudWatch, Datadog)가 자유 텍스트에 정규식을 쓰지 않고도 user=adalevel=ERROR로 필터링할 수 있습니다.

이 페이지의 예제는 출력이 순서대로 보이도록 os.Stdout에 씁니다. 실제 서비스에서는 로그를 보통 기본 로거가 쓰는 곳이기도 한 os.Stderr로 보냅니다.

레벨

레벨용도
slog.LevelDebug-4개발자를 위한 세부 정보, 운영 환경에서는 끔
slog.LevelInfo0일반 이벤트: 시작됨, 요청 처리됨, 작업 끝남
slog.LevelWarn4프로그램이 처리한 예상 밖의 일
slog.LevelError8작업 실패

핸들러는 최소 레벨보다 낮은 레코드를 버리며, 기본 최소 레벨은 Info입니다. 그래서 Level: slog.LevelDebug로 핸들러를 설정하기 전까지 slog.Debug(...)는 아무것도 출력하지 않습니다. 값 사이의 간격은 slog.Level(2) 같은 사용자 정의 레벨을 위한 여유입니다.

런타임에(플래그, 관리용 엔드포인트, 시그널로) 레벨을 바꾸려면 옵션에 slog.LevelVar를 넣고 나중에 Set을 호출하세요:

var level slog.LevelVar // zero value: Info
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: &level}))
level.Set(slog.LevelDebug) // from now on, debug records are written

텍스트냐 JSON이냐

slog.NewTextHandler는 터미널에서 읽기 쉬운 key=value 쌍을 씁니다. slog.NewJSONHandler는 대부분의 로그 파이프라인이 기대하는 형식인 한 줄에 JSON 객체 하나를 씁니다. 로깅 호출은 그대로이고 핸들러만 바뀝니다.

값은 타입을 유지합니다. JSON 출력에서 status는 숫자이고 retry는 불리언이며, time.Duration은 텍스트에서 42ms로, JSON에서 나노초로 출력되고, error는 메시지를 출력합니다. ReplaceAttr는 속성을 바꾸거나 제거하는 훅입니다. 여기서는 타임스탬프를 빼는 데 썼지만, 실제로는 키 이름을 바꾸거나(msgmessage로) 값을 가리는 데 씁니다.

속성

느슨한 타입 형태는 키와 값을 번갈아 씁니다: "user", "ada", "attempts", 3. 짧고, 실패하는 경우는 하나뿐입니다. 인자 개수가 홀수일 때입니다. 남은 값은 !BADKEY라는 키로 기록됩니다. go vet이 잡아 줍니다:

./main.go:14:2: call to slog.Info missing a final value

타입 안전성과 약간의 할당 절감을 원한다면 속성 생성자를 쓰고, 성능이 중요한 경로에서는 LogAttrs를 쓰세요:

logger.Info("order placed",
	slog.Int("order_id", 1017),
	slog.String("currency", "EUR"),
	slog.Float64("total", 59.90),
	slog.Duration("took", elapsed),
)

logger.LogAttrs(ctx, slog.LevelInfo, "order placed", slog.Int("order_id", 1017))

코드베이스 전체에서 키 이름 규칙을 하나로 정하세요(어떤 패키지에서는 userID, 다른 곳에서는 uid가 아니라 어디서나 user_id). 로그 시스템의 쿼리가 그 규칙에 의존합니다.

With: 맥락을 담는 로거

logger.With(attrs...)는 모든 레코드에 그 속성을 추가하는 새 로거를 반환합니다. 요청마다 또는 작업마다 하나씩 만들면 그 로거가 쓰는 모든 줄을 서로 묶을 수 있습니다:

호출할 때마다 반복하지 않아도 모든 줄에 service, version, request_id, user가 붙습니다. slog.Group은 속성을 중첩시키며, JSON 핸들러는 이를 중첩 객체("payment":{"amount":25,"currency":"USD"})로, 텍스트 핸들러는 점으로 이은 키(payment.amount=25)로 씁니다. logger.WithGroup("db")는 그 로거의 이후 모든 속성을 그룹 아래에 둡니다.

요청 범위의 로거는 매개변수나 구조체 필드로 넘기세요. context.Context에 저장할 수도 있지만 의존성을 숨기게 됩니다. slog의 InfoContext(ctx, ...) 메서드는 컨텍스트를 핸들러에 넘기며, 커스텀 핸들러는 이를 이용해 트레이스 ID를 꺼낼 수 있습니다.

LogValuer로 비밀 값 숨기기

타입은 slog.LogValuer를 구현해서 자신이 로그에 남는 방식을 정할 수 있습니다. 누가 값을 로그로 남기든 비밀번호와 토큰이 로그에 들어가지 않게 해 줍니다:

User는 ID와 이메일만 남기고, Token을 따로 로그로 남기면 REDACTED가 출력됩니다. 핸들러는 레코드가 실제로 쓰일 때만 LogValue를 호출하므로, 계산 비용이 큰 값에도 쓸 수 있습니다.

전통적인 log 패키지

logslog보다 먼저 있었고, 작은 프로그램과 스크립트에는 여전히 괜찮습니다. 날짜와 시간 접두사를 붙여 표준 에러에 줄을 씁니다:

플래그추가되는 것
log.LstdFlags (기본값)2009/11/10 23:00:00 날짜와 시간
log.Lmicroseconds시간에 마이크로초
log.LUTCUTC 기준 시간
log.Lshortfile / log.Llongfilemain.go:14 / 전체 경로
log.Lmsgprefix접두사를 줄 맨 앞이 아니라 메시지 앞에 둠

종료하거나 패닉을 일으키는 함수가 세 종류 있고, 그 차이가 중요합니다:

  • log.Fatal, log.Fatalf, log.Fatalln은 출력한 뒤 os.Exit(1)을 호출합니다. 지연된 호출은 실행되지 않습니다. main의 시작 실패에만 쓰고, 라이브러리 코드나 요청 핸들러에서는 절대 쓰지 마세요.
  • log.Panic 계열은 출력한 뒤 패닉을 일으키므로, 지연된 호출이 실행되고 패닉을 복구할 수 있습니다.
  • 나머지 함수는 모두 줄을 쓸 뿐입니다.

파일에 로그를 남기려면 파일을 열어 log.Newlog.SetOutput에 넘기세요. io.MultiWriter(os.Stderr, f)는 양쪽에 모두 씁니다.

log와 slog를 함께 쓰기

slog.SetDefault(logger)logger를 최상위 slog.Info 함수들의 기본값으로 만들고, log 패키지의 출력도 그것으로 보냅니다. 그러면 여러분의 코드나 의존성에 있는 기존 log.Printf 호출이 Info 레벨의 구조화된 레코드로 나옵니다:

slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
log.Printf("legacy message") // {"time":"...","level":"INFO","msg":"legacy message"}

SetDefault 전에는 기본 slog 로거가 log 패키지를 통해 쓰기 때문에, 그냥 slog.Info("hi")를 호출하면 2026/09/23 14:30:00 INFO hi가 출력됩니다.

실용적인 규칙

  • 오류는 로그로 남기거나 반환하되, 둘 다 하지 마세요. 오류를 로그로 남기고 반환하기도 하는 함수는 같은 실패가 호출 스택의 모든 단계에서 기록되게 만듭니다. 맥락을 붙여 위로 반환하고, 처리하는 곳에서 한 번만 로그로 남기세요.
  • 변하는 데이터는 메시지가 아니라 속성에 넣으세요. logger.Info("user created", "user_id", id)는 로그 시스템에서 잘 묶이지만, logger.Info(fmt.Sprintf("user %d created", id))는 사용자마다 다른 메시지를 만듭니다.
  • 비밀 값이나 요청 본문 전체를 절대 로그로 남기지 마세요. LogValuerReplaceAttr로 가리세요.
  • 운영 환경에서는 JSON, 개발 환경에서는 텍스트를 쓰세요. 시작할 때 플래그나 환경 변수로 핸들러를 고르세요.
  • 레벨을 신중하게 고르세요. 모든 것을 Error로 남기면 오류 알림이 소음이 됩니다.

자주 묻는 질문

Go의 slog란 무엇인가요?

log/slog는 Go 1.21에서 표준 라이브러리에 추가된 구조화된 로깅 패키지입니다. 형식화된 문자열 대신 각 레코드가 메시지, 레벨(Debug, Info, Warn, Error), 키-값 속성을 가지며, 핸들러가 이를 key=value 텍스트나 JSON으로 씁니다: slog.Info("login", "user", "ada", "attempts", 3).

slog에서 디버그 로그를 켜려면 어떻게 하나요?

기본 최소 레벨이 Info라서 slog.Debug는 아무것도 출력하지 않습니다. 더 낮은 레벨로 핸들러를 만들어 기본값으로 설정하세요: slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))). 프로그램 실행 중에 레벨을 바꾸고 싶다면 상수 대신 slog.LevelVar를 쓰세요.

Go에서 log와 slog의 차이는 무엇인가요?

log는 선택적인 타임스탬프 접두사가 붙은 자유 형식의 줄을 쓰며 레벨이 없습니다. slog는 로그 수집기가 파싱하고 필터링할 수 있는 레벨과 타입이 있는 키-값 속성을 가진 레코드를 씁니다. 둘 다 표준 라이브러리에 있으며, slog.SetDefaultlog 패키지의 출력도 slog 핸들러로 보냅니다.

log.Fatal은 지연된 함수를 실행하나요?

아니요. log.Fatallog.Fatalf는 메시지를 출력하고 os.Exit(1)을 호출하므로 지연된 호출을 모두 건너뜁니다. 정리할 것이 없는 main이나 초기 설정 코드에서만 쓰세요. log.Panic은 대신 패닉을 일으키므로 지연된 호출이 실행됩니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기