Skip to content

필사 모드: Observability & 모니터링 완전 가이드 2025: 로깅, 메트릭, 트레이싱, 알림 전략

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

TOC

1. 모니터링 vs Observability

1.1 모니터링의 한계

전통적인 모니터링은 **알려진 문제(known unknowns)**를 감지하는 데 초점을 맞춥니다. CPU 사용률이 90%를 넘으면 알림, 디스크 사용량이 80%를 넘으면 알림 -- 이런 임계값 기반 접근법입니다.

하지만 현대의 분산 시스템에서는 이것만으로 부족합니다:

- 마이크로서비스 간 복잡한 상호작용

- 일시적(transient) 오류의 빈번한 발생

- 예측하지 못한 문제(unknown unknowns)의 증가

- 단일 메트릭으로 설명할 수 없는 성능 저하

1.2 Observability란

Observability는 시스템의 **외부 출력(external outputs)**을 통해 **내부 상태(internal state)**를 이해할 수 있는 능력입니다.

핵심 차이:

- **모니터링**: "무엇이 고장났는가?" (What is broken?)

- **Observability**: "왜 고장났는가?" (Why is it broken?)

Observability의 세 가지 축(Three Pillars):

1. **Logs** - 이산 이벤트의 기록

2. **Metrics** - 시간에 따른 수치 측정

3. **Traces** - 분산 시스템에서의 요청 흐름

2. OpenTelemetry (OTel) 핵심 이해

2.1 OpenTelemetry란

OpenTelemetry는 CNCF(Cloud Native Computing Foundation)에서 관리하는 **벤더 중립적** 텔레메트리 수집 프레임워크입니다. 로그, 메트릭, 트레이스를 하나의 표준으로 통합합니다.

구성 요소:

- **API**: 계측(instrumentation)을 위한 인터페이스

- **SDK**: API의 구현체

- **Collector**: 텔레메트리 데이터 수집, 처리, 내보내기

- **자동 계측(Auto-instrumentation)**: 코드 수정 없이 텔레메트리 수집

2.2 OTel SDK 설정

// Node.js OpenTelemetry 설정

ATTR_SERVICE_NAME,

ATTR_SERVICE_VERSION,

} from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({

resource: new Resource({

[ATTR_SERVICE_NAME]: 'order-service',

[ATTR_SERVICE_VERSION]: '1.2.0',

environment: process.env.NODE_ENV || 'production',

}),

traceExporter: new OTLPTraceExporter({

url: 'http://otel-collector:4318/v1/traces',

}),

metricReader: new PeriodicExportingMetricReader({

exporter: new OTLPMetricExporter({

url: 'http://otel-collector:4318/v1/metrics',

}),

exportIntervalMillis: 30000,

}),

instrumentations: [

getNodeAutoInstrumentations({

// HTTP, Express, pg, Redis 등 자동 계측

'@opentelemetry/instrumentation-http': {

ignoreIncomingRequestHook: (req) => {

// 헬스체크 경로 제외

return req.url === '/health';

},

},

'@opentelemetry/instrumentation-express': {

enabled: true,

},

}),

],

});

sdk.start();

// 정상 종료 시 텔레메트리 플러시

process.on('SIGTERM', () => {

sdk.shutdown().then(() => process.exit(0));

});

2.3 수동 Span 생성

const tracer = trace.getTracer('order-service');

