// 이 파일이 하는 일: "관측성 — 보이지 않으면 고칠 수 없다" 코스 — 모니터링과 관측성의
// 차이에서 출발해, 메트릭 설계·구조화 로깅·분산 트레이싱·OpenTelemetry·대시보드/알람 설계,
// 그리고 실전 미스터리 풀이까지 7개 섹션으로 안내하는 정적 학습 페이지. 프레이밍은
// "분산 시스템은 눈이 없으면 고장도 못 찾는다" — 특급 과정답게 traderbox의 실제 구조
// (gateway→auth, Kafka audit.events, SSE 스트림)를 관측 대상 삼아 개념을 실무에 붙인다.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

import { Link } from 'react-router-dom';

// ── 예제 코드 상수들 ──

const CODE_PILLARS = `모니터링                        관측성(Observability)
"내가 아는 질문에 답한다"        "몰랐던 질문에도 답할 수 있다"

- CPU가 80% 넘었나?              - 왜 '이 유저'의 '이 요청'만 느리지?
- 서버가 죽었나?                 - 어제 배포 이후 p99가 왜 튀지?
- 미리 만든 그래프를 본다        - 그때그때 새 질문을 데이터에 던진다

세 기둥의 역할 분담:
  메트릭   → "무엇이 이상한가"   숫자·추세. 싸고 빠르다. 알람의 재료.
  트레이스 → "어디가 느린가"     한 요청의 여정. 서비스 사이를 잇는다.
  로그     → "정확히 무슨 일이"  그 지점의 상세 사건 기록. 가장 비싸다.

넓게(메트릭) → 좁혀서(트레이스) → 깊게(로그). 셋은 경쟁자가 아니라 순서다.`;

const CODE_METRIC_TYPES = `// 메트릭 3형제 — Micrometer(Spring Boot 내장) 기준
// ① 카운터: 오르기만 하는 수. "지금까지 몇 번?"
Counter.builder("auth.login.total")
    .tag("result", "success")        // 성공/실패를 레이블로 구분
    .register(registry)
    .increment();

// ② 게이지: 오르내리는 현재값. "지금 몇 개?"
Gauge.builder("market.sse.connections", connections, Set::size)
    .register(registry);             // SSE 동시 접속 수 — 스냅샷

// ③ 히스토그램: 값의 '분포'. "지연이 어떻게 퍼져 있나?"
Timer.builder("gateway.request.duration")
    .publishPercentiles(0.5, 0.95, 0.99)   // p50 / p95 / p99
    .register(registry)
    .record(elapsed);

// 용도 감각: 요청 수=카운터, 큐 길이·접속 수=게이지, 지연·크기=히스토그램`;

const CODE_RED_USE = `RED — '서비스'를 보는 렌즈 (요청을 받는 모든 것에)
  Rate      요청률   초당 몇 건이 들어오나
  Errors    에러율   그중 몇 건이 실패하나
  Duration  지연     각 요청이 얼마나 걸리나 (반드시 분포로!)

USE — '자원'을 보는 렌즈 (CPU·메모리·디스크·커넥션풀에)
  Utilization  사용률   자원이 얼마나 바쁜가
  Saturation   포화도   대기줄이 얼마나 쌓였나 (큐 길이·대기 스레드)
  Errors       에러     자원 수준의 실패 (디스크 I/O 에러 등)

실전 조합: 서비스마다 RED 세 개 + 그 서비스가 기대는 자원의 USE.
tbox-auth라면: 로그인 RED 3종 + DB 커넥션풀 USE. 이것만으로 개요판 완성.`;

const CODE_PERCENTILE = `같은 하루, 같은 API의 응답 시간 10건:
  40ms 42ms 45ms 48ms 50ms 52ms 55ms 60ms 65ms 2400ms

  평균(mean) = 285ms   ← "평균 285ms면 좀 느리네?" 실체 없는 숫자.
  p50 (중앙값) = 51ms   ← 절반의 사용자는 51ms 이하를 경험
  p90          = 65ms   ← 90%는 65ms 이하
  p99          ≈ 2400ms ← 상위 1%가 겪는 지옥. 여기가 진짜 문제.

평균의 거짓말: 단 한 건의 2.4초가 평균을 5배로 부풀렸다.
반대로 p99가 나쁜데 평균이 멀쩡하면 "일부 사용자만 계속 고통받는" 상태.
트래픽이 클수록 그 1%는 수천 명이다. SLO는 평균이 아니라 퍼센타일로 건다.`;

