| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 |
- dau 3만명
- JPA
- 추천 검색 기능
- 크롤링
- 카카오
- docker
- 백준
- ipo 매매자동화
- 셀러리
- 누적합
- BFS
- 프로그래머스
- 완전탐색
- TLS협상오류
- 몽고 인덱스
- 502오류
- 관측가능성
- 구현
- 베타적락
- AWS
- 알람시스템
- 결제서비스
- 아키텍쳐 개선
- next-stock
- ALB 502 BadGateway
- 백그라운드시 연결안됨
- gRPC
- 이분탐색
- 디버깅
- 쿠키
- Today
- Total
코딩관계론
Lambda 로그 누락 문제로 본 서버 전 영역 가시성 확보 방법 본문
최근 프로젝트에서 Kubernetes 환경에서 동시성을 극대화하기 위해 AWS Lambda를 도입했습니다. 하지만 Lambda로 분리된 워크로드 때문에 로그가 누락되면서 서버 전 구간의 추적성이 떨어지는 문제가 발생했습니다. 처음에는 오픈텔레메트리 프로토콜을 사용해서 간단히 traceparent 헤더만 전달하면 될 것으로 생각했지만, 실제로는 Lambda의 serverless-express에서는 AsyncLocalStorage 전파가 끊기게 되면서 전구간으로 이어지지 않는 문제가 발생했습니다.
이 글에서는 이러한 문제를 해결하기 위해 제가 시도했던 방법과 그 과정에서 얻은 인사이트를 공유합니다.
OpenTelemetry instrumentation을 활용해 trace ID를 로그에 삽입하는 방법과, Lambda 환경에서 이를 전달하여 전체 호출 체인을 연결하는 과정을 자세히 다룹니다.
마지막으로 CloudWatch 로그를 Kibana 대시보드까지 연결해 전체 가시성을 확보한 경험을 정리합니다. 개발자로서 동시성 최적화를 고민하면서도 가시성을 확보하는 과정에서 느꼈던 시행착오와 해결 과정을 통해, 로그 추적성과 모니터링의 중요성을 다시 한 번 깨달았습니다. 이 경험이 비슷한 문제를 겪고 있는 분들께 도움이 되길 바랍니다.
1. Instrumentation은 어떻게 실행될까?
Instrumentation은 모든 서비스에서 trace ID를 복원하고 전파하기 위해 async_hooks와 AsyncLocalStorage라는 Node.js의 비동기 컨텍스트 관리 기술을 사용합니다. 이 과정을 정확히 이해해야 왜 Lambda + serverless-express 환경에서는 자동 계측이 동작하지 않는지를 알 수 있습니다.
AsyncLocalStorage의 역할
간단히 말해 AsyncLocalStorage는 Node.js에서 “비동기 호출 체인별 thread-local storage” 역할을 합니다.
내부적으로는 저수준 API인 async_hooks를 이용해 각 비동기 리소스에 부여된 asyncId와 그 부모 관계(triggerAsyncId)를 추적하고, 이 관계를 기반으로 비동기 호출 전체에 걸쳐 하나의 store 객체를 자동 전파합니다. 이 덕분에 비동기 함수나 Promise 체인을 오가더라도 하나의 요청 단위로 trace ID를 잃지 않고 유지할 수 있습니다. (더 자세히 알고 싶다면 이 블로그 글을 참고하세요)

Instrumentation의 동작 방식
Instrumentation을 코드에 한 줄만 추가해도 자동으로 필요한 후킹(Hooking) 과정을 수행해 우리가 별도의 코드를 작성하지 않아도 됩니다. 이때 핵심이 되는 기술이 바로 몽키 패칭(Monkey Patching) 입니다. Instrumentation의 enable() 함수를 살펴보면, 이 함수가 실행될 때 각 JS 모듈에 후크(Hook) 를 등록합니다. 즉, 특정 모듈이 로드될 때마다 Instrumentation이 먼저 실행되어 OTel이 필요한 로직(컨텍스트 생성, 스팬 연결 등)을 삽입할 수 있도록 만듭니다.