async function processOrder(orderData) {

// 수동 Span 생성

return tracer.startActiveSpan('processOrder', async (span) => {

try {

// Span에 속성 추가

span.setAttribute('order.id', orderData.id);

span.setAttribute('order.total', orderData.total);

span.setAttribute('order.items_count', orderData.items.length);

// 하위 Span: 재고 확인

const inventory = await tracer.startActiveSpan(

'checkInventory',

async (childSpan) => {

childSpan.setAttribute('inventory.warehouse', 'us-east-1');

const result = await inventoryService.check(orderData.items);

childSpan.end();

return result;

}

);

if (!inventory.available) {

span.setStatus({

code: SpanStatusCode.ERROR,

message: 'Insufficient inventory',

});

throw new Error('Insufficient inventory');

}

// 하위 Span: 결제 처리

const payment = await tracer.startActiveSpan(

'processPayment',

async (childSpan) => {

childSpan.setAttribute('payment.method', orderData.paymentMethod);

childSpan.setAttribute('payment.amount', orderData.total);

const result = await paymentService.charge(orderData);

childSpan.end();

return result;

}

);

// Span 이벤트 추가

span.addEvent('order_completed', {

'order.id': orderData.id,

'payment.transaction_id': payment.transactionId,

});

span.setStatus({ code: SpanStatusCode.OK });

return { orderId: orderData.id, transactionId: payment.transactionId };

} catch (error) {

span.setStatus({

code: SpanStatusCode.ERROR,

message: error.message,

});

span.recordException(error);

throw error;

} finally {

span.end();

}

});

}

2.4 OTel Collector 설정

otel-collector-config.yaml

receivers:

otlp:

protocols:

grpc:

endpoint: 0.0.0.0:4317

http:

endpoint: 0.0.0.0:4318

processors:

batch:

send_batch_size: 1024

timeout: 5s

memory_limiter:

check_interval: 1s

limit_mib: 512

spike_limit_mib: 128

attributes:

actions:

- key: environment

value: production

action: upsert

테일 샘플링 (에러 트레이스 우선 수집)

tail_sampling:

decision_wait: 10s

policies:

- name: error-policy

type: status_code

status_code:

status_codes: [ERROR]

- name: slow-policy

type: latency

latency:

threshold_ms: 1000

- name: probabilistic-policy

type: probabilistic

probabilistic:

sampling_percentage: 10

exporters:

otlp/jaeger:

endpoint: jaeger:4317

tls:

insecure: true

prometheus:

endpoint: 0.0.0.0:8889

loki:

endpoint: http://loki:3100/loki/api/v1/push

service:

pipelines:

traces:

receivers: [otlp]

processors: [memory_limiter, batch, tail_sampling]

exporters: [otlp/jaeger]

metrics:

receivers: [otlp]

processors: [memory_limiter, batch]

exporters: [prometheus]

logs:

receivers: [otlp]

processors: [memory_limiter, batch]

exporters: [loki]

3. 로깅 (Logging)

3.1 구조화된 로깅 (Structured Logging)

// pino를 활용한 구조화된 로깅

const logger = pino({

level: process.env.LOG_LEVEL || 'info',

// JSON 포맷 (기본)

formatters: {

level(label) {

return { level: label };

},

bindings(bindings) {

return {

service: 'order-service',

version: '1.2.0',

host: bindings.hostname,

pid: bindings.pid,

};

},

},

// 타임스탬프 포맷

timestamp: pino.stdTimeFunctions.isoTime,

// 민감 정보 제거

redact: ['req.headers.authorization', 'req.headers.cookie', '*.password'],

});

// Correlation ID 미들웨어

function correlationMiddleware(req, res, next) {

const correlationId = req.headers['x-correlation-id'] || generateId();

req.correlationId = correlationId;

res.setHeader('x-correlation-id', correlationId);

// 요청별 child logger

req.log = logger.child({

correlationId,

requestId: generateId(),

method: req.method,

path: req.path,

userAgent: req.headers['user-agent'],

});

next();

}

// 사용 예시

app.post('/api/orders', correlationMiddleware, async (req, res) => {

req.log.info({ body: req.body }, 'Order creation started');

try {

const order = await createOrder(req.body);

req.log.info(

{ orderId: order.id, duration: Date.now() - req.startTime },

'Order created successfully'

);

res.json(order);

} catch (error) {

req.log.error(

{ err: error, body: req.body },

'Order creation failed'

);

res.status(500).json({ error: 'Internal server error' });

}

});

3.2 로그 레벨 전략

| 레벨 | 용도 | 예시 |

|------|------|------|

| FATAL | 시스템 중단 | 데이터베이스 연결 완전 실패 |

| ERROR | 오류 발생 | 결제 처리 실패 |

| WARN | 잠재적 문제 | 재시도 성공, 캐시 미스 |