const CODE_CARDINALITY = `# 카디널리티 = 한 레이블이 가질 수 있는 '서로 다른 값'의 수
# 메트릭 저장소는 레이블 조합마다 별도의 시계열을 만든다.

http_requests_total(method="GET",  path="/api/auth/login", status="200")
http_requests_total(method="POST", path="/api/auth/login", status="401")
# method(5종) x path(50종) x status(10종) = 최대 2,500 시계열 → OK

# 여기에 user_id 레이블을 넣는 순간:
http_requests_total(..., user_id="a3f9...")   # 유저 10만 명이면
# 2,500 x 100,000 = 2억 5천만 시계열 → 메트릭 서버가 먼저 죽는다.

# 규칙: 레이블엔 '종류가 유한하고 적은 것'만 (메서드·상태코드·서비스명).
# user_id·요청ID·URL 전체처럼 무한히 늘어나는 값은
# 메트릭이 아니라 로그·트레이스의 몫이다.`;

const CODE_LOG_JSON = `// grep의 세계: 사람이 읽는 한 줄 문자열
2026-07-20 14:03:12 ERROR Login failed for user 4821 from 10.0.3.7 after 3 attempts

// 쿼리의 세계: 기계가 읽는 구조화(JSON) 로그
JSON 한 줄 = 하나의 사건(event):
  "ts":       "2026-07-20T14:03:12.481Z",
  "level":    "ERROR",
  "service":  "tbox-auth",
  "event":    "login.failed",
  "user_id":  4821,
  "ip":       "10.0.3.7",
  "attempts": 3,
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"   // ← 트레이스와 잇는 다리!

// 이제 이런 질문이 '쿼리 한 줄'이 된다:
//   service=tbox-auth AND event=login.failed AND attempts>=3
//   → 지난 1시간, IP별로 집계
// 비결은 화려한 도구가 아니라 '모든 서비스가 같은 필드 이름을 쓰는 것'.
// service·event·user_id·trace_id 같은 공통 스키마를 팀 규칙으로 못박아라.`;

const CODE_LOG_LEVEL = `레벨      찍는 것                              운영 기본값
ERROR    사람이 조치해야 하는 실패              항상 켬
WARN     지금은 견뎠지만 신호인 것 (재시도 성공)  항상 켬
INFO     비즈니스 사건 (로그인, 주문, 배포)      항상 켬
DEBUG    개발자용 상세 (파라미터, 분기 경로)      끔 — 필요할 때만
TRACE    극상세 (루프 내부까지)                  끔

"운영에서 DEBUG를 켜면 생기는 일":
  초당 500 요청 x 요청당 DEBUG 30줄 = 초당 15,000줄
  → 디스크가 차오르고, 수집 파이프라인이 밀리고,
    정작 중요한 ERROR가 소음에 파묻힌다. 로그도 비용이다.
해법: 전체를 켜지 말고 '특정 로거만, 잠깐만' 런타임에 올렸다 내린다.`;

const CODE_TRACE = `한 요청의 트레이스(trace) — 스팬(span)들의 폭포수(waterfall):

trace_id: 4bf92f35...        총 340ms
├─ [gateway]  route /api/auth/me            ────────────── 340ms
│   ├─ [auth]  GET /api/auth/me             ──────────── 310ms
│   │   ├─ [auth]  JWT 검증                  ─ 2ms
│   │   ├─ [auth]  DB SELECT users           ──────── 210ms  ← 병목!
│   │   └─ [auth]  응답 직렬화               ─ 3ms
│   └─ [gateway] 응답 전달                   ─ 5ms

읽는 법:
- 트레이스 = 요청 하나의 전체 여정. 스팬 = 그 안의 구간 하나.
- 각 스팬은 부모를 가리켜 트리를 이룬다 (gateway 스팬이 auth 스팬의 부모).
- 폭포수에서 '가장 길고 자식이 없는 스팬'이 병목. 위에선 DB SELECT 210ms.
- 스팬 사이의 '빈 시간'도 단서다 — 큐 대기, 커넥션풀 대기가 거기 숨는다.`;

const CODE_CONTEXT = `# 컨텍스트 전파 — trace_id가 서비스를 '건너가는' 방법
# 비밀은 그냥 HTTP 헤더다. W3C 표준 traceparent:

GET /api/auth/me HTTP/1.1
Host: tbox-auth:8081
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
#             │  └── trace_id (요청 전체에서 동일) └── 부모 span_id

# gateway가 auth를 부를 때 이 헤더를 '이어 붙여' 보내면,
# auth는 새 트레이스를 만들지 않고 같은 trace_id 아래 자기 스팬을 단다.
# Kafka로 건널 땐? 메시지 헤더에 같은 값을 실어 보낸다 (audit.events도 가능).

# 전파가 한 군데라도 끊기면 → 트레이스가 두 동강 난 '고아 스팬'이 된다.
# 자체 스레드풀·비동기 경계가 단골 절단 지점이니 계측 라이브러리에 맡기는 게 안전.`;