패칭 코드 예시
// node_modules/@opentelemetry/instrumentation-http/build/src/http.js 일부
const http = require('http');
const shimmer = require('shimmer');
shimmer.wrap(http, 'createServer', function(original) {
return function patchedCreateServer(requestListener) {
const server = original.apply(this, arguments);
shimmer.wrap(server, 'emit', function(originalEmit) {
return function patchedEmit(event, req, res) {
if (event === 'request') {
// 👇 ALS.run() 혹은 context.with()로 실행 컨텍스트 진입
return contextManager.with(spanContext, () => {
return originalEmit.apply(this, arguments);
});
}
return originalEmit.apply(this, arguments);
};
});
return server;
};
});
패치 이후의 실행 단계 (Execution Flow)
패치가 완료되면 실제 요청이 들어올 때 아래 단계가 실행됩니다.
- HTTP Request 수신 → HttpRequest 객체가 생성되고 WrapperOtel이 이를 가로챕니다.
- traceparent 검사 → 요청 헤더에 traceparent가 존재하면 기존 trace ID를 재사용하고,
없으면 새로운 trace ID를 생성합니다. - Span 생성 및 전달 → 새 Span을 App.Handler() 로 전달하여 애플리케이션 로직이 실행됩니다.
- 로그 연결 → Lambda 내부 로그도 동일한 trace ID로 연결되어 CloudWatch 및 Kibana에서
E2E 호출 경로를 시각적으로 추적할 수 있습니다.
패치 단계는 서버가 기동될 때 단 한 번만 실행됩니다.
이후부터는 모든 요청이 WrapperOtel을 거쳐 자동으로 trace 정보를 전파합니다.

이렇게 되면 내부적으로 Node.js 서버에서 자동으로 traceId가 추적되는 것을 확인할 수 있습니다. 하지만 serverless-express 패턴에서는 왜 불가능해질까요?
2. aws-serverless-express와의 차이점
기존 Node.js와의 차이점
우리가 위에서 봤듯이 Instrumentation는 createServer와 emit을 패칭하여 traceId를 전 구간으로 전파하게 됩니다 하지만
aws-serverless-express는 http.Server를 '호스팅(hosting)'하는 것이 아니라 '시뮬레이션(simulation)'하는 것입니다.
- 람다에는 네트워크 포트를 열고(server.listen()) HTTP 요청을 수신 대기하는 실제 http.Server가 존재하지 않습니다.
- 대신 serverless-express는 람다 event JSON을 입력받아, http.IncomingMessage (req)와 http.ServerResponse (res) 객체를 수동으로 new 키워드를 사용해 생성합니다.
- 그리고 event.headers, event.body 등의 데이터를 이 '가짜' req 객체에 주입(populate)합니다.
따라서 전통적인 http.Server가 존재하지 않아 http.createServer를 후크하는 것이 불가능하여, AsyncLocalStorage에 trace 정보가 저장되지 않습니다.
계측 연결 방법
Lambda 환경에서는 serverless-express가 ExpressInstrumentation의 패칭 과정이 작동하지 않기에 미들웨어를 사용해서 수동 복원을 구성했습니다.
미들웨어를 사용한 복원
Lambda 환경에서는 serverless‑express 구조 때문에 OpenTelemetry의 http.createServer 패칭이 실행되지 않아 자동 계측을 통한 컨텍스트 전파가 실패합니다. 이를 해결하기 위해 요청 처리 체인 초반에 컨텍스트를 직접 복원하는 미들웨어를 추가해야 합니다. OpenTelemetry 문서에서 말하는 컨텍스트 전파는 분산된 서비스 간에 신호(트레이스 등)를 연관시키는 기술로, HTTP 헤더의 traceparent 값을 복원해 새로운 AsyncLocalStorage 컨텍스트를 시작하면 이러한 전파가 가능합니다.
const { context, propagation } = require('@opentelemetry/api');
function otelContextMiddleware(req, res, next) {
// 헤더에서 traceparent 등을 추출
const ctx = propagation.extract(context.active(), req.headers);
// 추출한 컨텍스트로 실행 컨텍스트를 전환
context.with(ctx, () => {
next();
});
}
이 미들웨어를 Express 애플리케이션에 등록하면, Lambda에서 호출될 때마다 요청 헤더에서 추적 정보를 읽어오고, context.with()로 새로운 컨텍스트를 시작하여 이후 서비스 로직에서 context.active() 호출 시 동일한 trace 정보가 유지됩니다. 따라서 serverless‑express 환경에서도 traceId를 전파하고 분산 추적을 이어갈 수 있습니다.
'개발' 카테고리의 다른 글
| Loadbalancer로 부터 온 502 Bad gateway (0) | 2025.10.19 |
|---|---|
| MTTR 단축을 위한 관측 가능성 시스템 개선 여정 (1) | 2025.08.10 |
| 20만명 이벤트와 서버 다운 (0) | 2025.07.10 |
| DAU 3만명 이 구조로는 못 버텨요… 내가 직접 설계한 비동기 설문 시스템 (1) | 2025.06.22 |
| 일평균 700건 이상의 요청에서 "세션 점유 중" 오류를 10건 이하로 (0) | 2025.06.16 |