<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Psutil on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/ko/tags/psutil/</link><description>Recent content in Psutil on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Mon, 06 Apr 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/ko/tags/psutil/index.xml" rel="self" type="application/rss+xml"/><item><title>Hybrid Image Search 개발기 #9 — OpenTelemetry 분산 추적, Grafana Cloud 연동, traced_span 헬퍼</title><link>https://ice-ice-bear.github.io/ko/posts/2026-04-06-hybrid-search-dev9/</link><pubDate>Mon, 06 Apr 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/ko/posts/2026-04-06-hybrid-search-dev9/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post Hybrid Image Search 개발기 #9 — OpenTelemetry 분산 추적, Grafana Cloud 연동, traced_span 헬퍼" /&gt;&lt;h2 id="개요"&gt;개요
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://ice-ice-bear.github.io/ko/posts/2026-04-03-hybrid-search-dev8/" &gt;이전 글: Hybrid Image Search 개발기 #8&lt;/a&gt;에서 톤/앵글 S3 마이그레이션과 EC2 배포 수정, hex 컬러 추출을 다뤘다. 이번에는 한 발짝 물러서서 관측 가능성(observability)에 집중했다.&lt;/p&gt;
&lt;p&gt;FastAPI 서버에 OpenTelemetry를 도입해 검색 파이프라인과 이미지 생성 파이프라인의 각 단계를 스팬으로 추적하고, EC2에 Grafana Alloy를 설치해 트레이스를 Grafana Cloud로 전송하는 구성을 완성했다. 이틀에 걸친 작업이었는데, 1일차의 깔끔한 구현과 2일차의 현실적인 디버깅이 극명하게 대비되었다.&lt;/p&gt;
&lt;h2 id="아키텍처--트레이스-수집-경로"&gt;아키텍처 — 트레이스 수집 경로
&lt;/h2&gt;&lt;p&gt;아래 다이어그램은 트레이스가 앱에서 Grafana Cloud까지 전달되는 경로를 보여준다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 A["FastAPI 앱 &amp;lt;br/&amp;gt; (OTel SDK)"] --&gt;|OTLP HTTP| B["Grafana Alloy &amp;lt;br/&amp;gt; (localhost:4318)"]
 B --&gt;|OTLP HTTP| C["Grafana Cloud &amp;lt;br/&amp;gt; Tempo"]
 C --&gt; D["Grafana UI &amp;lt;br/&amp;gt; 트레이스 조회"]
 A --&gt;|스팬 생성| E["traced_span &amp;lt;br/&amp;gt; CPU/메모리 메트릭"]
 E --&gt; A&lt;/pre&gt;&lt;p&gt;핵심 구성요소는 세 가지다. 앱 내부의 OTel SDK가 스팬을 생성하고, EC2 로컬의 Grafana Alloy가 OTLP 수신 후 배치 처리하며, Grafana Cloud Tempo가 최종 저장 및 조회를 담당한다.&lt;/p&gt;