const CODE_SAMPLING = `# 샘플링 — 전부 저장할 수 없을 때 무엇을 남길까
# 초당 1만 요청 x 요청당 스팬 10개 = 초당 10만 스팬. 다 저장하면 저장소가 터진다.

head-based (머리에서 결정)
  요청이 '시작될 때' 저장 여부를 정한다. 예: 10%만 무작위 저장.
  장점: 구현 단순, 부하 예측 가능
  단점: 하필 안 뽑힌 그 요청이 장애였다면? 놓친다.

tail-based (꼬리에서 결정)
  요청이 '끝난 뒤' 결과를 보고 정한다.
  예: 에러난 것 100% + 1초 넘은 것 100% + 정상은 1%만.
  장점: 문제 있는 트레이스는 반드시 남는다 ← 디버깅엔 이게 정답
  단점: 끝날 때까지 전 스팬을 버퍼에 들고 있어야 해서 수집기가 무겁다

실무 감각: 트래픽이 작을 땐 100% 저장부터 시작하고, 아플 때 샘플링을 도입해라.`;

const CODE_OTEL = `# OpenTelemetry(OTel) = 계측의 '표준 규격' — 특정 벤더의 것이 아니다
# 코드에는 OTel 규격으로 심고, 보내는 곳(백엔드)은 설정으로 갈아끼운다.
#   앱 --OTLP 프로토콜--> Collector --> Tempo / Jaeger / Datadog / ... (교체 자유)

# Spring Boot: '자동 계측' — 에이전트를 붙이기만 하면
#   HTTP 서버/클라이언트, JDBC, Kafka 스팬이 공짜로 생긴다
java -javaagent:opentelemetry-javaagent.jar \\
     -Dotel.service.name=tbox-auth \\
     -Dotel.exporter.otlp.endpoint=http://otel-collector:4318 \\
     -jar service-auth.jar
# (Micrometer Tracing으로 의존성만 넣는 방법도 있다 — Boot가 자동 구성)

# Next.js: 서버 사이드에 instrumentation.ts 훅으로 등록
// instrumentation.ts
export async function register() {
  const otel = await import('@vercel/otel');
  otel.registerOTel({ serviceName: 'tbox-web' });
}

# 자동 계측이 공짜로 주는 것: HTTP·DB·메시징 스팬, 표준 메트릭
# 직접 심어야 하는 것: 비즈니스 스팬("호가 계산"), 도메인 속성(종목코드 등)`;

const CODE_COMPOSE = `# 실습 스택 — Prometheus(메트릭) + Grafana(화면) + Tempo(트레이스)
# platform/docker-compose.observability.yml (새로 만든다)
services:
  prometheus:
    image: prom/prometheus:v3.4.0
    volumes: ["./prometheus.yml:/etc/prometheus/prometheus.yml"]
    ports: ["9090:9090"]        # 15초마다 각 서비스 /actuator/prometheus를 긁는다
  tempo:
    image: grafana/tempo:2.7.0
    ports: ["4318:4318"]        # 앱들이 OTLP로 스팬을 쏘는 곳
  grafana:
    image: grafana/grafana:12.0.0
    ports: ["3100:3000"]        # 데이터소스로 Prometheus + Tempo를 등록

# 미션: service-auth의 /api/auth/me 처리에 일부러 200ms 지연을 심고,
#   ① Grafana에서 gateway p99가 뛰는 걸 메트릭으로 목격
#   ② Tempo에서 그 시간대 느린 트레이스를 하나 열어
#   ③ 폭포수에서 '어느 스팬이 200ms를 먹었는지' 코드 안 보고 짚어내기
# 성공 기준: 지연을 심은 위치를 트레이스만 보고 맞히면 통과.`;

const CODE_ALERT = `증상 기반 알람 (사용자가 아프다) — 페이징(새벽에 깨움) 대상
  - 5분간 에러율 > 1%                  "지금 사용자가 실패를 보고 있다"
  - 5분간 p99 > 2s                     "지금 사용자가 기다리고 있다"
  - SSE 활성 연결 수가 5분 새 80% 급감   "시세 스트림이 끊기고 있다"

원인 기반 알람 (자원이 아프다) — 티켓/채널 알림이면 충분
  - CPU > 90%          그래서? 사용자 지연이 없다면 새벽엔 안 깨워도 된다
  - 디스크 > 85%        내일 아침에 늘리면 된다 (단, 예측상 오늘 밤 차면 페이징)
  - GC 시간 증가        지표로 지켜보다 지연에 번지면 그때 증상 알람이 운다

알람 피로(alert fatigue)의 법칙:
  울려도 아무도 안 움직이는 알람이 3개 생기면, 4번째 진짜 알람도 무시된다.
  "이 알람이 울리면 새벽 3시에 일어나 '즉시 할 일'이 있는가?" — 없으면 페이징에서 빼라.`;