| INFO | 주요 비즈니스 이벤트 | 주문 생성, 사용자 로그인 |

| DEBUG | 디버깅 정보 | SQL 쿼리, API 요청/응답 |

| TRACE | 상세 추적 | 함수 진입/종료, 변수 값 |

// 환경별 로그 레벨 설정

const logLevels = {

production: 'info',

staging: 'debug',

development: 'trace',

};

3.3 ELK Stack (Elasticsearch + Logstash + Kibana)

docker-compose.yml - ELK Stack

version: '3.8'

services:

elasticsearch:

image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0

environment:

- discovery.type=single-node

- xpack.security.enabled=false

- "ES_JAVA_OPTS=-Xms512m -Xmx512m"

ports:

- "9200:9200"

volumes:

- es-data:/usr/share/elasticsearch/data

logstash:

image: docker.elastic.co/logstash/logstash:8.12.0

volumes:

- ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf

depends_on:

- elasticsearch

kibana:

image: docker.elastic.co/kibana/kibana:8.12.0

ports:

- "5601:5601"

environment:

- ELASTICSEARCH_HOSTS=http://elasticsearch:9200

depends_on:

- elasticsearch

volumes:

es-data:

logstash.conf

input {

tcp {

port => 5044

codec => json

}

}

filter {

타임스탬프 파싱

date {

match => [ "timestamp", "ISO8601" ]

target => "@timestamp"

}

에러 스택 트레이스 파싱

if [level] == "error" {

grok {

match => {

"stack" => "%{GREEDYDATA:error_class}: %{GREEDYDATA:error_message}"

}

}

}

지역 정보 추가

if [client_ip] {

geoip {

source => "client_ip"

}

}

}

output {

elasticsearch {

hosts => ["elasticsearch:9200"]

index => "logs-%{+YYYY.MM.dd}"

}

}

3.4 Grafana Loki (경량 로그 수집)

Loki 설정

auth_enabled: false

server:

http_listen_port: 3100

common:

ring:

kvstore:

store: inmemory

replication_factor: 1

schema_config:

configs:

- from: 2024-01-01

store: tsdb

object_store: filesystem

schema: v13

index:

prefix: index_

period: 24h

storage_config:

filesystem:

directory: /loki/chunks

limits_config:

retention_period: 30d

// Loki에 직접 로그 전송 (winston-loki)

const logger = winston.createLogger({

transports: [

new LokiTransport({

host: 'http://loki:3100',

labels: {

service: 'order-service',

environment: 'production',

},

json: true,

batching: true,

interval: 5,

}),

],

});

4. 메트릭 (Metrics)

4.1 Prometheus 기본

Prometheus는 Pull 기반의 시계열 데이터베이스입니다.

메트릭 타입:

- **Counter**: 단조 증가 값 (요청 수, 에러 수)

- **Gauge**: 증감 가능한 값 (현재 연결 수, 메모리 사용량)

- **Histogram**: 값의 분포 (응답 시간, 페이로드 크기)

- **Summary**: 클라이언트 측 분위수 계산

// Node.js Prometheus 클라이언트

const register = new Registry();

// 기본 메트릭 수집 (CPU, 메모리, 이벤트 루프 등)

collectDefaultMetrics({ register });

// Counter: HTTP 요청 수

const httpRequestsTotal = new Counter({

name: 'http_requests_total',

help: 'Total number of HTTP requests',

labelNames: ['method', 'path', 'status'],

registers: [register],

});

// Gauge: 현재 활성 연결 수

const activeConnections = new Gauge({

name: 'active_connections',

help: 'Number of active connections',

registers: [register],

});

// Histogram: 요청 응답 시간

const httpRequestDuration = new Histogram({

name: 'http_request_duration_seconds',

help: 'HTTP request duration in seconds',

labelNames: ['method', 'path', 'status'],

buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],

registers: [register],

});

// Summary: DB 쿼리 시간

const dbQueryDuration = new Summary({

name: 'db_query_duration_seconds',

help: 'Database query duration',

labelNames: ['operation', 'table'],

percentiles: [0.5, 0.9, 0.95, 0.99],

registers: [register],

});

