11 KiB
title, source_type, status, confidence, tags, related_projects, last_reviewed, canonical_sources, audience, target_publish, status_label
| title | source_type | status | confidence | tags | related_projects | last_reviewed | canonical_sources | audience | target_publish | status_label | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Observability를 로그 한 줄이 아니라 운영 계약으로 보기 | blog | verified | high |
|
|
2026-07-03 |
|
backend-engineer | ready |
Observability를 로그 한 줄이 아니라 운영 계약으로 보기
Parent / 부모 (필수)
- 핵심 canonical: wiki/projects/ca-tmpl/observability-log-metric-trace-runbook
- 관련 개념 문서: wiki/concepts/observability-log-metric-trace-runbook - 일반 observability 개념. 현재 이 글의 구현 사실 근거는 verified project canonical에 둔다.
타깃 독자 / Target reader
- 독자 profile: skeleton에 log/metric/trace/runbook baseline을 넣고 싶은 백엔드 엔지니어.
- 이미 안다고 가정하는 것: MDC, Micrometer, trace id, runbook.
- 처음 듣는다고 가정하는 것: 관측 가능성을 응답
meta, 로그 MDC, trace context, runbook의 연결 계약으로 보는 관점.
도입 / Hook
로그가 많다고 장애 대응이 쉬워지는 것은 아닙니다. 요청 id가 응답에는 있는데 로그에는 없거나, 로그에는 trace id가 있는데 client가 받은 error envelope에는 없거나, metric alert는 울렸는데 어떤 runbook을 봐야 하는지 연결되지 않으면 관측 데이터는 흩어진 조각이 됩니다.
ca-tmpl은 observability를 “로그 한 줄 찍기”가 아니라 연결 가능한 계약으로 다루려 했습니다. request id, trace id, correlation id를 MDC에 올리고, 응답 envelope의 meta에도 투영하며, inbound header는 sanitize하고, user principal은 pseudonymize해서 로그에 싣는 식입니다. 다만 project canonical 기준으로 전체 log/metric/trace/runbook 체계가 모두 구현된 것은 아닙니다. 이 글은 구현된 foundation slice와 아직 planned/documented-only인 부분을 나눠서 정리합니다.
본문 outline / Body outline
- observability는 출력 포맷이 아니라 join 가능성이다.
- requestId/traceId/correlationId와 envelope meta.
- MDC key, header sanitizer, request logging filter.
- logback JSON/MDC include와 masking/sampling의 구현 범위.
- metric/trace/runbook 중 구현된 범위와 planned 범위.
- 운영 장애 대응 효과와 alert tuning 검증은 없음.
본문 / Body
장애 상황에서 가장 먼저 필요한 것은 “이 응답이 어떤 로그와 이어지는가”입니다. client가 받은 실패 응답에 requestId와 traceId가 있어도, 서버 로그에 같은 key가 없으면 검색이 끊깁니다. 반대로 로그에만 trace id가 있고 응답에는 없으면 client 문의에서 출발해 서버 이벤트로 들어가기 어렵습니다. ca-tmpl의 observability foundation은 이 연결을 기본 계약으로 둡니다.
구현의 중심에는 MDC key가 있습니다. MdcKeys는 request_id, trace_id, span_id, correlation_id, user_principal을 snake_case로 정의합니다. RequestLoggingFilter는 inbound X-Request-Id, X-Correlation-Id, traceparent를 읽고, 없거나 유효하지 않으면 서버에서 생성합니다. 값은 MDC에 들어가고 response header에도 다시 설정됩니다. 그래서 request 처리 중 남는 로그와 client가 받은 header가 같은 id로 이어질 수 있습니다.
ResponseMetaFactory는 이 MDC 값을 API envelope의 meta로 투영합니다. 로그에서는 snake_case key를 쓰지만, JSON wire format은 requestId, traceId, correlationId camelCase record입니다. 이 작은 변환이 중요합니다. 로그의 key naming과 API contract naming을 억지로 같게 만들지 않고, 각 영역의 규칙을 유지한 채 mapping 지점을 명확히 둔 것입니다.
header는 그대로 믿지 않습니다. HeaderSanitizer는 inbound header value에서 \r, \n, ASCII control char를 제거하고 길이를 제한합니다. request id나 correlation id는 로그/MDC에 들어가므로 log injection을 피해야 합니다. RequestLoggingFilter는 user principal도 raw id를 MDC에 넣지 않고 UserPrincipalPseudonymizerPort를 거쳐 pseudonymized value만 싣습니다. 즉 “관측 가능하게 남긴다”와 “민감 정보를 그대로 남긴다”를 구분합니다.
logback 설정도 이 계약을 받쳐줍니다. local/dev는 사람이 읽기 쉬운 pattern layout을 쓰고, 그 외 profile은 structured JSON encoder에 MDC key를 포함합니다. 설정에는 trace_id, span_id, request_id, correlation_id, user_principal include가 명시되어 있습니다. 또한 masking converter/decorator와 sampling turbo filter, async appender 설정도 존재합니다. 다만 이 글에서 말할 수 있는 것은 코드와 local verification 범위입니다. production log pipeline에서의 실제 누락률이나 비용 절감 효과는 검증된 주장이 아닙니다.
traceparent 처리도 선을 분명히 해야 합니다. 현재 RequestLoggingFilter는 inbound W3C traceparent가 유효하면 채택하고, 없으면 fresh ROOT traceparent를 생성합니다. 이것은 trace id를 응답/log에 연결하기 위한 foundation입니다. 하지만 실제 distributed tracer가 붙어 span tree를 export하고, 5xx span을 ERROR로 기록하고, trace backend에서 검색된다는 주장까지는 별도 구현/운영 검증이 필요합니다.
metric과 runbook도 마찬가지입니다. project canonical에는 log/metric/trace/runbook을 하나의 운영 계약으로 보는 방향이 있지만, 모든 항목이 같은 증거 등급은 아닙니다. 구현된 foundation은 MDC/header/meta/logback 중심입니다. Prometheus alert, alert tuning, runbook link-check와 실제 incident response 효과는 documented-only 또는 planned 범위로 남아 있습니다. 따라서 이 글의 결론은 “운영 관측성이 완성됐다”가 아니라 “ca-tmpl은 응답 meta와 로그 context를 연결하는 관측성 foundation을 구현했다”입니다.
코드 예제 / Code samples (있다면)
// 출처: [[wiki/projects/ca-tmpl/observability-log-metric-trace-runbook]]
// 실제 파일: adapter-web/.../MdcKeys.java, ca-tmpl @f6fbd4e196b4
public final class MdcKeys {
public static final String REQUEST_ID = "request_id";
public static final String TRACE_ID = "trace_id";
public static final String SPAN_ID = "span_id";
public static final String CORRELATION_ID = "correlation_id";
public static final String USER_PRINCIPAL = "user_principal";
}
// 출처: [[wiki/projects/ca-tmpl/observability-log-metric-trace-runbook]]
// 실제 파일: adapter-web/.../RequestLoggingFilter.java, ca-tmpl @f6fbd4e196b4
String requestId = resolveOrGenerate(req.getHeader("X-Request-Id"));
String correlationId = resolveOrGenerate(req.getHeader("X-Correlation-Id"));
res.setHeader("X-Request-Id", requestId);
res.setHeader("X-Correlation-Id", correlationId);
MDC.put(MdcKeys.REQUEST_ID, requestId);
MDC.put(MdcKeys.CORRELATION_ID, correlationId);
TraceParent traceParent = resolveOrGenerateTraceParent(req.getHeader("traceparent"));
MDC.put(MdcKeys.TRACE_ID, traceParent.traceId());
MDC.put(MdcKeys.SPAN_ID, traceParent.spanId());
res.setHeader("traceparent", traceParent.toHeader());
// 출처: [[wiki/projects/ca-tmpl/observability-log-metric-trace-runbook]]
// 실제 파일: adapter-web/.../HeaderSanitizer.java, ca-tmpl @f6fbd4e196b4
public static String sanitize(String raw, int maxLength) {
if (raw == null) {
return null;
}
StringBuilder sb = new StringBuilder(Math.min(raw.length(), maxLength));
for (int i = 0; i < raw.length() && sb.length() < maxLength; i++) {
char c = raw.charAt(i);
if (c >= 0x20) {
sb.append(c);
}
}
return sb.toString();
}
// 출처: [[wiki/projects/ca-tmpl/observability-log-metric-trace-runbook]]
// 실제 파일: adapter-web/.../ResponseMetaFactory.java, ca-tmpl @f6fbd4e196b4
public static ResponseMeta fromMdc() {
return new ResponseMeta(
MDC.get(MdcKeys.REQUEST_ID), MDC.get(MdcKeys.TRACE_ID), MDC.get(MdcKeys.CORRELATION_ID));
}
<!-- 출처: [[wiki/projects/ca-tmpl/observability-log-metric-trace-runbook]] -->
<!-- 실제 파일: app-bootstrap/src/main/resources/logback-spring.xml, ca-tmpl @f6fbd4e196b4 -->
<includeMdcKeyName>trace_id</includeMdcKeyName>
<includeMdcKeyName>span_id</includeMdcKeyName>
<includeMdcKeyName>request_id</includeMdcKeyName>
<includeMdcKeyName>correlation_id</includeMdcKeyName>
<includeMdcKeyName>user_principal</includeMdcKeyName>
Sources / 근거 (canonical 인용 필수, derived layer 의무)
- wiki/projects/ca-tmpl/observability-log-metric-trace-runbook - 이 글의 1차 canonical. MDC/header/meta/logback foundation, local verification, planned/documented-only 항목 경계를 따른다.
- wiki/concepts/observability-log-metric-trace-runbook - 관련 개념 문서. 로그, metric, trace, runbook의 일반 개념 배경으로만 둔다.
사실 vs 의견 / Fact vs opinion 구분
- 사실: ca-tmpl에는
MdcKeys,HeaderSanitizer,RequestLoggingFilter,ResponseMetaFactory,ResponseMeta,logback-spring.xmlMDC include 설정이 존재한다. 근거: wiki/projects/ca-tmpl/observability-log-metric-trace-runbook - 사실: inbound request/correlation id sanitize, traceparent 채택/생성, response header 설정, envelope meta projection은 구현된 foundation 범위다. 근거: wiki/projects/ca-tmpl/observability-log-metric-trace-runbook
- 사실: production alert tuning, 실제 incident response 효과, trace backend export 검증, runbook 운영 검증은 project canonical 기준으로 구현/운영 검증 범위가 아니다. 근거: wiki/projects/ca-tmpl/observability-log-metric-trace-runbook
- 의견: observability를 “데이터 출력”보다 “서로 join 가능한 운영 계약”으로 설명하면 skeleton 설계 의도가 더 잘 드러난다.
- 알지 못하는 것: 실제 alert fatigue, log volume/cost 변화, production trace 검색 성공률.
답할 수 있는 범위 / Answer boundary
- 자신 있게 답할 수 있는 후속 질문:
- response meta와 log context를 왜 연결하는가?
- snake_case MDC와 camelCase JSON meta를 왜 분리하는가?
- inbound header sanitize와 principal pseudonymization은 어떤 위험을 줄이는가?
- 다음 글로 넘길 부분:
- production alert threshold.
- OpenTelemetry exporter와 trace backend 운영.
- runbook link-check와 incident review 결과.
게시 체크리스트 / Publish checklist
- 모든 사실 주장에 canonical 링크 있음
- 사실 vs 의견 분리 명시됨
- 금지 마케팅 표현 없음
- 코드 예제 출처 명시
- 타깃 독자 가정과 톤 일치
/lint통과- 게시 URL 기록 (게시 후):