const CODE_SCENARIO = `미스터리: "VIP 유저 몇 명만 대시보드가 느리다" — 좁혀가기 3단계

① 메트릭 (무엇이): Grafana에서 /api/market p99 = 3.2s, p50 = 45ms
   → 평균은 멀쩡한데 꼬리만 나쁘다 = '일부 요청'만 느리다. 전역 장애 아님.
② 트레이스 (어디가): Tempo에서 duration > 2s 트레이스 검색 → 20건 열람
   → 전부 market의 '관심종목 시세 조회' 스팬이 2.9s. DB 스팬이 그 대부분.
③ 로그 (왜): 그 trace_id로 로그 조인 → "watchlist size: 480" (보통 유저는 ~15)
   → 결론: 관심종목 수백 개를 등록한 헤비 유저만 N+1 조회 폭발.

메트릭이 범위를, 트레이스가 위치를, 로그가 이유를 줬다.
trace_id가 세 기둥을 하나로 꿴 실이었다는 것 — 이게 이 코스 전체의 요약이다.`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
function Section({ n, title, sub, children }) {
  return (
    <section className="step-card" id={`sec-${n}`}>
      <div className="step-head">
        <span className="step-num">{n}</span>
        <div>
          <h3>{title}</h3>
          {sub && <span className="step-sub">{sub}</span>}
        </div>
      </div>
      <div className="step-body">{children}</div>
    </section>
  );
}

function Code({ children }) {
  return <pre className="code-block"><code>{children}</code></pre>;
}

const SECTIONS = [
  { n: 1, label: '관측성이란' },
  { n: 2, label: '메트릭 설계' },
  { n: 3, label: '구조화 로깅' },
  { n: 4, label: '분산 트레이싱' },
  { n: 5, label: 'OpenTelemetry' },
  { n: 6, label: '대시보드·알람' },
  { n: 7, label: '실전 시나리오' },
];