// Express 미들웨어

function metricsMiddleware(req, res, next) {

const start = process.hrtime.bigint();

activeConnections.inc();

res.on('finish', () => {

const duration = Number(process.hrtime.bigint() - start) / 1e9;

const labels = {

method: req.method,

path: req.route?.path || req.path,

status: res.statusCode.toString(),

};

httpRequestsTotal.inc(labels);

httpRequestDuration.observe(labels, duration);

activeConnections.dec();

});

next();

}

// 메트릭 엔드포인트

app.get('/metrics', async (req, res) => {

res.set('Content-Type', register.contentType);

res.end(await register.metrics());

});

4.2 PromQL 핵심 쿼리

초당 요청 수 (RPS)

rate(http_requests_total[5m])

서비스별 에러율

sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)

/

sum(rate(http_requests_total[5m])) by (service)

p99 응답 시간

histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))

p95 응답 시간 (경로별)

histogram_quantile(0.95,

sum(rate(http_request_duration_seconds_bucket[5m])) by (le, path)

)

메모리 사용량 증가율

deriv(process_resident_memory_bytes[1h])

가동 시간

time() - process_start_time_seconds

Apdex 스코어 (목표 응답시간 0.5초)

(

sum(rate(http_request_duration_seconds_bucket{le="0.5"}[5m]))

+

sum(rate(http_request_duration_seconds_bucket{le="2.0"}[5m]))

) / 2

/

sum(rate(http_request_duration_seconds_count[5m]))

4.3 Recording Rules

prometheus-rules.yml

groups:

- name: request_rates

interval: 30s

rules:

사전 계산된 요청률

- record: service:http_requests:rate5m

expr: sum(rate(http_requests_total[5m])) by (service)

사전 계산된 에러율

- record: service:http_errors:rate5m

expr: |

sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)

/

sum(rate(http_requests_total[5m])) by (service)

사전 계산된 지연시간 분위수

- record: service:http_latency:p99

expr: |

histogram_quantile(0.99,

sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)

)

5. 시각화 (Grafana)

5.1 RED Method 대시보드

RED Method는 서비스의 핵심 성능 지표를 모니터링하는 방법론입니다:

- **R**ate: 초당 요청 수

- **E**rrors: 에러율

- **D**uration: 응답 시간

{

"panels": [

{

"title": "Request Rate (RPS)",

"type": "timeseries",

"targets": [

{

"expr": "sum(rate(http_requests_total[5m])) by (service)",

"legendFormat": "service"

}

]

},

{

"title": "Error Rate (%)",

"type": "timeseries",

"targets": [

{

"expr": "100 * sum(rate(http_requests_total{status=~\"5..\"}[5m])) by (service) / sum(rate(http_requests_total[5m])) by (service)",

"legendFormat": "service"

}

]

},

{

"title": "Response Time (p99)",

"type": "timeseries",

"targets": [

{

"expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service))",

"legendFormat": "service"

}

]

}

]

}

5.2 USE Method (인프라 모니터링)

USE Method는 인프라 리소스를 모니터링하는 방법론입니다:

- **U**tilization: 리소스 사용률

- **S**aturation: 대기열 길이

- **E**rrors: 에러 수

CPU Utilization

100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

Memory Utilization