&lt;h2 id="1일차--깔끔한-초기-구현"&gt;1일차 — 깔끔한 초기 구현
&lt;/h2&gt;&lt;p&gt;첫날은 순조로웠다. OpenTelemetry 패키지를 추가하고, 텔레메트리 모듈을 만들고, 앱 라이프사이클에 연결하고, 두 파이프라인에 스팬을 삽입했다.&lt;/p&gt;
&lt;h3 id="텔레메트리-모듈-구조"&gt;텔레메트리 모듈 구조
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# telemetry.py — Provider를 임포트 시점에 설정&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;_resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Resource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;service.name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;hybrid-image-search&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;deployment.environment&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_environment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;_provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TracerProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;_exporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OTLPSpanExporter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;_endpoint&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/traces&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;_provider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_span_processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SimpleSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_exporter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_tracer_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;hybrid-image-search&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;TracerProvider&lt;/code&gt;를 모듈 수준에서 설정하는 이유는, uvicorn이 ASGI 앱을 바인딩하기 전에 provider가 준비되어야 &lt;code&gt;FastAPIInstrumentor&lt;/code&gt;가 올바른 provider를 참조하기 때문이다.&lt;/p&gt;
&lt;h3 id="파이프라인-스팬-삽입"&gt;파이프라인 스팬 삽입
&lt;/h3&gt;&lt;p&gt;검색 파이프라인에는 임베딩 생성, 벡터 검색, 재랭킹 등 단계별로 스팬을 삽입했다. 생성 파이프라인에는 레퍼런스 주입(&lt;code&gt;generation.injection&lt;/code&gt;), 프롬프트 빌드(&lt;code&gt;generation.prompt_build&lt;/code&gt;), Gemini API 호출(&lt;code&gt;generation.gemini_api&lt;/code&gt;) 스팬을 추가했다.&lt;/p&gt;
&lt;p&gt;DB 인덱스도 함께 추가했다. 검색과 생성 쿼리 성능이 트레이스에서 병목으로 보이기 전에 미리 정리한 것이다.&lt;/p&gt;
&lt;h2 id="2일차--현실과의-충돌"&gt;2일차 — 현실과의 충돌
&lt;/h2&gt;&lt;p&gt;EC2에 Grafana Alloy를 설치하고 Grafana Cloud 연동을 설정한 후, 트레이스가 전혀 나타나지 않았다. 여기서부터 연속 6개의 fix 커밋이 이어졌다.&lt;/p&gt;
&lt;h3 id="문제-1-tracerprovider-초기화-시점"&gt;문제 1: TracerProvider 초기화 시점
&lt;/h3&gt;&lt;p&gt;uvicorn이 앱을 로드하는 시점에 &lt;code&gt;TracerProvider&lt;/code&gt;가 아직 설정되지 않아 &lt;code&gt;FastAPIInstrumentor&lt;/code&gt;가 기본(no-op) provider를 참조했다. 해결: 모듈 임포트 시점에 provider를 설정하도록 변경.&lt;/p&gt;
&lt;h3 id="문제-2-batchspanprocessor의-비동기-플러시"&gt;문제 2: BatchSpanProcessor의 비동기 플러시
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;uv run&lt;/code&gt;으로 실행하면 프로세스가 빠르게 종료되면서 &lt;code&gt;BatchSpanProcessor&lt;/code&gt;의 백그라운드 스레드가 스팬을 내보내기 전에 죽었다. 해결: &lt;code&gt;SimpleSpanProcessor&lt;/code&gt;로 교체해 스팬 생성 즉시 동기적으로 전송.&lt;/p&gt;
&lt;h3 id="문제-3-grpc-exporter-침묵"&gt;문제 3: gRPC exporter 침묵
&lt;/h3&gt;&lt;p&gt;gRPC exporter가 연결 실패를 로그 없이 삼키고 있었다. 해결: OTLP HTTP exporter로 전환. HTTP는 디버깅이 용이하고 Alloy의 기본 HTTP 엔드포인트(4318)와 바로 호환된다.&lt;/p&gt;
&lt;h3 id="문제-4-텔레메트리-초기화-크래시"&gt;문제 4: 텔레메트리 초기화 크래시
&lt;/h3&gt;&lt;p&gt;OTel 초기화 중 예외가 발생하면 앱 전체가 죽었다. 해결: &lt;code&gt;try/except&lt;/code&gt;로 감싸서 텔레메트리 실패가 앱 가동을 막지 않도록 변경.&lt;/p&gt;
&lt;h3 id="문제-5-fastapiinstrumentor-provider-누락"&gt;문제 5: FastAPIInstrumentor provider 누락
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;FastAPIInstrumentor().instrument()&lt;/code&gt;가 글로벌 provider를 자동으로 찾지 못하는 경우가 있었다. 해결: &lt;code&gt;tracer_provider&lt;/code&gt;를 명시적으로 전달.&lt;/p&gt;
&lt;h3 id="문제-6-모듈-임포트-순서"&gt;문제 6: 모듈 임포트 순서
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;main.py&lt;/code&gt;에서 &lt;code&gt;app = FastAPI()&lt;/code&gt;와 instrumentation 호출의 순서 문제. 해결: &lt;code&gt;FastAPIInstrumentor&lt;/code&gt;를 모듈 수준에서 &lt;code&gt;app&lt;/code&gt; 생성 직후에 호출.&lt;/p&gt;
&lt;h2 id="grafana-alloy-구성"&gt;Grafana Alloy 구성
&lt;/h2&gt;&lt;p&gt;EC2에 배포한 Alloy 설정은 간결하다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;otelcol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;receiver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;otlp&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;default&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;grpc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;127.0.0.1:4317&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;127.0.0.1:4318&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;traces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;otelcol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;otelcol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;default&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;5s&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;traces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;otelcol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;otlphttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grafana_cloud&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;otelcol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;otlphttp&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;grafana_cloud&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;GRAFANA_OTLP_ENDPOINT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;otelcol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grafana_cloud&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;otelcol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basic&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;grafana_cloud&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;GRAFANA_INSTANCE_ID&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;GRAFANA_API_TOKEN&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;앱은 localhost:4318로 OTLP HTTP를 보내고, Alloy가 5초 배치로 묶어 Grafana Cloud Tempo로 전송한다. 인증 정보는 환경 변수로 관리한다.&lt;/p&gt;
&lt;h2 id="traced_span--cpu메모리-메트릭-자동-수집"&gt;traced_span — CPU/메모리 메트릭 자동 수집
&lt;/h2&gt;&lt;p&gt;마지막으로 &lt;code&gt;traced_span&lt;/code&gt; 컨텍스트 매니저를 만들어, 스팬 전후의 CPU 시간과 메모리 사용량을 자동으로 측정해 스팬 속성에 기록하도록 했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;traced_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;CPU/메모리 측정이 포함된 스팬 생성.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;mem_before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_info&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rss&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cpu_before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu_times&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;mem_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_info&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rss&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cpu_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu_times&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;process.memory_mb&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mem_after&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;process.memory_delta_kb&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;mem_after&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mem_before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;process.cpu_user_ms&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;cpu_after&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cpu_before&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;process.cpu_system_ms&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;cpu_after&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cpu_before&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;psutil.Process&lt;/code&gt;로 현재 프로세스의 RSS 메모리와 CPU user/system 시간을 스팬 시작/종료 시점에 측정한다. 이를 통해 Grafana에서 각 파이프라인 단계가 소비하는 리소스를 개별적으로 확인할 수 있다. 검색 파이프라인과 생성 파이프라인 모두 &lt;code&gt;traced_span&lt;/code&gt;으로 교체 완료했다.&lt;/p&gt;
&lt;h2 id="커밋-로그"&gt;커밋 로그
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;메시지&lt;/th&gt;
 &lt;th&gt;변경 파일&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;feat: add no-text directive for injected refs and remove color palettes&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;prompt.py&lt;/code&gt;, &lt;code&gt;App.tsx&lt;/code&gt;, &lt;code&gt;GeneratedImageDetail.tsx&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;deps: add OpenTelemetry packages for observability&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;requirements.txt&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat: add telemetry module with OpenTelemetry init and tracer&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;telemetry.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat: wire OpenTelemetry init into app lifespan&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;main.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat: add OpenTelemetry spans to search pipeline stages&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;search.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat: add OpenTelemetry spans to generation pipeline&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;generation.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;add indices&lt;/td&gt;
 &lt;td&gt;DB migration&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;infra: add Grafana Alloy config and EC2 setup guide&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;infra/alloy/config.alloy&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;deps: move OpenTelemetry packages to pyproject.toml&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;pyproject.toml&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix: set OTel TracerProvider at import time&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;telemetry.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix: use SimpleSpanProcessor for reliable export under uv run&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;telemetry.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix: switch to OTLP HTTP exporter for reliable trace delivery&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;telemetry.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix: add error handling for telemetry init&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;telemetry.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix: pass tracer_provider explicitly to FastAPIInstrumentor&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;main.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix: move FastAPI instrumentation to module level in main.py&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;main.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat: add traced_span helper with CPU/memory resource metrics&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;telemetry.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat: use traced_span for CPU/memory metrics in search and generation pipelines&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;search.py&lt;/code&gt;, &lt;code&gt;generation.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="인사이트"&gt;인사이트
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;OTel 초기화는 반드시 임포트 시점에 완료하라.&lt;/strong&gt; uvicorn 같은 ASGI 서버는 앱 모듈을 임포트한 직후 라우터와 미들웨어를 바인딩한다. &lt;code&gt;FastAPIInstrumentor&lt;/code&gt;가 이 시점에 유효한 &lt;code&gt;TracerProvider&lt;/code&gt;를 찾지 못하면 no-op tracer를 캐시해 버리고, 이후 아무리 provider를 설정해도 계측이 동작하지 않는다. 모듈 최상단에서 provider를 설정하는 패턴이 이 문제를 근본적으로 방지한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BatchSpanProcessor는 장수(long-lived) 프로세스 전용이다.&lt;/strong&gt; &lt;code&gt;uv run&lt;/code&gt;이나 테스트처럼 프로세스가 빠르게 종료되는 환경에서는 백그라운드 플러시 스레드가 작동할 틈이 없다. &lt;code&gt;SimpleSpanProcessor&lt;/code&gt;는 성능 대비 안정성 트레이드오프가 명확하지만, 개발/소규모 프로덕션에서는 합리적인 선택이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;gRPC보다 HTTP를 먼저 시도하라.&lt;/strong&gt; OTLP gRPC exporter는 연결 실패를 조용히 처리해 디버깅을 어렵게 만든다. HTTP exporter는 상태 코드와 에러 메시지를 명확히 반환하므로, 새 인프라를 연결할 때 HTTP로 먼저 동작을 확인한 뒤 필요시 gRPC로 전환하는 것이 효율적이다.&lt;/p&gt;</description></item></channel></rss>