본문으로 건너뛰기

로깅 (Logging)

Fluent Bit DaemonSet을 사용하여 모든 컨테이너 로그를 수집하고 CloudWatch Logs로 전송합니다. 구조화된 JSON 로그 포맷과 트레이스 ID 연동을 통해 효율적인 로그 분석이 가능합니다.

아키텍처

Fluent Bit 구성

DaemonSet 배포

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
namespace: amazon-cloudwatch
spec:
template:
spec:
serviceAccountName: fluent-bit # IRSA
tolerations:
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
- key: node-pool
operator: Exists
effect: NoSchedule
containers:
- name: fluent-bit
image: public.ecr.aws/aws-observability/aws-for-fluent-bit:stable
env:
- name: AWS_REGION
valueFrom:
configMapKeyRef:
name: cluster-info
key: region
- name: CLUSTER_NAME
valueFrom:
configMapKeyRef:
name: cluster-info
key: cluster-name
- name: HOST_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true

Fluent Bit 설정

[SERVICE]
Flush 5
Log_Level info
Daemon off
Parsers_File parsers.conf
HTTP_Server On
HTTP_Listen 0.0.0.0
HTTP_Port 2020

[INPUT]
Name tail
Tag kube.*
Path /var/log/containers/*.log
Parser docker
DB /var/log/flb_kube.db
Mem_Buf_Limit 50MB
Skip_Long_Lines On
Refresh_Interval 10

[FILTER]
Name kubernetes
Match kube.*
Kube_URL https://kubernetes.default.svc:443
Kube_CA_File /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Kube_Token_File /var/run/secrets/kubernetes.io/serviceaccount/token
Merge_Log On
Merge_Log_Key log_processed
K8S-Logging.Parser On
K8S-Logging.Exclude Off
Labels On
Annotations Off

[FILTER]
Name modify
Match kube.*
Add cluster ${CLUSTER_NAME}
Add region ${AWS_REGION}

[OUTPUT]
Name cloudwatch_logs
Match kube.*
region ${AWS_REGION}
log_group_name /eks/${CLUSTER_NAME}/containers
log_stream_prefix ${HOST_NAME}-
auto_create_group true
log_format json/emf
retry_limit 2

표준 로그 포맷

모든 서비스는 다음 JSON 포맷으로 로그를 출력해야 합니다:

{
"timestamp": "2026-03-15T10:30:45.123Z",
"level": "INFO",
"service": "order-service",
"region": "us-east-1",
"trace_id": "0af7651916cd43dd8448eb211c80319c",
"span_id": "b7ad6b7169203331",
"message": "주문 생성 완료",
"order_id": "ORD-123456",
"user_id": "a0000001-0000-0000-0000-000000000001",
"amount": 159000,
"duration_ms": 45
}

필수 필드

필드타입설명
timestampstring (ISO 8601)로그 발생 시간
levelstringDEBUG, INFO, WARN, ERROR, FATAL
servicestring서비스 이름
regionstringAWS 리전
messagestring로그 메시지

권장 필드

필드타입설명
trace_idstringOpenTelemetry trace ID
span_idstringOpenTelemetry span ID
user_idstring사용자 ID
request_idstring요청 ID
duration_msnumber처리 시간 (밀리초)

언어별 로깅 구현

Go (zerolog)

import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/trace"
)

func init() {
zerolog.TimeFieldFormat = time.RFC3339Nano

log.Logger = zerolog.New(os.Stdout).With().
Str("service", "order-service").
Str("region", os.Getenv("AWS_REGION")).
Timestamp().
Logger()
}

// 트레이스 컨텍스트 포함 로깅
func LogWithTrace(ctx context.Context) zerolog.Logger {
span := trace.SpanFromContext(ctx)
if span.SpanContext().IsValid() {
return log.With().
Str("trace_id", span.SpanContext().TraceID().String()).
Str("span_id", span.SpanContext().SpanID().String()).
Logger()
}
return log.Logger
}

// 사용 예시
func CreateOrder(ctx context.Context, order *Order) error {
logger := LogWithTrace(ctx)

logger.Info().
Str("order_id", order.ID).
Str("user_id", order.UserID).
Int64("amount", order.Amount).
Msg("주문 생성 시작")

// 처리 로직...

logger.Info().
Str("order_id", order.ID).
Int64("duration_ms", elapsed.Milliseconds()).
Msg("주문 생성 완료")

return nil
}

Java (Logback + Logstash Encoder)

<!-- logback-spring.xml -->
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<customFields>
{"service":"payment-service","region":"${AWS_REGION:-unknown}"}
</customFields>
<provider class="net.logstash.logback.composite.loggingevent.LoggingEventPatternJsonProvider">
<pattern>
{
"trace_id": "%mdc{traceId}",
"span_id": "%mdc{spanId}"
}
</pattern>
</provider>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

@Service
public class PaymentService {
private static final Logger log = LoggerFactory.getLogger(PaymentService.class);

public PaymentResult processPayment(PaymentRequest request) {
// MDC에 추가 컨텍스트 설정
MDC.put("order_id", request.getOrderId());
MDC.put("user_id", request.getUserId());

try {
log.info("결제 처리 시작 - 금액: {}", request.getAmount());

// 결제 로직...

log.info("결제 처리 완료 - 트랜잭션: {}", result.getTransactionId());
return result;

} catch (Exception e) {
log.error("결제 처리 실패: {}", e.getMessage(), e);
throw e;
} finally {
MDC.clear();
}
}
}

Python (structlog)

import structlog
import os
from opentelemetry import trace

def configure_logging():
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.processors.JSONRenderer()
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)

def get_logger(name: str):
logger = structlog.get_logger(name)
return logger.bind(
service="recommendation-service",
region=os.getenv("AWS_REGION", "unknown")
)

# 트레이스 컨텍스트 추가
def log_with_trace(logger, **kwargs):
span = trace.get_current_span()
if span.get_span_context().is_valid:
kwargs["trace_id"] = format(span.get_span_context().trace_id, "032x")
kwargs["span_id"] = format(span.get_span_context().span_id, "016x")
return logger.bind(**kwargs)

# 사용 예시
logger = get_logger(__name__)

async def get_recommendations(user_id: str):
ctx_logger = log_with_trace(logger, user_id=user_id)

ctx_logger.info("추천 생성 시작")

# 추천 로직...

ctx_logger.info(
"추천 생성 완료",
item_count=len(recommendations),
duration_ms=elapsed_ms
)

return recommendations

로그 레벨 가이드라인

레벨용도예시
DEBUG개발/디버깅용 상세 정보변수 값, SQL 쿼리
INFO정상 동작 기록요청 시작/완료, 상태 변경
WARN잠재적 문제재시도, 폴백 사용
ERROR에러 발생 (복구 가능)API 호출 실패, 유효성 검사 실패
FATAL심각한 에러 (복구 불가)서비스 시작 실패

CloudWatch Logs 구조

로그 그룹

Terraform으로 관리되는 로그 그룹:

# 네임스페이스별 로그 그룹
/eks/multi-region-mall/core-services
/eks/multi-region-mall/user-services
/eks/multi-region-mall/fulfillment
/eks/multi-region-mall/business-services
/eks/multi-region-mall/platform

# 보관 기간: 90일

로그 스트림

{node-name}-{namespace}_{pod-name}_{container-name}-{container-id}

예시:

ip-10-0-1-123-core-services_order-service-abc123_order-service-def456

CloudWatch Logs Insights 쿼리

에러 로그 검색

fields @timestamp, @message, service, trace_id, error
| filter level = "ERROR"
| sort @timestamp desc
| limit 100

특정 주문 추적

fields @timestamp, @message, service, level
| filter order_id = "ORD-123456"
| sort @timestamp asc

느린 요청 분석

fields @timestamp, service, message, duration_ms
| filter duration_ms > 1000
| stats avg(duration_ms) as avg_duration,
max(duration_ms) as max_duration,
count(*) as request_count
by service
| sort avg_duration desc

트레이스 ID로 로그 연결

fields @timestamp, @message, service, level, span_id
| filter trace_id = "0af7651916cd43dd8448eb211c80319c"
| sort @timestamp asc

서비스별 에러율

fields service, level
| stats count(*) as total,
sum(case when level = "ERROR" then 1 else 0 end) as errors
by service
| display service, errors, total, (errors * 100.0 / total) as error_rate
| sort error_rate desc

사용자별 활동

fields @timestamp, service, message, user_id
| filter user_id = "a0000001-0000-0000-0000-000000000001"
| sort @timestamp desc
| limit 50

로그와 트레이스 연결

Grafana에서 로그와 트레이스를 연결하여 조회할 수 있습니다:

Grafana 데이터 소스 연결 설정

# Tempo 데이터 소스
jsonData:
tracesToLogsV2:
datasourceUid: cloudwatch
filterByTraceID: true
filterBySpanID: true
customQuery: true
query: |
fields @timestamp, @message, service, level
| filter trace_id = "${__trace.traceId}"
| sort @timestamp asc

민감 정보 마스킹

로그에 민감 정보가 포함되지 않도록 마스킹합니다:

Go

func maskSensitiveData(data string) string {
// 신용카드 번호 마스킹
cardRegex := regexp.MustCompile(`\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}`)
data = cardRegex.ReplaceAllString(data, "****-****-****-****")

// 이메일 마스킹
emailRegex := regexp.MustCompile(`[\w.-]+@[\w.-]+\.\w+`)
data = emailRegex.ReplaceAllStringFunc(data, func(email string) string {
parts := strings.Split(email, "@")
return parts[0][:2] + "***@" + parts[1]
})

return data
}

Python

import re

class SensitiveDataFilter:
PATTERNS = [
(re.compile(r'\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}'), '****-****-****-****'),
(re.compile(r'[\w.-]+@[\w.-]+\.\w+'), lambda m: m.group()[:2] + '***@***'),
(re.compile(r'password["\s:=]+["\']?\w+["\']?', re.I), 'password=***'),
]

@classmethod
def filter(cls, message: str) -> str:
for pattern, replacement in cls.PATTERNS:
if callable(replacement):
message = pattern.sub(replacement, message)
else:
message = pattern.sub(replacement, message)
return message

트러블슈팅

로그가 수집되지 않을 때

# 1. Fluent Bit 상태 확인
kubectl get pods -n amazon-cloudwatch -l app=fluent-bit

# 2. Fluent Bit 로그 확인
kubectl logs -n amazon-cloudwatch -l app=fluent-bit --tail=100

# 3. CloudWatch 로그 그룹 확인
aws logs describe-log-groups --log-group-name-prefix "/eks/multi-region-mall"

# 4. 컨테이너 로그 직접 확인
kubectl logs <pod-name> -n <namespace> --tail=50

로그 포맷 검증

# Pod 로그가 JSON 형식인지 확인
kubectl logs <pod-name> | head -5 | jq .

# 필수 필드 확인
kubectl logs <pod-name> | head -1 | jq 'has("timestamp", "level", "service", "message")'

관련 문서