(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

Disk I/O Saturation

rate(node_disk_io_time_weighted_seconds_total[5m])

Network Errors

rate(node_network_receive_errs_total[5m])

+ rate(node_network_transmit_errs_total[5m])

6. 분산 트레이싱 (Distributed Tracing)

6.1 핵심 개념

- **Trace**: 하나의 요청이 시스템을 통과하는 전체 경로

- **Span**: Trace 내의 개별 작업 단위

- **Context Propagation**: 서비스 간 트레이스 컨텍스트 전달

- **Trace ID**: 전체 요청을 식별하는 고유 ID

- **Span ID**: 개별 작업을 식별하는 고유 ID

- **Parent Span ID**: 상위 작업의 Span ID

6.2 Context Propagation

// W3C Trace Context 전파

// 요청 헤더:

// traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01

// tracestate: congo=t61rcWkgMzE

// Express 미들웨어에서 Context 추출/주입

// HTTP 클라이언트에서 Context 주입

async function makeRequest(url, data) {

const headers = {};

// 현재 컨텍스트에서 trace 정보를 헤더에 주입

propagation.inject(context.active(), headers);

return fetch(url, {

method: 'POST',

headers: {

'Content-Type': 'application/json',

...headers, // traceparent, tracestate 포함

},

body: JSON.stringify(data),

});

}

// gRPC에서 Context 전파

// 자동 계측으로 gRPC 메타데이터에 Context 주입/추출

6.3 Jaeger 설정

docker-compose.yml - Jaeger

services:

jaeger:

image: jaegertracing/all-in-one:1.53

ports:

- "16686:16686" # UI

- "4317:4317" # OTLP gRPC

- "4318:4318" # OTLP HTTP

environment:

- COLLECTOR_OTLP_ENABLED=true

- SPAN_STORAGE_TYPE=elasticsearch

- ES_SERVER_URLS=http://elasticsearch:9200

6.4 샘플링 전략

// 샘플링 설정

// 비율 기반 샘플링 (10%만 수집)

const sampler = new TraceIdRatioBasedSampler(0.1);

// 부모 기반 샘플링 (부모가 샘플링되었으면 자식도 샘플링)

const parentBasedSampler = new ParentBasedSampler({

root: new TraceIdRatioBasedSampler(0.1),

});

샘플링 전략 비교:

| 전략 | 설명 | 장점 | 단점 |

|------|------|------|------|

| Head Sampling | 트레이스 시작 시 결정 | 단순, 낮은 오버헤드 | 에러 트레이스 누락 가능 |

| Tail Sampling | 트레이스 완료 후 결정 | 에러/느린 트레이스 보존 | 높은 메모리 사용 |

| Rate Limiting | 초당 수집량 제한 | 예측 가능한 비용 | 트래픽 급증 시 누락 |

| Probabilistic | 확률 기반 수집 | 균등한 표본 | 희귀 이벤트 누락 |

7. 알림 전략 (Alerting Strategy)

7.1 알림 피로 (Alert Fatigue) 방지

알림 피로는 너무 많은 알림으로 인해 중요한 알림을 무시하게 되는 현상입니다.

원칙:

1. **실행 가능한(actionable)** 알림만 보내기

2. **심각도(severity) 레벨** 구분

3. **적절한 라우팅** (누가, 언제 받을 것인가)

4. **알림 그룹화** (같은 문제의 알림 묶기)

5. **자동 해소** (문제 해결 시 자동으로 알림 종료)

7.2 심각도 레벨

Prometheus 알림 규칙

groups:

- name: service_alerts

rules:

Critical: 즉시 대응 필요

- alert: HighErrorRate

expr: |

sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)

/

sum(rate(http_requests_total[5m])) by (service)

> 0.05

for: 5m

labels:

severity: critical

annotations:

summary: "High error rate on service"

description: "Error rate is above 5% for 5 minutes"

runbook_url: "https://wiki.example.com/runbooks/high-error-rate"

Warning: 주의 필요

- alert: HighLatency

expr: |

histogram_quantile(0.99,

sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)

) > 2

for: 10m

labels:

severity: warning

annotations:

summary: "High latency on service"

description: "p99 latency is above 2 seconds for 10 minutes"

Info: 정보 제공

- alert: PodRestart

expr: |

increase(kube_pod_container_status_restarts_total[1h]) > 3

labels:

severity: info

annotations:

summary: "Pod restarting frequently"

7.3 Alertmanager 설정

alertmanager.yml

global:

resolve_timeout: 5m

slack_api_url: 'https://hooks.slack.com/services/xxx'

route:

group_by: ['alertname', 'service']

group_wait: 30s

group_interval: 5m

repeat_interval: 4h

receiver: 'default-slack'

routes:

Critical -> PagerDuty + Slack

- match:

severity: critical

receiver: 'pagerduty-critical'

repeat_interval: 1h

continue: true

- match:

severity: critical

receiver: 'slack-critical'

Warning -> Slack

- match:

severity: warning