export default function Lv4ObservabilityMetricsTracingPage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 특급 과정</div>
        <h1>관측성<br />— 보이지 않으면 고칠 수 없다</h1>
        <p>
          서비스가 6개로 쪼개지는 순간, "코드에 print 찍어 보기"는 죽은 기술이 됩니다.
          요청 하나가 게이트웨이→인증→DB를 건너다니는데, 어느 상자 안에서 느려졌는지
          바깥에선 보이지 않으니까요. <strong>메트릭·로그·트레이스</strong>라는 세 기둥으로
          분산 시스템의 내부를 들여다보고, <strong>"이 요청, 어디서 느려졌나"</strong>에
          데이터로 답하는 기술 — 그게 관측성입니다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 85분</span>
          <span className="chip">실습: 트레이스로 200ms 지연 찾기</span>
          <span className="chip">선수 지식: MSA·Docker Compose·HTTP</span>
        </div>
      </div>

      {/* 섹션 바로가기 */}
      <nav className="pill-nav">
        {SECTIONS.map((s) => (
          <a key={s.n} href={`#sec-${s.n}`}>
            <b>{s.n}</b>
            {s.label}
          </a>
        ))}
      </nav>

      <Section n={1} title="모니터링을 넘어 관측성으로" sub="아는 질문에 답하기 vs 모르는 질문에도 답하기">
        <p>
          <strong>모니터링</strong>은 "CPU가 80%를 넘으면 알려 줘"처럼 <strong>미리 아는
          질문</strong>에 답을 준비해 두는 일이에요. 훌륭하지만 한계가 뚜렷합니다 — 장애는
          늘 <strong>예상 못 한 모양</strong>으로 오거든요. <strong>관측성(observability)</strong>은
          한 발 더 갑니다. 시스템이 밖으로 내보내는 데이터(메트릭·로그·트레이스)가 충분히
          풍부해서, <strong>사고 당일에 처음 떠올린 질문</strong>에도 즉석에서 답을 캘 수 있는
          상태를 말해요. 그래프를 "보는" 게 아니라 데이터에 "질문하는" 겁니다.
        </p>
        <p>
          왜 지금 배울까요? 모놀리스 시절엔 프로세스가 하나라 디버거와{' '}
          <span className="icode">print</span>로도 버텼어요. 그런데 우리 traderbox처럼
          gateway·auth·market·log·content·macro가 <strong>각자의 컨테이너</strong>에서 돌면,
          한 요청의 흔적이 여섯 군데에 흩어집니다. 어느 프로세스에 디버거를 붙일지조차
          모르는 상태 — 여기서 print 디버깅은 무력해집니다.
        </p>
        <Code>{CODE_PILLARS}</Code>
        <p>
          세 기둥은 서로 <strong>역할이 다릅니다</strong>. 메트릭은 값싸고 빨라서 "무엇이
          이상한가"를 넓게 감시하고, 트레이스는 한 요청의 여정을 따라가 "어디가 느린가"를
          짚고, 로그는 그 지점에서 "정확히 무슨 일이 있었나"를 증언해요. 하나만으로는
          부족하고, <strong>넓게→좁게→깊게</strong> 순서로 갈아타는 게 기본기입니다.
        </p>
        <div className="tip">
          <b>비유</b> 병원 진료와 같아요. 체온·혈압(메트릭)으로 "이상하다"를 감지하고,
          CT 촬영(트레이스)으로 "어느 부위인지" 좁히고, 조직 검사(로그)로 "정확한 원인"을
          확정합니다. 체온계만으로 수술 부위를 정하는 의사는 없죠 — 지표 하나로 원인을
          단정하는 개발자도 마찬가지입니다.
        </div>
      </Section>

      <Section n={2} title="메트릭 설계 — 숫자로 시스템의 맥박 재기" sub="카운터·게이지·히스토그램, 그리고 평균의 거짓말">
        <p>
          메트릭은 <strong>이름 + 레이블 + 시간에 따른 숫자</strong>예요. 원재료는 딱 세
          종류입니다. 오르기만 하는 <strong>카운터</strong>(총 요청 수), 오르내리는 현재값{' '}
          <strong>게이지</strong>(동시 접속 수), 값의 분포를 담는 <strong>히스토그램</strong>
          (응답 시간). 어떤 걸 쓸지는 "이 숫자가 시간이 지나면 어떻게 움직이나"를 자문하면
          답이 나옵니다.
        </p>
        <Code>{CODE_METRIC_TYPES}</Code>
        <p>
          "그래서 <strong>뭘</strong> 재야 하죠?"엔 검증된 답안이 있어요. 요청을 받는
          서비스엔 <strong>RED</strong>(요청률·에러율·지연), CPU·커넥션풀 같은 자원엔{' '}
          <strong>USE</strong>(사용률·포화도·에러). 이 두 렌즈만 끼면 "일단 다 수집하고
          보자"는 늪을 피할 수 있습니다.
        </p>
        <Code>{CODE_RED_USE}</Code>
        <p>
          지연(D)을 볼 땐 함정이 하나 있어요 — <strong>평균은 거짓말을 합니다</strong>.
          응답 시간 분포는 한쪽 꼬리가 긴 모양이라, 소수의 아주 느린 요청이 평균을 통째로
          끌어올리거나, 반대로 다수의 빠른 요청이 소수의 고통을 가려 버려요. 그래서{' '}
          <strong>퍼센타일</strong>로 봅니다. p50은 "절반의 경험", p99는 "가장 아픈 1%의
          경험"이에요.
        </p>
        <Code>{CODE_PERCENTILE}</Code>
        <p>
          마지막 함정은 <strong>카디널리티 폭발</strong>. 메트릭 저장소는 레이블 값의 조합마다
          시계열을 하나씩 만들기 때문에, 레이블에 <span className="icode">user_id</span>처럼
          무한히 늘어나는 값을 넣는 순간 시계열 수가 곱셈으로 폭발합니다.
        </p>
        <Code>{CODE_CARDINALITY}</Code>
        <div className="warn">
          <b>실수 1순위</b> "유저별로 지연을 보고 싶어서" 레이블에 user_id를 넣는 것.
          의도는 좋지만 그 질문의 정답 도구는 메트릭이 아니라 <strong>트레이스와 로그</strong>
          입니다(섹션 7에서 실제로 그렇게 풉니다). 메트릭 레이블은{' '}
          <strong>값의 종류가 유한하고 적은 것</strong>만 — 이 한 줄이 메트릭 서버의 수명을
          정해요.
        </div>
      </Section>

      <Section n={3} title="구조화 로깅 — grep을 넘어 쿼리로" sub="문자열이 아니라 '사건 데이터'를 남긴다">
        <p>
          로그를 "사람이 읽을 한 줄 문장"으로 남기면, 서버 6대 시대엔 6군데에 흩어진 문장을{' '}
          <span className="icode">grep</span>으로 뒤지는 수밖에 없어요. 발상을 바꿔 로그를{' '}
          <strong>기계가 읽을 JSON 사건 데이터</strong>로 남기면, 로그 더미가 통째로{' '}
          <strong>쿼리 가능한 데이터베이스</strong>가 됩니다.
        </p>
        <Code>{CODE_LOG_JSON}</Code>
        <p>
          여기서 도구보다 중요한 게 <strong>일관된 필드 스키마</strong>예요. auth는{' '}
          <span className="icode">userId</span>, market은 <span className="icode">uid</span>,
          log는 <span className="icode">user</span>라고 제각각 쓰면, 서비스를 가로지르는 쿼리가
          그 즉시 불가능해집니다. <span className="icode">service</span>·
          <span className="icode">event</span>·<span className="icode">user_id</span>·
          <span className="icode">trace_id</span> 같은 공통 필드를 <strong>팀 규칙</strong>으로
          못박는 것 — 그게 구조화 로깅의 절반입니다. 특히{' '}
          <span className="icode">trace_id</span>는 섹션 4의 트레이스와 로그를 잇는 다리라서
          빠지면 안 돼요.
        </p>
        <p>
          두 번째 축은 <strong>로그 레벨 전략</strong>. 레벨은 "중요한 순서"가 아니라{' '}
          <strong>"운영에서 상시로 켜 둘 것과 아닌 것"의 경계선</strong>이에요.
        </p>
        <Code>{CODE_LOG_LEVEL}</Code>
        <p>
          마지막으로 <strong>중앙 수집</strong>. 컨테이너는 언제든 죽고 다시 뜨니, 로그가
          컨테이너 안에만 있으면 시체와 함께 사라져요. 각 서비스가 표준 출력으로 뱉으면
          수집기가 중앙 저장소로 모으는 파이프라인이 필요합니다. 사실 우리 traderbox엔 이미
          그 원형이 있어요 — 각 서비스의 <span className="icode">AuditEventPublisher</span>가
          감사 사건을 Kafka 토픽 <span className="icode">audit.events</span>로 쏘고, log
          서비스의 <span className="icode">AuditEventConsumer</span>가 받아 저장하죠.
          "사건을 구조화해 → 비동기 파이프로 → 중앙에 모은다"는 뼈대가 정확히 같습니다.
        </p>
        <div className="tip">
          <b>관측 관점에서 다시 읽기</b> Kafka를 끼운 이유도 관측성 문법으로 읽을 수 있어요.
          로그·감사 수집이 <strong>본 요청의 응답 속도를 방해하면 안 되고</strong>(비동기),
          수집기가 잠시 죽어도 <strong>사건이 유실되면 안 되기</strong>(버퍼) 때문입니다.
          관측 데이터는 어디까지나 조연이라, 주연(사용자 요청)의 발목을 잡는 순간 설계가
          잘못된 거예요.
        </div>
      </Section>

      <Section n={4} title="분산 트레이싱 — 요청의 여정을 이어 붙이기" sub="trace_id 하나로 서비스 6개를 꿰는 실">
        <p>
          이 코스의 심장입니다. <strong>트레이스</strong>는 요청 하나가 시스템을 통과한{' '}
          <strong>전체 여정의 기록</strong>이고, <strong>스팬</strong>은 그 여정의 구간
          하나("auth의 DB 조회")예요. 스팬마다 시작 시각·소요 시간·부모 스팬이 붙어서,
          모아 보면 폭포수 모양의 트리가 됩니다.
        </p>
        <Code>{CODE_TRACE}</Code>
        <p>
          폭포수를 읽는 눈은 하나예요 — <strong>가장 넓고(오래 걸리고) 자식이 없는
          스팬</strong>을 찾는 것. 자식이 있는 스팬은 시간의 대부분을 자식에게 위임한 것이라,
          진짜 범인은 항상 잎(leaf)에 있습니다. 위 예시라면 340ms 중 210ms를 먹은{' '}
          <span className="icode">DB SELECT</span>가 범인이죠. 스팬과 스팬 사이의{' '}
          <strong>빈 틈</strong>도 단서예요 — 커넥션풀 대기, 큐 대기처럼 "아무 코드도 안
          돌지만 시간이 가는" 구간이 거기 숨습니다.
        </p>
        <p>
          그런데 서비스가 다른 프로세스, 다른 컨테이너인데 어떻게 스팬들이 한 트레이스로
          묶일까요? 비밀은 허무할 만큼 단순합니다 — <strong>HTTP 헤더로 trace_id를 넘겨주는
          것</strong>, 이걸 <strong>컨텍스트 전파(context propagation)</strong>라고 불러요.
        </p>
        <Code>{CODE_CONTEXT}</Code>
        <p>
          마지막 조각은 <strong>샘플링</strong>. 트레이스는 요청마다 생기니 트래픽이 크면
          전부 저장할 수 없어요. 언제 "남길지"를 정하는 두 전략이 있습니다.
        </p>
        <Code>{CODE_SAMPLING}</Code>
        <div className="warn">
          <b>전파가 끊기는 순간</b> 트레이싱 도입 후 가장 흔한 사고는 "트레이스가 gateway에서
          끝나 있어요"입니다. 서비스 하나가 <span className="icode">traceparent</span> 헤더를
          안 넘기면 그 뒤는 전부 <strong>고아 스팬</strong>이 돼요. 직접 만든 스레드풀,
          비동기 콜백, 커스텀 HTTP 클라이언트가 단골 절단 지점 — 그래서 다음 섹션의{' '}
          <strong>자동 계측</strong>에 맡기는 게 수공예보다 안전합니다.
        </div>
      </Section>

      <Section n={5} title="OpenTelemetry — 계측의 표준" sub="코드엔 표준으로 심고, 백엔드는 갈아끼운다">
        <p>
          메트릭·로그·트레이스를 코드에 심는 일을 <strong>계측(instrumentation)</strong>이라
          해요. 문제는 이걸 특정 상용 도구의 SDK로 해 버리면, 도구를 바꿀 때{' '}
          <strong>서비스 전부의 코드를 다시 짜야</strong> 한다는 것 — 전형적인 벤더 락인이죠.{' '}
          <strong>OpenTelemetry(OTel)</strong>는 그래서 나온 <strong>업계 표준 규격</strong>
          입니다. 코드에는 OTel API로 심고, 데이터는 OTLP라는 공용 프로토콜로 내보내고,
          받는 쪽(Tempo든 Jaeger든 상용 SaaS든)은 <strong>설정 한 줄로</strong> 바꿉니다.
        </p>
        <Code>{CODE_OTEL}</Code>
        <p>
          실무 감각의 핵심은 <strong>자동 계측과 수동 계측의 경계</strong>예요.{' '}
          <strong>자동 계측</strong>은 에이전트가 HTTP 서버/클라이언트·JDBC·Kafka 같은
          표준 라이브러리를 알아서 감싸 주는 것 — 붙이기만 하면 섹션 4의 폭포수가 공짜로
          나옵니다. 반면 "이 스팬이 <strong>어떤 종목의 호가 계산</strong>이었는지" 같은{' '}
          <strong>비즈니스 의미</strong>는 기계가 알 수 없으니, 중요한 도메인 로직에만
          수동 스팬과 속성을 <strong>골라서</strong> 심어요. 전부 수동으로 심겠다는 건
          수공예 낭비고, 전부 자동에 맡기겠다는 건 "무엇을 하던 요청인지" 모르는 반쪽
          트레이스입니다.
        </p>
        <div className="tip">
          <b>우리 스택과의 연결</b> Spring Boot는 <strong>Micrometer</strong>가 이미 내장돼
          있어 메트릭은 Actuator 엔드포인트로 거의 공짜고, 트레이싱은 Micrometer Tracing이나
          OTel 자바 에이전트 중 하나로 붙입니다. Next.js는{' '}
          <span className="icode">instrumentation.ts</span> 훅이 서버 사이드 계측의 표준
          입구예요. 프론트(web)→gateway→auth까지 <strong>같은 trace_id</strong>가 흐르게
          만드는 것이 목표 그림입니다.
        </div>
      </Section>

      <Section n={6} title="대시보드와 알람 설계" sub="좋은 대시보드는 질문에 답하고, 좋은 알람은 새벽에 깨울 가치가 있다">
        <p>
          데이터를 모았으면 이제 <strong>보여 주는 설계</strong>입니다. 나쁜 대시보드는
          "모을 수 있는 그래프를 전부 붙인 벽지"예요. 좋은 대시보드는{' '}
          <strong>질문 하나에 답하는 한 장</strong>입니다. 추천 구성은 서비스마다{' '}
          <strong>한 장짜리 개요 화면</strong>: 맨 윗줄에 RED 세 개(요청률·에러율·p99),
          그 아래 의존 자원의 USE(커넥션풀·CPU·큐), 마지막 줄에 배포 시각 표시. 새벽에
          비상 걸린 사람이 <strong>30초 안에</strong> "정상인가, 어디부터 볼까"를 판단할 수
          있으면 합격이에요.
        </p>
        <p>
          알람은 대시보드보다 훨씬 엄격해야 합니다. 기준은 두 가지 분류에서 나와요 —{' '}
          <strong>증상 기반</strong>(사용자가 지금 아프다: 에러율·지연)과{' '}
          <strong>원인 기반</strong>(자원이 아프다: CPU·디스크). 우선순위는 언제나{' '}
          <strong>증상이 먼저</strong>입니다. CPU가 99%여도 사용자 지연이 없으면 새벽에 깨울
          일이 아니고, CPU가 30%여도 에러율이 5%면 지금 당장 일어나야 하니까요.
        </p>
        <Code>{CODE_ALERT}</Code>
        <div className="warn">
          <b>알람 피로는 기술 부채다</b> "혹시 몰라서" 걸어 둔 알람이 매일 밤 헛울음을 울면,
          팀은 그 채널을 근육처럼 무시하게 됩니다. 그러다 진짜 장애 알람도 같이 묻혀요.
          울렸는데 아무 조치 없이 닫힌 알람은 <strong>버그와 똑같이 취급해서</strong> 임계값을
          고치거나 페이징에서 강등하세요. 알람 목록은 코드처럼 리뷰하고 정리하는 대상입니다.
        </div>
        <div className="tip">
          <b>배포 시각을 그래프에 그려라</b> 장애의 가장 흔한 원인은 방금 한 배포입니다.
          대시보드에 배포 이벤트를 세로선으로 겹쳐 두면, "그 선 직후 p99가 꺾였네"를 누구나
          한눈에 봐요. 섹션 7의 카나리아 판단도 결국 이 그림 하나에서 시작합니다.
        </div>
      </Section>

      <Section n={7} title="관측성으로 미스터리 풀기 — 실전 시나리오" sub="메트릭 → 트레이스 → 로그, 좁혀가는 순서 훈련">
        <p>
          도구를 아는 것과 <strong>수사 순서</strong>가 몸에 밴 건 다릅니다. 실제 티켓
          하나로 훈련해 봅시다 — "특정 유저 몇 명만 대시보드가 느리대요. 재현은 안 돼요."
          print 디버깅이라면 시작조차 못 할 문제죠.
        </p>
        <Code>{CODE_SCENARIO}</Code>
        <p>
          장수명 연결도 특급 과정다운 관측 대상이에요. 우리{' '}
          <span className="icode">MarketStreamService</span>의 SSE 시세 스트림처럼{' '}
          <strong>몇 분씩 살아 있는 연결</strong>은 "요청률·지연" 같은 단발 요청 지표로는
          안 보입니다. 대신 <strong>활성 연결 수 게이지</strong>, <strong>연결 지속시간
          히스토그램</strong>, <strong>종료 사유별 카운터</strong>(정상 종료/타임아웃/에러)를
          봐야 해요. "활성 연결이 주기적으로 뚝 떨어졌다 회복"하는 톱니 모양이 보이면, 중간의
          프록시 타임아웃이나 버퍼링(우리가 <span className="icode">compress: false</span>로
          막아 둔 바로 그 문제)을 의심하는 식입니다.
        </p>
        <p>
          마지막은 <strong>배포 직후 지표 비교</strong> — 카나리아 판단의 데이터 근거예요.
          새 버전을 일부 트래픽에만 열고, 구버전과 신버전의 <strong>같은 지표</strong>
          (에러율·p99·자원 사용)를 나란히 놓습니다. "신버전 p99가 구버전보다 20% 이상 나쁘면
          자동 롤백" 같은 규칙을 걸면, 배포 판정이 사람의 감이 아니라 <strong>숫자의
          일</strong>이 되죠. 관측성이 없으면 카나리아는 그냥 "일부 유저로 도박하기"입니다.
        </p>
        <div className="tip">
          <b>실습 미션</b> 아래 스택을 <span className="icode">platform/</span>에 얹고,
          gateway→auth 경로에 트레이스를 심은 뒤, auth 코드 어딘가에{' '}
          <strong>일부러 200ms 지연</strong>을 숨겨 두세요(짝꿍이 숨기면 더 좋습니다).
          그리고 <strong>코드를 다시 열지 말고</strong> Grafana+Tempo만으로 지연의 위치를
          짚어내면 통과입니다. "트레이스가 코드 리딩을 대체하는 순간"을 한 번 겪고 나면,
          관측성에 드는 품이 왜 남는 장사인지 몸으로 알게 돼요.
        </div>
        <Code>{CODE_COMPOSE}</Code>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>🔭 여기까지 왔다면</h3>
          <p className="muted">
            이제 분산 시스템이 <strong>블랙박스가 아니게</strong> 됐어요. 메트릭으로 이상을
            감지하고(RED·USE·퍼센타일), 트레이스로 위치를 좁히고(스팬·컨텍스트 전파),
            로그로 원인을 확정하는(구조화·trace_id 조인) — 세 기둥을 순서대로 갈아타는
            수사법을 손에 쥐었습니다. 그런데 새벽 3시, 증상 알람이 정말로 울린 그 순간부터는
            다른 종류의 기술이 필요해요. 누가 지휘하고, 무엇부터 복구하고, 끝난 뒤 어떻게
            같은 사고를 막을까요? 다음{' '}
            <Link to="/learn/lv4-sre-incident-response"><strong>SRE와 장애 대응 — 무너져도
            빨리 일어나는 팀</strong></Link> 코스에서, 관측성이 켜 준 불빛 아래 움직이는 법을
            이어서 익혀 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
