[Go] slog
개요
Go 1.21부터 추가된 log/slog 패키지는 구조화된 로깅을 위한 표준 라이브러리입니다.
주요 특징:
- 구조화된 로그: 키-값 쌍으로 로그 기록
- 레벨 기반: Debug, Info, Warn, Error 레벨 지원
- 핸들러 패턴: TextHandler, JSONHandler, 커스텀 핸들러
- 성능: 고성능 로깅 (할당 최소화)
- 컨텍스트 지원: context.Context 전파
- 타입 안전: 정적 타입 체크
- 확장 가능: Handler 인터페이스로 커스터마이징
기본 사용법
1. 로그 레벨
package main
import (
"log/slog"
)
func main() {
// 기본 핸들러 (TextHandler, INFO 레벨)
slog.Debug("This won't appear") // DEBUG < INFO
slog.Info("Application started")
slog.Warn("Low disk space")
slog.Error("Database connection failed")
// 출력:
// 2024/01/01 10:00:00 INFO Application started
// 2024/01/01 10:00:00 WARN Low disk space
// 2024/01/01 10:00:00 ERROR Database connection failed
}
2. 속성 추가
func main() {
// 키-값 쌍으로 속성 추가
slog.Info("User logged in", "user_id", 123, "username", "alice")
// 2024/01/01 10:00:00 INFO User logged in user_id=123 username=alice
// 여러 속성
slog.Error("Request failed",
"method", "POST",
"path", "/api/users",
"status", 500,
"duration_ms", 1234,
)
}
3. 타입 안전 속성
func main() {
// slog.Attr 사용 (권장)
slog.Info("Server starting",
slog.String("host", "localhost"),
slog.Int("port", 8080),
slog.Bool("tls_enabled", true),
slog.Duration("timeout", 30*time.Second),
)
// Any 타입
slog.Info("User data",
slog.Any("user", map[string]interface{}{
"id": 123,
"name": "Alice",
}),
)
}
Handler
1. TextHandler
import (
"log/slog"
"os"
)
func main() {
// TextHandler 생성
handler := slog.NewTextHandler(os.Stdout, nil)
logger := slog.New(handler)
logger.Info("Server started", "port", 8080)
// time=2024-01-01T10:00:00.000Z level=INFO msg="Server started" port=8080
}
2. JSONHandler
func main() {
// JSONHandler 생성
handler := slog.NewJSONHandler(os.Stdout, nil)
logger := slog.New(handler)
logger.Info("Server started", "port", 8080)
// {"time":"2024-01-01T10:00:00.000Z","level":"INFO","msg":"Server started","port":8080}
}
3. 기본 Logger 설정
func main() {
// 전역 기본 Logger 설정
handler := slog.NewJSONHandler(os.Stdout, nil)
logger := slog.New(handler)
slog.SetDefault(logger)
// 이후 slog.Info 등은 새 핸들러 사용
slog.Info("Using JSON handler")
}
4. 파일 출력
func main() {
// 파일로 로그 출력
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic(err)
}
defer file.Close()
handler := slog.NewJSONHandler(file, nil)
logger := slog.New(handler)
logger.Info("Log to file", "timestamp", time.Now())
}
5. MultiWriter
import "io"
func main() {
// stdout과 파일에 동시 출력
file, _ := os.Create("app.log")
defer file.Close()
multi := io.MultiWriter(os.Stdout, file)
handler := slog.NewJSONHandler(multi, nil)
logger := slog.New(handler)
logger.Info("Logged to both stdout and file")
}
HandlerOptions
1. 로그 레벨 설정
func main() {
// DEBUG 레벨부터 출력
opts := &slog.HandlerOptions{
Level: slog.LevelDebug,
}
handler := slog.NewJSONHandler(os.Stdout, opts)
logger := slog.New(handler)
logger.Debug("Debug message") // 이제 출력됨
logger.Info("Info message")
}
2. 동적 레벨
func main() {
// 런타임에 레벨 변경 가능
var level slog.LevelVar
level.Set(slog.LevelInfo)
opts := &slog.HandlerOptions{
Level: &level,
}
handler := slog.NewJSONHandler(os.Stdout, opts)
logger := slog.New(handler)
logger.Debug("Not shown") // INFO 레벨이므로 출력 안됨
// 레벨 변경
level.Set(slog.LevelDebug)
logger.Debug("Now shown") // 이제 출력됨
}
3. 소스 위치 추가
func main() {
opts := &slog.HandlerOptions{
AddSource: true, // 파일명과 라인 번호 추가
}
handler := slog.NewJSONHandler(os.Stdout, opts)
logger := slog.New(handler)
logger.Info("With source location")
// {"time":"...","level":"INFO","source":{"file":"main.go","line":15},"msg":"With source location"}
}
4. ReplaceAttr
func main() {
opts := &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// 시간 포맷 변경
if a.Key == slog.TimeKey {
return slog.String("timestamp", time.Now().Format(time.RFC3339))
}
// 레벨을 소문자로
if a.Key == slog.LevelKey {
level := a.Value.Any().(slog.Level)
return slog.String("severity", strings.ToLower(level.String()))
}
return a
},
}
handler := slog.NewJSONHandler(os.Stdout, opts)
logger := slog.New(handler)
logger.Info("Custom attributes")
// {"timestamp":"2024-01-01T10:00:00Z","severity":"info","msg":"Custom attributes"}
}
5. 민감 정보 마스킹
func main() {
opts := &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// password 필드 마스킹
if a.Key == "password" {
return slog.String("password", "***REDACTED***")
}
// credit_card 필드 마스킹
if a.Key == "credit_card" {
return slog.String("credit_card", "****-****-****-****")
}
return a
},
}
handler := slog.NewJSONHandler(os.Stdout, opts)
logger := slog.New(handler)
logger.Info("User login",
"username", "alice",
"password", "secret123",
)
// {"time":"...","level":"INFO","msg":"User login","username":"alice","password":"***REDACTED***"}
}
Logger 생성 및 사용
1. With 메서드
func main() {
// 공통 속성을 가진 Logger 생성
baseLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// 요청별 Logger
requestLogger := baseLogger.With(
"request_id", "req-123",
"user_id", 456,
)
requestLogger.Info("Processing request")
requestLogger.Info("Request completed")
// 모든 로그에 request_id와 user_id가 포함됨
}
2. WithGroup
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// 그룹화된 속성
serverLogger := logger.WithGroup("server")
serverLogger.Info("Starting",
"host", "localhost",
"port", 8080,
)
// {"time":"...","level":"INFO","msg":"Starting","server":{"host":"localhost","port":8080}}
}
3. 중첩 그룹
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger = logger.WithGroup("app").WithGroup("database")
logger.Info("Connected",
"host", "localhost",
"port", 5432,
)
// {"time":"...","level":"INFO","msg":"Connected","app":{"database":{"host":"localhost","port":5432}}}
}
4. 인라인 그룹
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("Server metrics",
"cpu", 45.5,
slog.Group("memory",
"used", 1024,
"total", 4096,
),
slog.Group("disk",
"read", 100,
"write", 50,
),
)
// {"time":"...","level":"INFO","msg":"Server metrics","cpu":45.5,"memory":{"used":1024,"total":4096},"disk":{"read":100,"write":50}}
}
Context 사용
1. Context 전달
import "context"
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
ctx := context.Background()
// Context와 함께 로깅
logger.InfoContext(ctx, "Processing request")
logger.ErrorContext(ctx, "Request failed", "error", "timeout")
}
2. Context에서 Logger 추출
type contextKey string
const loggerKey contextKey = "logger"
func WithLogger(ctx context.Context, logger *slog.Logger) context.Context {
return context.WithValue(ctx, loggerKey, logger)
}
func LoggerFromContext(ctx context.Context) *slog.Logger {
if logger, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
return logger
}
return slog.Default()
}
func processRequest(ctx context.Context) {
logger := LoggerFromContext(ctx)
logger.Info("Processing...")
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger = logger.With("request_id", "req-123")
ctx := WithLogger(context.Background(), logger)
processRequest(ctx)
}
3. Trace ID 전파
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// Trace ID를 Context에 저장
ctx := context.WithValue(context.Background(), "trace_id", "trace-abc-123")
// Context에서 Trace ID 추출하여 로깅
traceID := ctx.Value("trace_id").(string)
logger = logger.With("trace_id", traceID)
logger.InfoContext(ctx, "Request received")
logger.InfoContext(ctx, "Request processed")
// 모든 로그에 trace_id가 포함됨
}
성능 최적화
1. Enabled 체크
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
// 비효율적: Debug가 비활성화되어도 문자열 생성
logger.Debug("Expensive operation: " + expensiveOperation())
// 효율적: Enabled 체크
if logger.Enabled(context.Background(), slog.LevelDebug) {
logger.Debug("Expensive operation: " + expensiveOperation())
}
}
func expensiveOperation() string {
// 무거운 작업
return "result"
}
2. LogAttrs
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// 일반적인 방법 (가변 인자)
logger.Info("Message", "key1", "value1", "key2", "value2")
// LogAttrs 사용 (더 효율적)
logger.LogAttrs(context.Background(), slog.LevelInfo, "Message",
slog.String("key1", "value1"),
slog.String("key2", "value2"),
)
}
3. 사전 할당된 Attributes
var (
// 자주 사용하는 속성 미리 생성
serviceAttr = slog.String("service", "my-app")
versionAttr = slog.String("version", "1.0.0")
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// 사전 할당된 속성 재사용
logger.LogAttrs(context.Background(), slog.LevelInfo, "Application started",
serviceAttr,
versionAttr,
)
}
4. 조건부 로깅
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelWarn,
}))
// Debug와 Info는 무시됨 (할당 최소화)
logger.Debug("Debug message")
logger.Info("Info message")
logger.Warn("Warn message") // 출력됨
}
커스텀 Handler
1. Handler 인터페이스
type Handler interface {
Enabled(context.Context, Level) bool
Handle(context.Context, Record) error
WithAttrs(attrs []Attr) Handler
WithGroup(name string) Handler
}
2. 간단한 커스텀 Handler
type CustomHandler struct {
slog.Handler
prefix string
}
func NewCustomHandler(w io.Writer, prefix string, opts *slog.HandlerOptions) *CustomHandler {
return &CustomHandler{
Handler: slog.NewTextHandler(w, opts),
prefix: prefix,
}
}
func (h *CustomHandler) Handle(ctx context.Context, r slog.Record) error {
// 모든 메시지에 prefix 추가
r.Message = h.prefix + r.Message
return h.Handler.Handle(ctx, r)
}
func main() {
handler := NewCustomHandler(os.Stdout, "[APP] ", nil)
logger := slog.New(handler)
logger.Info("Server started")
// time=... level=INFO msg="[APP] Server started"
}
3. 색상 출력 Handler
type ColorHandler struct {
slog.Handler
}
func NewColorHandler(w io.Writer, opts *slog.HandlerOptions) *ColorHandler {
return &ColorHandler{
Handler: slog.NewTextHandler(w, opts),
}
}
func (h *ColorHandler) Handle(ctx context.Context, r slog.Record) error {
// 레벨별 색상
var color string
switch r.Level {
case slog.LevelDebug:
color = "\033[36m" // Cyan
case slog.LevelInfo:
color = "\033[32m" // Green
case slog.LevelWarn:
color = "\033[33m" // Yellow
case slog.LevelError:
color = "\033[31m" // Red
default:
color = "\033[0m" // Reset
}
reset := "\033[0m"
// 색상 추가
r.Message = color + r.Message + reset
return h.Handler.Handle(ctx, r)
}
func main() {
handler := NewColorHandler(os.Stdout, nil)
logger := slog.New(handler)
logger.Debug("Debug message") // Cyan
logger.Info("Info message") // Green
logger.Warn("Warn message") // Yellow
logger.Error("Error message") // Red
}
4. 필터링 Handler
type FilterHandler struct {
slog.Handler
filter func(slog.Record) bool
}
func NewFilterHandler(h slog.Handler, filter func(slog.Record) bool) *FilterHandler {
return &FilterHandler{
Handler: h,
filter: filter,
}
}
func (h *FilterHandler) Handle(ctx context.Context, r slog.Record) error {
if !h.filter(r) {
return nil // 필터링
}
return h.Handler.Handle(ctx, r)
}
func main() {
baseHandler := slog.NewJSONHandler(os.Stdout, nil)
// "password" 속성이 있는 로그 필터링
handler := NewFilterHandler(baseHandler, func(r slog.Record) bool {
hasPassword := false
r.Attrs(func(a slog.Attr) bool {
if a.Key == "password" {
hasPassword = true
return false
}
return true
})
return !hasPassword
})
logger := slog.New(handler)
logger.Info("Safe log", "user", "alice") // 출력됨
logger.Info("Unsafe log", "password", "secret") // 필터링됨
}
5. 샘플링 Handler
type SamplingHandler struct {
slog.Handler
rate int
counter atomic.Int64
}
func NewSamplingHandler(h slog.Handler, rate int) *SamplingHandler {
return &SamplingHandler{
Handler: h,
rate: rate,
}
}
func (h *SamplingHandler) Handle(ctx context.Context, r slog.Record) error {
count := h.counter.Add(1)
if count%int64(h.rate) != 0 {
return nil // 샘플링
}
return h.Handler.Handle(ctx, r)
}
func main() {
baseHandler := slog.NewJSONHandler(os.Stdout, nil)
// 10개 중 1개만 기록
handler := NewSamplingHandler(baseHandler, 10)
logger := slog.New(handler)
for i := 0; i < 100; i++ {
logger.Info("High frequency log", "iteration", i)
}
// 약 10개의 로그만 출력됨
}
실전 예제
1. HTTP 미들웨어
func LoggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 요청 정보 로깅
logger.Info("Request started",
"method", r.Method,
"path", r.URL.Path,
"remote_addr", r.RemoteAddr,
)
// Response Writer 래핑
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
// 다음 핸들러 실행
next.ServeHTTP(wrapped, r)
// 응답 정보 로깅
duration := time.Since(start)
logFunc := logger.Info
if wrapped.statusCode >= 500 {
logFunc = logger.Error
} else if wrapped.statusCode >= 400 {
logFunc = logger.Warn
}
logFunc("Request completed",
"method", r.Method,
"path", r.URL.Path,
"status", wrapped.statusCode,
"duration_ms", duration.Milliseconds(),
"bytes", wrapped.bytesWritten,
)
})
}
}
type responseWriter struct {
http.ResponseWriter
statusCode int
bytesWritten int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.bytesWritten += n
return n, err
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
})
handler := LoggingMiddleware(logger)(mux)
http.ListenAndServe(":8080", handler)
}
2. 에러 로깅
type AppError struct {
Code string
Message string
Err error
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
func (e *AppError) LogValue() slog.Value {
attrs := []slog.Attr{
slog.String("code", e.Code),
slog.String("message", e.Message),
}
if e.Err != nil {
attrs = append(attrs, slog.String("cause", e.Err.Error()))
}
return slog.GroupValue(attrs...)
}
func processData(logger *slog.Logger) error {
err := someOperation()
if err != nil {
appErr := &AppError{
Code: "DATA_PROCESSING_ERROR",
Message: "Failed to process data",
Err: err,
}
logger.Error("Operation failed",
"error", appErr,
"operation", "processData",
)
return appErr
}
return nil
}
func someOperation() error {
return fmt.Errorf("database connection timeout")
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
if err := processData(logger); err != nil {
os.Exit(1)
}
}
3. 구조화된 애플리케이션 로거
type App struct {
logger *slog.Logger
}
func NewApp() *App {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger = logger.With(
"service", "my-app",
"version", "1.0.0",
"environment", os.Getenv("ENV"),
)
return &App{logger: logger}
}
func (a *App) Start() {
a.logger.Info("Application starting")
// HTTP 서버 로거
httpLogger := a.logger.WithGroup("http")
a.startHTTPServer(httpLogger)
// Database 로거
dbLogger := a.logger.WithGroup("database")
a.connectDatabase(dbLogger)
a.logger.Info("Application started successfully")
}
func (a *App) startHTTPServer(logger *slog.Logger) {
logger.Info("Starting HTTP server",
"host", "0.0.0.0",
"port", 8080,
)
}
func (a *App) connectDatabase(logger *slog.Logger) {
logger.Info("Connecting to database",
"host", "localhost",
"port", 5432,
"database", "mydb",
)
}
func main() {
app := NewApp()
app.Start()
}
4. 메트릭과 함께 사용
type Metrics struct {
logger *slog.Logger
requestCount atomic.Int64
errorCount atomic.Int64
totalDuration atomic.Int64
}
func NewMetrics(logger *slog.Logger) *Metrics {
m := &Metrics{logger: logger}
// 주기적으로 메트릭 로깅
go m.logMetricsPeriodically()
return m
}
func (m *Metrics) RecordRequest(duration time.Duration, err error) {
m.requestCount.Add(1)
m.totalDuration.Add(duration.Milliseconds())
if err != nil {
m.errorCount.Add(1)
}
}
func (m *Metrics) logMetricsPeriodically() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
requests := m.requestCount.Load()
errors := m.errorCount.Load()
totalDuration := m.totalDuration.Load()
var avgDuration int64
if requests > 0 {
avgDuration = totalDuration / requests
}
m.logger.Info("Metrics snapshot",
"requests", requests,
"errors", errors,
"error_rate", float64(errors)/float64(max(requests, 1)),
"avg_duration_ms", avgDuration,
)
}
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
metrics := NewMetrics(logger)
// 요청 시뮬레이션
for i := 0; i < 100; i++ {
start := time.Now()
var err error
if i%10 == 0 {
err = fmt.Errorf("simulated error")
}
time.Sleep(time.Millisecond * time.Duration(rand.Intn(100)))
metrics.RecordRequest(time.Since(start), err)
}
time.Sleep(15 * time.Second)
}
5. 요청 추적
func generateRequestID() string {
b := make([]byte, 16)
rand.Read(b)
return fmt.Sprintf("%x", b)
}
func TraceMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Request ID 생성
requestID := generateRequestID()
// Context에 Request ID 추가
ctx := context.WithValue(r.Context(), "request_id", requestID)
// Logger에 Request ID 추가
reqLogger := logger.With("request_id", requestID)
// Context에 Logger 저장
ctx = context.WithValue(ctx, "logger", reqLogger)
reqLogger.Info("Request received",
"method", r.Method,
"path", r.URL.Path,
)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
// Context에서 Logger 가져오기
logger := r.Context().Value("logger").(*slog.Logger)
logger.Info("Processing request")
// 비즈니스 로직
time.Sleep(100 * time.Millisecond)
logger.Info("Request processed successfully")
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
mux := http.NewServeMux()
mux.HandleFunc("/api/data", handleRequest)
handler := TraceMiddleware(logger)(mux)
http.ListenAndServe(":8080", handler)
}
6. 로그 집계
type LogAggregator struct {
logger *slog.Logger
buffer []slog.Record
mu sync.Mutex
flushSize int
}
func NewLogAggregator(logger *slog.Logger, flushSize int) *LogAggregator {
return &LogAggregator{
logger: logger,
buffer: make([]slog.Record, 0, flushSize),
flushSize: flushSize,
}
}
func (la *LogAggregator) Log(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) {
la.mu.Lock()
defer la.mu.Unlock()
r := slog.NewRecord(time.Now(), level, msg, 0)
r.AddAttrs(attrs...)
la.buffer = append(la.buffer, r)
if len(la.buffer) >= la.flushSize {
la.flush()
}
}
func (la *LogAggregator) flush() {
for _, r := range la.buffer {
la.logger.Handler().Handle(context.Background(), r)
}
la.buffer = la.buffer[:0]
}
func (la *LogAggregator) Flush() {
la.mu.Lock()
defer la.mu.Unlock()
la.flush()
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
aggregator := NewLogAggregator(logger, 10)
// 많은 로그 생성
for i := 0; i < 25; i++ {
aggregator.Log(context.Background(), slog.LevelInfo, "Event occurred",
slog.Int("event_id", i),
)
}
// 남은 로그 플러시
aggregator.Flush()
}
일반적인 실수
1. 짝이 맞지 않는 키-값
// ❌ 나쁜 예 (값 누락)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("User action",
"user_id", 123,
"action", // 값이 없음!
)
// 출력: {"time":"...","level":"INFO","msg":"User action","user_id":123,"action":"!BADKEY"}
}
// ✅ 좋은 예
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("User action",
"user_id", 123,
"action", "login",
)
}
2. 민감 정보 로깅
// ❌ 나쁜 예
func login(logger *slog.Logger, username, password string) {
logger.Info("User login attempt",
"username", username,
"password", password, // 비밀번호 노출!
)
}
// ✅ 좋은 예
func login(logger *slog.Logger, username, password string) {
logger.Info("User login attempt",
"username", username,
// 비밀번호는 로그하지 않음
)
}
// ✅ 또는 ReplaceAttr 사용
3. 비효율적인 로깅
// ❌ 나쁜 예 (불필요한 문자열 생성)
func processItems(logger *slog.Logger, items []Item) {
logger.Debug("Processing items: " + fmt.Sprintf("%+v", items))
// DEBUG가 비활성화되어도 fmt.Sprintf 실행됨
}
// ✅ 좋은 예
func processItems(logger *slog.Logger, items []Item) {
if logger.Enabled(context.Background(), slog.LevelDebug) {
logger.Debug("Processing items", "items", items)
}
}
4. Logger를 값으로 전달
// ❌ 나쁜 예
func processData(logger slog.Logger) { // 값 복사
logger.Info("Processing")
}
// ✅ 좋은 예
func processData(logger *slog.Logger) { // 포인터
logger.Info("Processing")
}
5. 전역 Logger 과다 사용
// ❌ 나쁜 예
var globalLogger *slog.Logger
func init() {
globalLogger = slog.New(slog.NewJSONHandler(os.Stdout, nil))
}
func processRequest() {
globalLogger.Info("Processing") // 테스트하기 어려움
}
// ✅ 좋은 예
type Processor struct {
logger *slog.Logger
}
func NewProcessor(logger *slog.Logger) *Processor {
return &Processor{logger: logger}
}
func (p *Processor) ProcessRequest() {
p.logger.Info("Processing") // 의존성 주입
}
6. Context 무시
// ❌ 나쁜 예
func handleRequest(ctx context.Context, logger *slog.Logger) {
logger.Info("Handling request") // Context 무시
}
// ✅ 좋은 예
func handleRequest(ctx context.Context, logger *slog.Logger) {
logger.InfoContext(ctx, "Handling request") // Context 전달
}
7. 레벨 혼동
// ❌ 나쁜 예
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// 이건 디버그 정보인데 Info로 로깅
logger.Info("Variable value", "x", 123)
// 이건 중요한 에러인데 Warn으로 로깅
logger.Warn("Database connection failed")
}
// ✅ 좋은 예
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Debug("Variable value", "x", 123)
logger.Error("Database connection failed")
}
베스트 프랙티스
1. 구조화된 로깅
// ✅ 항상 구조화된 로그 사용
logger.Info("User registered",
"user_id", user.ID,
"username", user.Name,
"email", user.Email,
"registration_date", time.Now(),
)
// ❌ 문자열 연결 사용하지 않기
logger.Info(fmt.Sprintf("User %s registered with ID %d", user.Name, user.ID))
2. 일관된 키 이름
// ✅ 일관된 명명 규칙
const (
KeyUserID = "user_id"
KeyRequestID = "request_id"
KeyDurationMS = "duration_ms"
KeyError = "error"
)
logger.Info("Request completed",
KeyRequestID, "req-123",
KeyUserID, 456,
KeyDurationMS, 150,
)
3. 적절한 레벨 사용
// Debug: 개발/디버깅 정보
logger.Debug("Cache lookup", "key", key, "found", found)
// Info: 일반적인 정보
logger.Info("Server started", "port", 8080)
// Warn: 주의가 필요한 상황
logger.Warn("High memory usage", "percent", 85)
// Error: 에러 상황
logger.Error("Database query failed", "error", err, "query", sql)
4. Context 활용
func (s *Service) ProcessRequest(ctx context.Context, req Request) error {
logger := s.logger.With(
"request_id", req.ID,
"user_id", req.UserID,
)
logger.InfoContext(ctx, "Processing request")
// 비즈니스 로직
logger.InfoContext(ctx, "Request processed successfully")
return nil
}
5. 에러 로깅
// ✅ 에러 정보를 풍부하게
func processData(logger *slog.Logger) error {
err := someOperation()
if err != nil {
logger.Error("Operation failed",
"error", err,
"operation", "processData",
"retry_count", retryCount,
slog.Group("context",
"timestamp", time.Now(),
"environment", os.Getenv("ENV"),
),
)
return err
}
return nil
}
6. 성능 고려
// ✅ 고빈도 로깅에서는 Enabled 체크
func processBatch(logger *slog.Logger, items []Item) {
debugEnabled := logger.Enabled(context.Background(), slog.LevelDebug)
for _, item := range items {
// 비즈니스 로직
if debugEnabled {
logger.Debug("Item processed",
"item_id", item.ID,
"details", item.String(), // 비싼 연산
)
}
}
}
7. 테스트 가능한 로깅
// ✅ 의존성 주입으로 테스트 용이
type Service struct {
logger *slog.Logger
db *Database
}
func NewService(logger *slog.Logger, db *Database) *Service {
return &Service{
logger: logger,
db: db,
}
}
// 테스트
func TestService(t *testing.T) {
// 테스트용 Logger
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, nil))
service := NewService(logger, mockDB)
service.Process()
// 로그 검증
if !strings.Contains(buf.String(), "Processing") {
t.Error("Expected log not found")
}
}
8. 그룹화와 네임스페이스
// ✅ 관련 정보 그룹화
func (s *Service) LogRequestMetrics(r *http.Request, duration time.Duration, status int) {
s.logger.Info("Request completed",
slog.Group("request",
"method", r.Method,
"path", r.URL.Path,
"remote_addr", r.RemoteAddr,
),
slog.Group("response",
"status", status,
"duration_ms", duration.Milliseconds(),
),
)
}
정리
- 기본: Debug, Info, Warn, Error 레벨, 키-값 쌍
- Handler: TextHandler, JSONHandler, 커스텀 핸들러
- Options: Level, AddSource, ReplaceAttr (마스킹)
- Logger: With, WithGroup, 중첩 그룹
- Context: InfoContext, 추적 ID 전파
- 성능: Enabled 체크, LogAttrs, 조건부 로깅
- 커스텀: Handler 인터페이스, 색상, 필터, 샘플링
- 실전: HTTP 미들웨어, 에러, 메트릭, 추적, 집계
- 실수: 짝 불일치, 민감 정보, 비효율, 값 전달, 전역, Context 무시
- 베스트: 구조화, 일관성, 적절한 레벨, Context, 에러, 성능, 테스트, 그룹화