receiver: 'slack-warning'

repeat_interval: 4h

Info -> Slack (업무 시간만)

- match:

severity: info

receiver: 'slack-info'

active_time_intervals:

- business-hours

receivers:

- name: 'default-slack'

slack_configs:

- channel: '#alerts-general'

- name: 'pagerduty-critical'

pagerduty_configs:

- service_key: 'YOUR_PAGERDUTY_KEY'

severity: critical

- name: 'slack-critical'

slack_configs:

- channel: '#alerts-critical'

color: 'danger'

- name: 'slack-warning'

slack_configs:

- channel: '#alerts-warning'

color: 'warning'

- name: 'slack-info'

slack_configs:

- channel: '#alerts-info'

time_intervals:

- name: business-hours

time_intervals:

- weekdays: ['monday:friday']

times:

- start_time: '09:00'

end_time: '18:00'

inhibit_rules:

Critical이 발생하면 같은 서비스의 Warning 억제

- source_match:

severity: critical

target_match:

severity: warning

equal: ['service']

7.4 Runbook 작성

Runbook: High Error Rate

알림 조건

- 에러율(5xx)이 5% 이상인 상태가 5분 이상 지속

즉시 확인 사항

1. 영향 범위 확인 (어떤 엔드포인트인지)

2. 최근 배포 이력 확인

3. 의존 서비스 상태 확인

대응 절차

1. Grafana 대시보드에서 에러 패턴 확인

2. 로그에서 에러 상세 내용 확인

3. 트레이스에서 실패 지점 식별

4. 최근 배포가 원인이면 롤백

5. 의존 서비스 문제이면 서킷 브레이커 확인

에스컬레이션

- 15분 내 해결 불가: 팀 리드에 보고

- 30분 내 해결 불가: 시니어 엔지니어 호출

8. SLO / SLI / SLA

8.1 용어 정의

- **SLI (Service Level Indicator)**: 측정 가능한 서비스 품질 지표

- 예: 성공률, 응답 시간, 가용성

- **SLO (Service Level Objective)**: SLI에 대한 목표값

- 예: 가용성 99.9%, p99 응답시간 200ms 이하

- **SLA (Service Level Agreement)**: 고객과의 계약

- SLO 위반 시 보상 조건 포함

8.2 Error Budget (에러 예산)

SLO: 99.9% 가용성

= 한 달(30일)에 허용되는 다운타임: 43.2분

= Error Budget: 0.1%

Error Budget 소진율:

- 전체의 50% 소진: 주의

- 전체의 75% 소진: 기능 릴리스 동결

- 전체의 100% 소진: 안정성 작업에만 집중

8.3 Burn Rate Alert

Burn Rate 기반 알림

groups:

- name: slo_alerts

rules:

빠른 소진 (1시간 내 2% 소진) - 즉시 대응

- alert: SLOBurnRateCritical

expr: |

(

sum(rate(http_requests_total{status=~"5.."}[1h]))

/

sum(rate(http_requests_total[1h]))

) > (14.4 * 0.001)

for: 2m

labels:

severity: critical

annotations:

summary: "SLO burn rate critical - error budget exhausting fast"

느린 소진 (6시간 내 5% 소진) - 주의

- alert: SLOBurnRateWarning

expr: |

(

sum(rate(http_requests_total{status=~"5.."}[6h]))

/

sum(rate(http_requests_total[6h]))

) > (6 * 0.001)

for: 15m

labels:

severity: warning

annotations:

summary: "SLO burn rate elevated"

9. APM 도구 비교

9.1 주요 APM 솔루션

| 기능 | Datadog | New Relic | Dynatrace | 오픈소스 스택 |

|------|---------|-----------|-----------|------------|

| 가격 모델 | 호스트/사용량 기반 | 사용량 기반 | 호스트 기반 | 인프라 비용만 |

| 자동 계측 | 우수 | 우수 | 최고 | 보통 |

| APM | 포함 | 포함 | 포함 | Jaeger/Tempo |

| 로그 관리 | 포함 | 포함 | 포함 | ELK/Loki |

| 메트릭 | 포함 | 포함 | 포함 | Prometheus |

