Skip to content
Published on

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

Authors

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 설정
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { Resource } from '@opentelemetry/resources';
import {
  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 생성

import { trace, SpanStatusCode, context } from '@opentelemetry/api';

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를 활용한 구조화된 로깅
import pino from '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)
import winston from 'winston';
import LokiTransport from '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 클라이언트
import { Registry, Counter, Gauge, Histogram, Summary } from 'prom-client';

const register = new Registry();

// 기본 메트릭 수집 (CPU, 메모리, 이벤트 루프 등)
import { collectDefaultMetrics } from 'prom-client';
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는 서비스의 핵심 성능 지표를 모니터링하는 방법론입니다:

  • Rate: 초당 요청 수
  • Errors: 에러율
  • Duration: 응답 시간
{
  "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는 인프라 리소스를 모니터링하는 방법론입니다:

  • Utilization: 리소스 사용률
  • Saturation: 대기열 길이
  • Errors: 에러 수
# 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 추출/주입
import { propagation, context, trace } from '@opentelemetry/api';

// 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 전파
import { GrpcInstrumentation } from '@opentelemetry/instrumentation-grpc';

// 자동 계측으로 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 샘플링 전략

// 샘플링 설정
import { ParentBasedSampler, TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base';

// 비율 기반 샘플링 (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 솔루션

기능DatadogNew RelicDynatrace오픈소스 스택
가격 모델호스트/사용량 기반사용량 기반호스트 기반인프라 비용만
자동 계측우수우수최고보통
APM포함포함포함Jaeger/Tempo
로그 관리포함포함포함ELK/Loki
메트릭포함포함포함Prometheus
AI 기반 분석WatchdogApplied IntelligenceDavis 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. 퀴즈

Q1: Observability의 Three Pillars(세 가지 축)는 무엇인가요?

정답: Logs, Metrics, Traces

  • Logs: 개별 이벤트의 기록. 디버깅과 감사에 사용.
  • Metrics: 시간에 따른 수치 측정. 시스템 성능 추이 파악.
  • Traces: 분산 시스템에서 요청의 전체 경로를 추적. 병목 지점 식별.

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

Q2: Prometheus에서 p99 응답 시간을 조회하는 PromQL을 작성하세요.

정답:

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) 레이블로 그룹화합니다.

Q3: Head Sampling과 Tail Sampling의 차이점은 무엇인가요?

정답:

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

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

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

Q4: SLO가 99.9%일 때 한 달(30일) 동안 허용되는 다운타임은 얼마인가요?

정답: 약 43.2분

계산: 30일 * 24시간 * 60분 = 43,200분 Error Budget = 43,200 * 0.001 = 43.2분

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

Q5: RED Method와 USE Method의 차이점을 설명하세요.

정답:

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으로 릴리스와 안정성의 균형을 맞추세요. 비용 최적화를 위해 샘플링과 보존 정책을 적극 활용하는 것도 잊지 마세요.