| AI 기반 분석 | Watchdog | Applied Intelligence | Davis AI | 없음 |

| 설정 복잡도 | 낮음 | 낮음 | 중간 | 높음 |

| 벤더 종속성 | 높음 | 높음 | 높음 | 없음 |

9.2 오픈소스 스택 구성

+-----------------+ +------------------+

| Application |---->| OTel Collector |

| (OTel SDK) | | (수집/처리) |

+-----------------+ +--------+---------+

|

+------------+------------+

| | |

+-----v-----+ +---v---+ +-----v-----+

| Prometheus | | Tempo | | Loki |

| (Metrics) | |(Trace)| | (Logs) |

+-----+------+ +---+---+ +-----+-----+

| | |

+------+-----+------+-----+

| |

+----v----+ +----v----+

| Grafana | | Grafana |

| (시각화)| | (통합) |

+---------+ +---------+

10. 비용 최적화

10.1 데이터 볼륨 관리

로그 보존 정책

retention_policy:

hot_tier: 7d # 최근 7일: 빠른 검색

warm_tier: 30d # 최근 30일: 느린 검색

cold_tier: 90d # 최근 90일: 아카이브

delete_after: 365d # 1년 후 삭제

인덱스 수명 관리 (Elasticsearch ILM)

index_lifecycle:

phases:

hot:

actions:

rollover:

max_size: 50gb

max_age: 1d

warm:

min_age: 7d

actions:

shrink:

number_of_shards: 1

forcemerge:

max_num_segments: 1

cold:

min_age: 30d

actions:

searchable_snapshot:

snapshot_repository: s3_repo

delete:

min_age: 365d

10.2 샘플링으로 비용 절감

// 적응형 샘플링

class AdaptiveSampler {

constructor(targetRate = 100) {

this.targetRate = targetRate; // 초당 목표 수집량

this.currentRate = 0;

this.samplingProbability = 1.0;

// 10초마다 샘플링 확률 조정

setInterval(() => this.adjust(), 10000);

}

shouldSample() {

this.currentRate++;

return Math.random() < this.samplingProbability;

}

adjust() {

if (this.currentRate > this.targetRate * 10) {

// 트래픽이 목표의 10배 이상이면 샘플링 확률 줄이기

this.samplingProbability = this.targetRate / this.currentRate;

} else {

this.samplingProbability = Math.min(1.0, this.samplingProbability * 1.1);

}

this.currentRate = 0;

}

}

10.3 메트릭 집계

Prometheus Recording Rules로 원본 데이터 집계

groups:

- name: aggregation

interval: 1m

rules:

서비스별 집계 (인스턴스 레이블 제거로 카디널리티 감소)

- record: service:requests:rate5m

expr: sum(rate(http_requests_total[5m])) by (service, method, status)

시간 해상도 줄이기 (5분 -> 1시간)

- record: service:requests:rate1h

expr: sum(rate(http_requests_total[1h])) by (service)

11. 프로덕션 체크리스트

11.1 배포 전 확인 사항

- 모든 서비스에 구조화된 로깅 적용

- Correlation ID 전파 확인

- Prometheus 메트릭 엔드포인트 노출

- OTel 자동 계측 활성화

- 헬스체크 엔드포인트 구현

- SLO 정의 및 에러 예산 설정

11.2 운영 확인 사항

- Grafana 대시보드 (RED/USE method)

- 알림 규칙 설정 (Critical/Warning/Info)

- Runbook 작성 완료

- 온콜 로테이션 설정

- 로그 보존 정책 설정

- 비용 모니터링

11.3 정기 검토 사항

- 월별 SLO 리뷰

- 알림 노이즈 분석

- 미사용 대시보드/알림 정리

- 비용 최적화 검토

- 인시던트 사후 분석 (Post-mortem)

12. 퀴즈

**정답: Logs, Metrics, Traces**

- **Logs**: 개별 이벤트의 기록. 디버깅과 감사에 사용.

- **Metrics**: 시간에 따른 수치 측정. 시스템 성능 추이 파악.

- **Traces**: 분산 시스템에서 요청의 전체 경로를 추적. 병목 지점 식별.

이 세 가지를 결합하면 "무엇이 고장났는가"뿐만 아니라 "왜 고장났는가"를 이해할 수 있습니다.

**정답:**

histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))

`histogram_quantile` 함수는 Histogram 메트릭에서 분위수를 계산합니다. `0.99`는 99번째 백분위수를 의미하고, `rate()`로 5분간의 변화율을 구한 후 `le`(less than or equal) 레이블로 그룹화합니다.

**정답:**

**Head Sampling**은 트레이스가 **시작될 때** 수집 여부를 결정합니다. 단순하고 오버헤드가 낮지만, 에러가 발생한 트레이스를 놓칠 수 있습니다.

**Tail Sampling**은 트레이스가 **완료된 후** 수집 여부를 결정합니다. 에러나 느린 응답의 트레이스를 확실하게 보존할 수 있지만, 모든 Span을 메모리에 보관해야 하므로 리소스 사용량이 높습니다.

프로덕션에서는 보통 Tail Sampling을 OTel Collector에서 수행하여 에러 트레이스를 우선적으로 수집합니다.

**정답: 약 43.2분**

계산: 30일 * 24시간 * 60분 = 43,200분

Error Budget = 43,200 * 0.001 = 43.2분

즉, 한 달에 43.2분까지의 다운타임은 SLO 범위 내입니다. Error Budget이 소진되면 새 기능 릴리스를 중단하고 안정성 개선에 집중해야 합니다.

**정답:**

**RED Method**는 **서비스** 모니터링에 사용됩니다:

- Rate: 초당 요청 수

- Errors: 에러율

- Duration: 응답 시간

**USE Method**는 **인프라 리소스** 모니터링에 사용됩니다:

- Utilization: 리소스 사용률 (CPU, 메모리 등)

- Saturation: 대기열 길이, 과부하 정도

- Errors: 하드웨어/시스템 에러

RED는 사용자 경험 관점에서 서비스를 모니터링하고, USE는 시스템 관점에서 인프라를 모니터링합니다. 둘을 함께 사용하면 문제의 근본 원인을 빠르게 파악할 수 있습니다.

13. 참고 자료

1. **OpenTelemetry Documentation** - https://opentelemetry.io/docs/

2. **Prometheus Documentation** - https://prometheus.io/docs/

3. **Grafana Documentation** - https://grafana.com/docs/

4. **Jaeger Documentation** - https://www.jaegertracing.io/docs/

5. **Grafana Loki** - https://grafana.com/oss/loki/

6. **Grafana Tempo** - https://grafana.com/oss/tempo/

7. **ELK Stack** - https://www.elastic.co/elk-stack

8. **Google SRE Book - Monitoring** - https://sre.google/sre-book/monitoring-distributed-systems/

9. **Google SRE Book - Service Level Objectives** - https://sre.google/sre-book/service-level-objectives/

10. **pino Logger** - https://getpino.io/

11. **Alertmanager** - https://prometheus.io/docs/alerting/latest/alertmanager/

12. **PagerDuty Incident Response** - https://response.pagerduty.com/

13. **The RED Method** - https://grafana.com/blog/2018/08/02/the-red-method-how-to-instrument-your-services/

14. **The USE Method** - https://www.brendangregg.com/usemethod.html

Observability는 단순히 도구를 설치하는 것이 아니라 **문화**입니다. 모든 팀원이 로그를 구조화하고, 메트릭을 정의하고, 트레이스를 활용하는 습관을 갖추어야 합니다. SLO를 중심으로 알림 전략을 세우고, Error Budget으로 릴리스와 안정성의 균형을 맞추세요. 비용 최적화를 위해 샘플링과 보존 정책을 적극 활용하는 것도 잊지 마세요.

현재 단락 (1/823)

전통적인 모니터링은 **알려진 문제(known unknowns)**를 감지하는 데 초점을 맞춥니다. CPU 사용률이 90%를 넘으면 알림, 디스크 사용량이 80%를 넘으면 알림 -...

작성 글자: 0원문 글자: 20,192작성 단락: 0/823