<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Observability · 인재덕</title>
    <link>https://jared.lynskey.co.nz/ko/tags/observability/</link>
    <description>소프트웨어 엔지니어링과 개발 조직 리더십에 대한 이야기. 성과 리뷰, 채용과 팀 확장, CI/CD, 데브옵스 모니터링, 에이전틱 AI. 서울에서 씁니다.</description>
    <generator>Hugo</generator>
    <language>ko</language>
    <copyright>© 2026 인재덕</copyright>
    <lastBuildDate>Sat, 07 Nov 2026 00:00:00 +1300</lastBuildDate>
    <atom:link href="https://jared.lynskey.co.nz/ko/tags/observability/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Fly.io 위 Django 모니터링: 남긴 DevOps 도구와 버린 도구</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2026/2026-10-22-monitoring-tools-two-products/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2026/2026-10-22-monitoring-tools-two-products/</guid>
      <pubDate>Thu, 22 Oct 2026 00:00:00 +1300</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>DevOps</category>
      <category>Monitoring</category>
      <category>Observability</category>
      <category>Site Reliability</category>
      <category>Fly.io</category>
      <category>Django</category>
      <category>Cloudflare</category>
      <category>Sentry</category>
      <category>Crash Reporting</category>
      <category>Small Teams</category>
      <category>Tech Stack</category>
      <description>Fly.io 위의 작은 Django 앱 두 개를 한 달 2달러 정도로 모니터링하는 법. 에러는 채팅으로, 로그는 S3에 두고 DuckDB로 조회, Fly 메트릭, 크래시 리포트, 그리고 버린 것들.</description>
      <content:encoded><![CDATA[<p>제가 만드는 마켓플레이스 겸 커뮤니티 앱과 현장 기술자용 견적 앱, 이렇게 작은 프로덕트 두 개를 둘 다 Fly.io의 가장 싼 머신에서 돌리고 있습니다. 제 DevOps 모니터링은 결국 다섯 가지로 정리됩니다. ERROR 로그를 올리는 채팅 채널, 오브젝트 스토리지에 쌓아 두고 DuckDB로 조회하는 전체 로그, Fly에 기본으로 딸린 Prometheus 메트릭, Sentry의 오픈소스 SDK로 받아 제 API로 보내는 앱 크래시 리포트, 그리고 매일 아침 올라오는 요약 글 하나입니다.</p>
<p>호스팅 APM도, 프로덕션 트레이싱도, 일부러 기억해서 열어야 하는 대시보드도 없습니다. 따로 돈이 드는 건 로그 아카이브 하나로, 한 달에 2달러 정도입니다. 아래는 전체 목록과, 버렸거나 더는 믿지 않게 된 것들입니다. 그중 하나 때문에 저는 사용자가 한 명도 겪지 않은 장애를 며칠이나 쫓아다녔습니다.</p>

<h2 class="relative group">여기서 말하는 모니터링, 그리고 이게 얼마나 작은 규모인지
    <div id="여기서-말하는-모니터링-그리고-이게-얼마나-작은-규모인지" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%97%ac%ea%b8%b0%ec%84%9c-%eb%a7%90%ed%95%98%eb%8a%94-%eb%aa%a8%eb%8b%88%ed%84%b0%eb%a7%81-%ea%b7%b8%eb%a6%ac%ea%b3%a0-%ec%9d%b4%ea%b2%8c-%ec%96%bc%eb%a7%88%eb%82%98-%ec%9e%91%ec%9d%80-%ea%b7%9c%eb%aa%a8%ec%9d%b8%ec%a7%80" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>제가 말하는 DevOps 모니터링은 두 가지입니다. 누가 알려주기 전에 뭔가 고장났다는 걸 먼저 아는 것, 그리고 짐작하지 않고 원인을 찾아낼 수 있는 것. 아래 도구는 전부 이 두 기준으로 평가했습니다.</p>
<p>작년에 <a href="https://jared.lynskey.co.nz/ko/posts/2025/2025-10-13-devops-monitoring/" >개발자가 진짜로 쓰는 모니터링 만들기</a>라는 글을 썼는데, 그때는 주로 팀을 염두에 뒀습니다. 작은 팀이라면 클라우드 업체가 기본으로 주는 모니터링, 서비스가 수백 개라면 호스팅 APM, 이런 식이었죠. 이번 글은 같은 생각의 가장 작은 쪽 끝입니다. 두 프로덕트 모두 <a href="https://jared.lynskey.co.nz/ko/posts/2026/2026-07-22-my-current-stack/" >요즘 쓰는 기술 스택</a>에서 설명한 대로 Fly의 shared-cpu-1x 머신 위의 Django이고, 개발자는 둘 다 저 혼자입니다. 모니터링이 하나의 프로덕트이고 개발자가 그 사용자라면, 제 모니터링의 사용자는 딱 한 명인 셈이죠. 9월에 Fly 메트릭으로 기준치를 뽑아 봤더니 마켓플레이스 API의 평균 CPU 사용률이 1%였습니다. 엔터프라이즈 스택이 아니고, 그런 척할 생각도 없습니다.</p>
<p>메트릭, 로그, 트레이스라는 세 기둥 중 제게 있는 건 둘입니다. 두 프로덕트 모두 분산 트레이싱이 없습니다. 각각 Django 코드베이스 하나에 데이터베이스 하나라서, 트레이스가 넘나들 서비스 경계가 없기 때문입니다. 제가 보는 트레이스는 <a href="https://jared.lynskey.co.nz/ko/posts/2026/2026-10-01-cloudflare-warp-dev-network/" >제 워크스테이션</a>에서 돌리는 리버스 프록시의 트레이스가 전부입니다.</p>

<h2 class="relative group">목록
    <div id="목록" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%aa%a9%eb%a1%9d" aria-label="앵커">#</a>
    </span>
    
</h2>
<table>
	<thead>
			<tr>
					<th>도구</th>
					<th>무엇을 보는가</th>
					<th>남겼나 버렸나, 그리고 이유</th>
					<th>비용</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Django 로깅 핸들러가 채우는 팀 채팅 채널</td>
					<td>API, WebSocket 서버, 워커의 모든 ERROR, 배포, 잘못된 로그인 코드가 몰리는 경우</td>
					<td>남김. 누가 시키지 않아도 들여다보는 유일한 곳</td>
					<td>무료</td>
			</tr>
			<tr>
					<td>Fly 로그 시퍼 → S3 호환 버킷, DuckDB로 조회</td>
					<td>마켓플레이스 앱들의 모든 로그 줄</td>
					<td>남김. Fly 자체 버퍼는 몇 분, 이건 전부, 그것도 SQL로</td>
					<td>월 약 2달러 + 스토리지</td>
			</tr>
			<tr>
					<td>Fly 기본 메트릭(Prometheus API)</td>
					<td>CPU, 메모리, 버스트 크레딧, 응답 수, 매니지드 Postgres 익스포터</td>
					<td>남김. DB 멈춤이 제 코드 탓이 아니라는 걸 보여줌</td>
					<td>Fly에 포함</td>
			</tr>
			<tr>
					<td><code>/api/health/</code> Fly 헬스 체크 (두 앱 모두)</td>
					<td>머신에 트래픽을 보내도 되는지</td>
					<td>라우팅용으로는 남김. 사용자가 괜찮다는 증거로는 버림</td>
					<td>Fly에 포함</td>
			</tr>
			<tr>
					<td>DSN을 제 API로 돌린 Sentry SDK</td>
					<td>폰에서 나는 앱 크래시</td>
					<td>SDK는 남김. sentry.io는 가입하지 않음</td>
					<td>API 외 추가 비용 없음</td>
			</tr>
			<tr>
					<td>팀 채팅의 아침 요약</td>
					<td>전날 에러, 사용량, 검색, 채팅, 보드</td>
					<td>남김. CI의 cron 작업에서 제 Mac으로 옮김</td>
					<td>무료</td>
			</tr>
			<tr>
					<td>앱 분석 SDK(두 앱 모두), 검색 엔진의 웹마스터 콘솔</td>
					<td>웹·iOS·Android 사용량, 검색 엔진이 사이트를 어떻게 보는지</td>
					<td>남김. API로 읽음</td>
					<td>무료</td>
			</tr>
			<tr>
					<td>Cloudflare GraphQL 분석</td>
					<td>특정 상태 코드나 경로 뒤에 누가 있는지</td>
					<td>남김. 단, 믿기 전에 user agent로 묶어 봄</td>
					<td>무료 플랜, 쿼리당 하루</td>
			</tr>
			<tr>
					<td>호스팅 OTA 업데이트 서비스 대시보드</td>
					<td>폰들이 어떤 OTA 릴리스를 돌리는지, 크래시 롤백</td>
					<td>아무것도 알려주지 못하던 관리자 목록에서 옮겨 옴</td>
					<td>업데이트 서비스에 포함</td>
			</tr>
			<tr>
					<td>작은 프로브 스크립트</td>
					<td>배포나 재시작 중 실패한 요청</td>
					<td>남김. 오리진을 직접 치니 캐시가 공백을 가리지 못함</td>
					<td>무료</td>
			</tr>
			<tr>
					<td><code>flyctl logs</code></td>
					<td>앱 출력의 최근 몇 분</td>
					<td>실시간 확인용으로는 남김. 기록용으로는 버림</td>
					<td>Fly에 포함</td>
			</tr>
			<tr>
					<td>Cloudflare Early Hints</td>
					<td>아무것도 안 봄. 속도용 기능</td>
					<td>오리진이 <code>Link</code> 헤더를 보내게 한 뒤로는 계속 씀. 이 기능의 캐시 미스가 존 전체 504의 전부였음</td>
					<td>무료</td>
			</tr>
	</tbody>
</table>
<p>표에서 &ldquo;두 앱 모두&quot;라고 적지 않은 건 전부 마켓플레이스 얘기입니다. 견적 앱은 이보다 적습니다. Fly의 헬스 체크, 로그, 메트릭, iOS 앱 분석, 그리고 데이터베이스를 매일 Parquet으로 내보내 DuckDB로 조회하는 것 정도죠. 크래시 리포팅도 알림 채널도 없고, 이 글에서 가장 큰 빈틈이 바로 이겁니다. (업데이트 서비스 행에 대해 밝혀 둘 것: 이 서비스를 만든 회사는 그쪽 블로그에 글을 쓰는 대가로 제게 돈을 지급합니다.)</p>

<h2 class="relative group">에러: 채팅으로 올리는 로깅 핸들러
    <div id="에러-채팅으로-올리는-로깅-핸들러" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%97%90%eb%9f%ac-%ec%b1%84%ed%8c%85%ec%9c%bc%eb%a1%9c-%ec%98%ac%eb%a6%ac%eb%8a%94-%eb%a1%9c%ea%b9%85-%ed%95%b8%eb%93%a4%eb%9f%ac" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>9월 전까지는 처리되지 않은 500, 실패한 푸시 알림, 깨진 WebSocket 프레임이 나타나는 곳이 <code>flyctl logs</code>뿐이었고, 그 스트림을 누가 시키지도 않았는데 읽는 사람은 없습니다. 지금은 로깅 핸들러가 ERROR 이상인 모든 레코드를 API, WebSocket 서버, 백그라운드 워커 어디서 나든 채팅 채널로 보냅니다. 경고(WARNING)는 일부러 알리지 않습니다.</p>
<p>핸들러는 백그라운드 스레드에서 전송하니 요청을 느리게 만들지 않습니다. 핸들러 안에서는 아무것도 로그를 남기지 않으니 재귀에 빠질 일도 없습니다. 그리고 같은 에러는 10분에 한 번까지만, 시간당 최대 30건까지만 보내서, 장애가 나도 메시지 천 개가 아니라 하나로 읽힙니다.</p>
<p>이 채널에는 에러와 나란히 놓고 읽고 싶은 것들도 올라옵니다. 배포는 시작할 때, 올라왔을 때, 실패한 지점에서 커밋과 실행한 사람을 함께 올리고, OTA 업데이트와 <a href="https://jared.lynskey.co.nz/ko/posts/2026/2026-09-14-web-releases-without-redeploying/" >웹 릴리스</a>도 마찬가지입니다. 한 계정에 잘못된 로그인 코드가 몰리면 그것도 ERROR로 남기 때문에, 누가 비밀번호를 찍어 보고 있으면 장애와 같은 자리에 나타납니다.</p>
<p>10월에는 어느 로그에도 대응하는 줄이 없는 알림이 하나 왔습니다. 그 뒤로 알림마다 짧은 랜덤 id, 호스트명, 프로세스 시작 명령을 붙이고, 같은 줄을 그 프로세스의 로그에도 씁니다. 그래서 id를 어디서도 못 찾으면, 그 알림은 그곳에 로그를 남기지 않는 프로세스, 예컨대 SSH 셸에서 온 겁니다. 채널에 올라오는 건 전부, 제가 같이 일하는 코딩 에이전트가 읽을 수 있는 피드에도 들어갑니다. 코드를 만지는 에이전트도 제가 보는 시점에 같은 에러를 보는 거죠.</p>

<h2 class="relative group">로그: Fly의 로그 쉬퍼로 S3에 쌓고 DuckDB로 조회
    <div id="로그-fly의-로그-쉬퍼로-s3에-쌓고-duckdb로-조회" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%a1%9c%ea%b7%b8-fly%ec%9d%98-%eb%a1%9c%ea%b7%b8-%ec%89%ac%ed%8d%bc%eb%a1%9c-s3%ec%97%90-%ec%8c%93%ea%b3%a0-duckdb%eb%a1%9c-%ec%a1%b0%ed%9a%8c" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>Fly 자체 버퍼는 몇 분치 출력, 대략 마지막 백 줄 정도를 갖고 있습니다. 배포를 지켜보기엔 충분하지만 어제 일에는 쓸모가 없죠. 그래서 세 번째 Fly 앱에서 Fly의 로그 시퍼를 돌립니다. 조직의 로그 스트림을 구독하는 Vector인데, S3 싱크 하나만 바꿨습니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">sinks</span><span class="p">.</span><span class="nx">aws_s3</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">type</span> <span class="p">=</span> <span class="s2">&#34;aws_s3&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">bucket</span> <span class="p">=</span> <span class="s2">&#34;myapp-logs&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">compression</span> <span class="p">=</span> <span class="s2">&#34;gzip&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">key_prefix</span> <span class="p">=</span> <span class="s2">&#34;logs/{{fly.app.name}}/%F/&#34;</span>   <span class="c"># one folder per app per day</span>
</span></span><span class="line"><span class="cl">  <span class="nx">batch</span><span class="p">.</span><span class="nx">timeout_secs</span> <span class="p">=</span> <span class="mi">300</span>                  <span class="c"># a quiet app still lands its lines</span></span></span></code></pre></div></div>
<p>마켓플레이스 앱들의 모든 로그 줄이 gzip으로 압축된 JSON으로 S3 호환 버킷에 쌓입니다. 시퍼는 256MB짜리 shared-cpu-1x 머신 한 대로, 한 달에 2달러 정도에 스토리지는 푼돈입니다. DuckDB는 매크로 하나로 파일을 제자리에서 읽고, 하루치가 1초 안팎이면 나옵니다.</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="n">MACRO</span><span class="w"> </span><span class="n">logs</span><span class="p">(</span><span class="n">app</span><span class="p">,</span><span class="w"> </span><span class="k">day</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;*&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="k">TABLE</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">read_json_auto</span><span class="p">(</span><span class="s1">&#39;s3://myapp-logs/logs/&#39;</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="n">app</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">&#39;/&#39;</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="k">day</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="s1">&#39;/*.log.gz&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                               </span><span class="n">union_by_name</span><span class="w"> </span><span class="p">:</span><span class="o">=</span><span class="w"> </span><span class="k">true</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">timestamp</span><span class="p">,</span><span class="w"> </span><span class="n">message</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">logs</span><span class="p">(</span><span class="s1">&#39;myapp-api&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;2026-09-06&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">message</span><span class="w"> </span><span class="k">LIKE</span><span class="w"> </span><span class="s1">&#39;% 500 %&#39;</span><span class="p">;</span></span></span></code></pre></div></div>
<p><a href="https://jared.lynskey.co.nz/ko/posts/2026/2026-09-09-scale-to-zero-504s/" >0으로 스케일 다운 글</a>에서 콜드 스타트를 잰 것도 이 매크로였습니다. 시작 스크립트가 단계마다 알리고, 줄마다 머신 id가 붙어 있으니, 나머지는 <code>GROUP BY</code> 한 번이면 됩니다.</p>
<p>정리하면 하나의 스트림을 읽는 사람에 따라 세 단계로 나눈 셈입니다. 아카이브는 INFO 이상을 전부 보관하고(프로덕션 앱이 INFO로 로그를 남기는 이유이기도 합니다), 실시간 확인 스크립트는 경고와 에러만 보여주고, 채팅 채널은 ERROR를 받습니다. 다만 셋 다 호출한 쪽은 모릅니다. 액세스 로그에 클라이언트 IP도 user agent도 없어서, 그건 Cloudflare에서 봅니다.</p>

<h2 class="relative group">메트릭: Fly에 기본으로 딸린 Prometheus
    <div id="메트릭-fly에-기본으로-딸린-prometheus" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%a9%94%ed%8a%b8%eb%a6%ad-fly%ec%97%90-%ea%b8%b0%eb%b3%b8%ec%9c%bc%eb%a1%9c-%eb%94%b8%eb%a6%b0-prometheus" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>Fly는 조직마다 매니지드 Prometheus를 돌려주기 때문에 저는 메트릭 스택을 따로 운영하지 않습니다. 궁금한 게 생기면 API를 칩니다. 머신별 메모리, CPU, 버스트 크레딧(공유 CPU에서는 <code>fly_instance_cpu_balance</code>가 중요합니다), 앱별 응답 수와 동시 요청 수 같은 것들이죠. 처음부터 알았으면 좋았을 게 두 가지 있습니다. <code>fly tokens create</code>로 만든 토큰은 <code>Authorization: FlyV1 &lt;token&gt;</code>으로 보내야 합니다. <code>Bearer</code>는 <code>flyctl auth token</code>의 개인 토큰에만 통하고, 읽기 전용 토큰은 &ldquo;something went wrong resolving organization&quot;을 돌려줍니다. 그리고 보관 기간이 2주 정도입니다(Fly 설명으로는 15일쯤).</p>
<p>이게 제 몫을 한 건 9월이었습니다. 로그에 &ldquo;Could not open a database connection&quot;이 하루에도 몇 번씩 찍혔고, 딱 앱 버그처럼 보였습니다. 매니지드 Postgres 클러스터도 같은 Prometheus에 들어 있었는데, 그 에러가 난 매 분마다 프라이머리의 스크레이프 시간이 80밀리초 남짓에서 1~10초로 뛰었고, 레플리카는 멀쩡했고, Postgres 시작 시각은 그대로였습니다. Postgres가 재시작된 게 아니라 그 아래 호스트가 멈췄던 겁니다. 타임스탬프를 Fly 지원팀에 보냈고, 10월 초 점검 재시작으로 프라이머리가 다른 호스트로 옮겨진 뒤로 매일 나던 멈춤은 사라졌습니다.</p>

<h2 class="relative group">크래시 리포트: 제 API를 바라보는 Sentry SDK
    <div id="크래시-리포트-제-api를-바라보는-sentry-sdk" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ed%81%ac%eb%9e%98%ec%8b%9c-%eb%a6%ac%ed%8f%ac%ed%8a%b8-%ec%a0%9c-api%eb%a5%bc-%eb%b0%94%eb%9d%bc%eb%b3%b4%eb%8a%94-sentry-sdk" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>예전에는 폰에서 앱이 죽어도 제가 볼 수 있는 곳 어디에도 흔적이 남지 않았습니다. 뻔한 답은 Sentry였고, 저는 그 절반만 가져왔습니다. 앱에는 Sentry의 오픈소스 SDK가 들어 있지만 DSN은 제 Django API의 엔드포인트를 가리킵니다. 그 엔드포인트가 Sentry의 envelope 형식을 받아서 크래시를 묶고, 같은 채팅 핸들러로 알립니다. 트레이싱, 세션, 리플레이는 꺼 두었고 에러만 받습니다.</p>
<p>sentry.io에 가입하지 않은 건, 하루 몇 건 수준의 리포트를 위해 사용자 데이터를 쥔 업체 하나, 개인정보 처리방침 한 줄, 청구서 하나를 더 늘리고 싶지 않았기 때문입니다. 그 대가는 심볼리케이션이고, 이건 아직 대체하지 못했습니다. 스택은 minify된 JavaScript거나 날것의 네이티브 주소라서, 이슈를 보면 무엇이 어느 릴리스와 기기에서 죽었는지, 직전에 사용자가 뭘 했는지는 알지만 소스의 어디인지는 모릅니다.</p>

<h2 class="relative group">대시보드는 메시지 하나
    <div id="대시보드는-메시지-하나" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%8c%80%ec%8b%9c%eb%b3%b4%eb%93%9c%eb%8a%94-%eb%a9%94%ec%8b%9c%ec%a7%80-%ed%95%98%eb%82%98" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>두 앱 모두 대시보드를 따로 만들지 않았습니다. 작년 글에 대시보드를 가리는 기준을 하나 적었는데, 아무도 알아서 열지 않는 대시보드라면 존재 가치를 증명하지 못한 거라는 겁니다. 알아서 도착하는 메시지는 열 필요가 없으니, 제게 가장 대시보드에 가까운 건 매일 아침 팀 채팅에 올라오는 글 하나입니다.</p>
<p>맨 앞은 지난 24시간의 프로덕션 에러입니다. 채팅 채널이 아니라 로그 아카이브에서 읽는데, 알림은 횟수가 제한돼 있어서 무언가가 몇 번 일어났는지 말해 주지 못하기 때문입니다. 숫자, id, 주소를 가린 뒤 묶기 때문에 같은 실패 백 건은 횟수가 붙은 한 줄이 되고, 이메일 주소가 채널에 흘러들 일도 없습니다.</p>
<p>그다음이 실패한 헬스 체크, 전날 사용량을 일주일 전 같은 요일과 비교한 것(일요일은 월요일과 절대 같지 않으니까요), 최근 일주일 검색 유입, 답장을 기다리는 채팅, 작업 보드, 그리고 지출입니다. 불러오지 못한 섹션이 있으면 그렇다고 적고, 글은 그래도 올라갑니다.</p>
<p>앱 분석 데이터를 API로 읽다 보면 숫자를 조금은 의심하게 됩니다. 8월에는 아제르바이잔에서 Android 첫 실행이 121건 찍히고 그 뒤로 삭제가 119건 따라왔는데, 그달 Android 합계는 이걸 빼고 읽습니다.</p>
<p>릴리스에는 따로 보는 화면이 있습니다. OTA 업데이트를 제 서버에서 내보내던 시절, 릴리스를 보는 방법은 &ldquo;is active&rdquo; 체크박스가 달린 관리자 목록이 전부였고, 업데이트를 받은 폰이 몇 대인지 전혀 몰랐습니다. 지금 쓰는 서비스의 대시보드는 기기들이 실제로 어떤 릴리스를 돌리는지, 설치가 어떻게 됐는지, 그리고 잘못된 번들이 죽어서 SDK가 이전 번들로 되돌렸을 때의 크래시 롤백까지 보여줍니다. <a href="https://jared.lynskey.co.nz/ko/posts/2026/2026-09-13-moving-to-codemagic-patch/" >호스팅 업데이트 서비스로 옮긴 이야기</a>는 따로 정리해 두었습니다.</p>

<h2 class="relative group">버린 것, 또는 더는 믿지 않게 된 것
    <div id="버린-것-또는-더는-믿지-않게-된-것" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%b2%84%eb%a6%b0-%ea%b2%83-%eb%98%90%eb%8a%94-%eb%8d%94%eb%8a%94-%eb%af%bf%ec%a7%80-%ec%95%8a%ea%b2%8c-%eb%90%9c-%ea%b2%83" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>이 중 유료 도구는 하나도 없습니다. 대부분은 신호인 줄 알았는데 신호가 아니었던 것들이고, 하나는 잘못된 곳에서 돌던 작업입니다.</p>

<h3 class="relative group">user agent로 묶어 보기 전까지의 Cloudflare 504 비율
    <div id="user-agent로-묶어-보기-전까지의-cloudflare-504-비율" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#user-agent%eb%a1%9c-%eb%ac%b6%ec%96%b4-%eb%b3%b4%ea%b8%b0-%ec%a0%84%ea%b9%8c%ec%a7%80%ec%9d%98-cloudflare-504-%eb%b9%84%ec%9c%a8" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>9월에 Cloudflare 대시보드를 보니 하루 요청의 22%가 504로 실패하는 것처럼 나왔습니다. 앱이 30초마다 폴링하는 엔드포인트에 몰려 있었는데, 헬스 체크는 전부 정상이었고 앱 자체 로그로는 그 요청들이 빨랐습니다. 며칠을 매달렸습니다. 머신이 콜드 스타트에 25~33초가 걸리고 그걸 하루 92번 한다는 걸 찾아내 고쳤고, <a href="https://jared.lynskey.co.nz/ko/posts/2026/2026-09-09-scale-to-zero-504s/" >0으로 스케일 다운 글</a>에 그게 원인이라고 썼습니다.</p>
<p>다음 날 아침 같은 504를 user agent로 묶어 봤습니다. 9월 5일부터 9일 사이의 7,344건이 전부 Cloudflare 자체의 Early Hints 캐시 조회(user agent는 <code>bastion early hints</code>, <code>nginx-ssl early hints</code>)였습니다. 이 조회는 오리진에 아예 가지 않습니다. 그 줄의 504는 타임아웃이 아니라 캐시 미스를 기록한 것이죠. 실제 요청 경로를 따라 하기 때문에 가장 바쁜 엔드포인트에 떨어졌고, 정말로 사용자가 실패하는 것처럼 보였습니다. 앱, 브라우저, curl, 검색 엔진 크롤러에서는 한 건도 없었습니다. 오리진이 <code>Link</code> 헤더를 보내지 않으니 Early Hints는 아무 일도 하고 있지 않았고, 그래서 일단 껐습니다. 나중에는 <a href="https://jared.lynskey.co.nz/ko/posts/2026/cloudflare-504-early-hints/" >제대로 동작하게 만들었습니다</a>.</p>
<p>콜드 스타트 작업은 그래도 남는 게 있었습니다. 배포도 깨어나는 것도 빨라졌으니까요. 하지만 고친다고 생각했던 장애는 사용자에게 닿은 적이 없었습니다.</p>
<p>그래서 꼭 전하고 싶은 규칙이 이겁니다. CDN의 에러율을 믿기 전에 user agent로 묶어서, 그게 CDN 자기 장치에서 나온 게 아닌지 확인하세요. Cloudflare 무료 플랜에서는 <code>httpRequestsAdaptiveGroups</code>에 대한 GraphQL 쿼리로, 하루씩 끊어서 볼 수 있습니다. 다음 소동도 같은 쿼리로 정리됐습니다. 폴링 엔드포인트 하나가 하루 70~260건에서 2,880건으로 뛰었는데, 스크레이퍼가 아니었습니다. 테스트하고 나서 iOS 시뮬레이터에 앱을 켜 둔 채로 둔 제 Mac이었습니다.</p>

<h3 class="relative group">초록불 헬스 체크
    <div id="초록불-헬스-체크" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%b4%88%eb%a1%9d%eb%b6%88-%ed%97%ac%ec%8a%a4-%ec%b2%b4%ed%81%ac" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>Fly는 두 앱의 <code>/api/health/</code>를 30초마다 확인하고, 그 결과로 어느 머신에 트래픽을 보낼지 정합니다. 그 일은 잘합니다. 하지만 헬스 체크는 이미 떠 있는 머신을 상대로 돌기 때문에, 머신이 뜨기 전에 일어나는 일은 보지 못합니다. 콜드 스타트, 프록시 앞의 대기열, 끝내 도착하지 않는 요청 같은 것들이죠. 지금은 라우팅 말고는 바라는 게 없습니다.</p>
<p>배포 때는 대신 프로브 스크립트를 씁니다. 배포 내내 오리진을 치다가 하나라도 실패하면 1로 종료합니다. 이 스크립트가 평범한 <code>flyctl deploy</code>에서 7.3초짜리 공백을 잡아냈고, 예비 머신을 먼저 깨우는 배포 스크립트로 바꾼 뒤에는 2,514건 중 실패가 0건이었습니다.</p>
<p>헬스 체크는 스스로 장애를 두 번 일으키기도 했습니다. 7월에는 Django의 <code>SECURE_SSL_REDIRECT</code>가 Fly의 평문 HTTP 프로브에 301로 답해서, 모든 머신이 체크에 실패했고 모두가 503을 받았습니다. 8월에는 헬스 뷰의 <code>except Exception</code>을 좁혔더니 &ldquo;connection already closed&rdquo; 에러가 500으로 새어 나갔고, 뷰의 재시도가 바로 그 경우를 위해 있었는데도 Fly가 머신을 빼 버렸습니다. 지금 체크는 이 프로세스가 요청을 처리할 수 있는지만 묻고, DB가 멈춰도 두 앱이 라우팅에서 빠지지 않습니다.</p>
<p>아침 요약도 세는 법에서 같은 교훈을 얻었습니다. 마켓플레이스는 놀고 있는 머신을 서스펜드하기 때문에, 요청에 깨어난 머신은 시작 후 몇백 밀리초 만에 첫 체크에 실패하고 다음 체크는 통과합니다. 이걸 세다 보니 매일 아침이 장애처럼 읽혔고, 그래서 1분 안에 다시 통과한 실패는 이제 머신 기상과 배포로 따로 적습니다.</p>

<h3 class="relative group">CI의 예약 작업
    <div id="ci의-예약-작업" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#ci%ec%9d%98-%ec%98%88%ec%95%bd-%ec%9e%91%ec%97%85" aria-label="앵커">#</a>
    </span>
    
</h3>
<p>아침 요약은 cron 일정으로 도는 CI 워크플로로 시작했다가 하루 만에 옮겼습니다. 앱 분석과 검색 데이터를 읽게 되자 필요한 키가 분석 프로젝트의 서비스 계정 키였는데, 그 프로젝트에서 뭐든 할 수 있는 키라 CI 시크릿에 두고 싶지 않았습니다. 지금은 제 Mac의 스케줄러가 20분마다 실행하고, 오클랜드 시간으로 아침 8시가 지났는데 오늘 글이 아직 안 올라갔을 때만 올립니다. 그러니 8시에 잠들어 있던 Mac은 깨어나면서 올리죠. 워크플로는 앱 분석과 검색 섹션을 뺀 수동 대비책으로 남겨 두었습니다.</p>

<h2 class="relative group">다시 시작한다면
    <div id="다시-시작한다면" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%8b%a4%ec%8b%9c-%ec%8b%9c%ec%9e%91%ed%95%9c%eb%8b%a4%eb%a9%b4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>첫 장애가 난 뒤가 아니라 첫 사용자가 오기 전에 세 가지를 깔겠습니다. 팀이 이미 읽는 채팅 채널로 에러 보내기(횟수 제한 포함), SQL로 조회할 수 있는 오브젝트 스토리지 로그 아카이브, 그리고 플랫폼이 이미 모으고 있는 메트릭. 한 달에 몇 달러면 됩니다. 크래시 리포팅은 앱이 폰에 올라가는 주에 붙입니다. 그러지 않으면 폰에서 난 크래시는 아무에게도 아무것도 알려주지 않으니까요. 아침 요약은 확인할 곳이 너무 많아져서 결국 아무 데도 확인하지 않게 될 즈음에 붙이면 됩니다.</p>
<p>트레이싱은 요청이 넘나들 두 번째 서비스가 생길 때까지, 호스팅 APM은 공유 대시보드와 온콜 순번, 남의 청구서를 감당할 예산이 필요한 팀이 생길 때까지 미루겠습니다. 작년 글의 도구들이 제값을 하기 시작하는 건 그때부터입니다. 그 전까지 가장 쓸모 있는 습관은 공짜입니다. 숫자가 무서워 보이면, 무엇이 고장났는지 정하기 전에 그 숫자를 무엇이 만들었는지부터 확인하세요. 이 글에 나온 소동 중 여럿은 프로덕트가 아니라 모니터링에서 나왔습니다.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2026/2026-10-22-monitoring-tools-two-products/featured.jpg" medium="image" type="image/jpeg" />
    </item>
    <item>
      <title>개발자가 실제로 쓰는 모니터링 시스템 구축: 메트릭·로그·트레이스와 CloudWatch vs Datadog</title>
      <link>https://jared.lynskey.co.nz/ko/posts/2025/2025-10-13-devops-monitoring/</link>
      <guid isPermaLink="true">https://jared.lynskey.co.nz/ko/posts/2025/2025-10-13-devops-monitoring/</guid>
      <pubDate>Sun, 12 Oct 2025 00:00:00 +0000</pubDate>
      <dc:creator>Jared Lynskey</dc:creator>
      <category>DevOps</category>
      <category>Monitoring</category>
      <category>Observability</category>
      <category>CloudWatch</category>
      <category>Datadog</category>
      <category>Developer Experience</category>
      <category>Site Reliability</category>
      <description>개발자가 실제로 쓰는 모니터링 시스템을 구축하는 방법입니다. 메트릭·로그·트레이스를 연결하는 법, 어디부터 봐야 할지 알려 주는 알림, 그리고 CloudWatch와 Datadog 중 무엇을 고를지 정리했습니다.</description>
      <content:encoded><![CDATA[<p>개발자를 위한 모니터링 시스템이 할 일은 결국 하나입니다. 뭔가 고장 나면 빨리 알려 주고, 무엇이 어디서 고장 났는지 바로 보여 주는 것이죠. 그러려면 메트릭·로그·트레이스가 서로 연결돼 있어야 하고, 알림은 어디부터 볼지 짚어 줘야 하고, 대시보드는 시키지 않아도 열어 보게 되는 것이어야 합니다. 모니터링 프로그램을 직접 개발하든 기성 도구를 쓰든 기준은 같습니다. AWS라면 작은 팀은 CloudWatch로 충분하고, 규모가 커지면 Datadog이 제값을 합니다.</p>
<p>&ldquo;내 컴퓨터에서는 되는데&quot;와 &ldquo;프로덕션에서 된다&rdquo; 사이에는 간극이 있고, 모니터링은 그 간극을 메우라고 존재합니다. 그런데 대부분은 메우지 못하죠. 장애가 났을 때 개발자가 손을 뻗는 도구가 아니라, 운영팀을 위한 도구로 만들어지기 때문입니다.</p>
<p>최근 프리랜서로 DevOps와 인프라 일을 시작했는데, 새로운 현장에 들어가면 가장 먼저 보는 것 중 하나가 모니터링 구성입니다. README보다 팀의 실체를 훨씬 잘 보여주거든요. 그리고 실패 패턴은 거의 매번 같습니다. 도구는 다 있는데, 개발자가 실제로 일하는 방식과 연결이 안 되어 있는 겁니다. 프로덕션 로그만 볼 수 있었다면 5분에 끝날 버그에 누군가 3시간을 쓰고, 장애는 알림이 아니라 고객 문의 티켓으로 알게 되고, 반대로 하루 200개씩 쏟아지는 알림은 대부분 노이즈라 모두가 전부 무시하게 되고, 그 결과 인시던트가 터질 때마다 처음 30분을 &ldquo;고치는&rdquo; 게 아니라 &ldquo;뭐가 고장났는지&rdquo; 찾는 데 씁니다.</p>
<p>이 글은 일반론입니다. 프로덕트 두 개에 <a href="https://jared.lynskey.co.nz/ko/posts/2026/2026-10-22-monitoring-tools-two-products/" >제가 지금 실제로 쓰는 도구</a>는 따로 정리해 뒀습니다.</p>

<h2 class="relative group">메트릭, 로그, 트레이스는 연결돼야 의미가 있다
    <div id="메트릭-로그-트레이스는-연결돼야-의미가-있다" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%eb%a9%94%ed%8a%b8%eb%a6%ad-%eb%a1%9c%ea%b7%b8-%ed%8a%b8%eb%a0%88%ec%9d%b4%ec%8a%a4%eb%8a%94-%ec%97%b0%ea%b2%b0%eb%8f%bc%ec%95%bc-%ec%9d%98%eb%af%b8%ea%b0%80-%ec%9e%88%eb%8b%a4" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>메트릭은 뭔가 잘못됐다는 걸 알려줍니다(에러율 급증, 레이턴시 상승). 로그는 무슨 일이 있었는지(스택 트레이스, 요청 페이로드, 에러 메시지), 트레이스는 어디서 일어났는지(어느 서비스, 어느 엔드포인트, 어느 DB 호출) 알려주죠. 여기까진 다들 아는 얘기입니다. 중요한 건 이 셋이 연결되어 있느냐입니다. 알림이 울리면 메트릭에서 관련 로그로, 다시 트레이스로 클릭만으로 넘어갈 수 있어야 합니다. 개발자가 도구 세 개를 오가며 타임스탬프를 손으로 맞추고 있다면, 각 도구가 아무리 훌륭해도 그 구성은 실패한 겁니다.</p>
<p>대시보드에도 같은 잣대를 들이댈 수 있습니다. CPU, 메모리, 디스크 I/O는 용량 계획에는 중요하지만 500 에러 디버깅에는 아무 도움이 안 됩니다. 개발자들이 자발적으로 여는 대시보드에는 기술 메트릭 옆에 비즈니스 메트릭(에러율 옆에 시간당 가입 수), 타임라인 위에 겹쳐진 최근 배포, 로그 링크가 붙은 최근 1시간 상위 5개 에러, 그리고 평균이 아닌 레이턴시 퍼센타일(p50, p95, p99)이 있습니다. 아무도 알아서 열지 않는 대시보드라면 존재 가치를 증명하지 못한 거죠.</p>
<p>알림도 같은 원리입니다. 모든 알림은 두 가지 질문에 답해야 합니다. 뭐가 고장났는가, 그리고 어디서부터 봐야 하는가.</p>
<p>나쁜 알림: &ldquo;web-server-3 CPU 높음&rdquo;</p>
<p>좋은 알림: &ldquo;14:32부터 /api/payments 에러율 &gt; 5%. 마지막 배포: 14:15, @sarah. [로그 보기] [트레이스 보기]&rdquo;</p>
<p>두 번째 쪽으로 가려면 몇 가지 요령이 있습니다. 임의의 임계값이 아니라 베이스라인 이탈로 알림을 걸고, 소유권 기준으로 라우팅하고(결제 에러는 조직 전체가 아니라 결제팀으로), 연관된 에러는 개별 알림 50개가 아니라 Slack 메시지 하나로 묶는 겁니다. 피드백 루프도 짧게 유지해야 합니다. &ldquo;뭔가 고장남&quot;에서 &ldquo;개발자가 알게 됨&quot;까지 1분 이내가 기준인데, 그러려면 5분 배치가 아닌 실시간 로그 스트리밍, 모든 대시보드의 배포 마커, 서비스 경계를 넘어도 끊기지 않는 분산 트레이싱이 필요합니다.</p>

<h2 class="relative group">쓸 사람을 위해 만들 것
    <div id="쓸-사람을-위해-만들-것" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%93%b8-%ec%82%ac%eb%9e%8c%ec%9d%84-%ec%9c%84%ed%95%b4-%eb%a7%8c%eb%93%a4-%ea%b2%83" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>운영팀이 저지르는 가장 큰 실수는 자기 자신을 위한 모니터링을 만드는 겁니다. 해법이 대단한 것도 아닙니다. 디버깅 중인 개발자 옆에 앉아서 어디서 막히는지 지켜보세요. 인시던트 중에 어떤 질문이 오가는지 물어보세요. &ldquo;어느 서비스야?&rdquo;, &ldquo;뭐가 바뀌었지?&rdquo;, &ldquo;요청이 어떻게 생겼는데?&rdquo; 같은 것들이요. 그리고 제품에 정말 중요한 메트릭이 뭔지 찾아내세요. CPU인 경우는 거의 없고, 보통은 체크아웃 완료율이나 업로드 성공률 같은 것들입니다.</p>
<p>그다음은 마찰 제거입니다. 새 서비스 계측에 하루가 걸리면 아무도 안 합니다. 쓰고 있는 프레임워크(Django, Express, Spring)를 자동 계측하는 라이브러리, 복사해서 바로 쓰는 대시보드·알림 템플릿, 티켓 없이 직접 메트릭을 추가할 수 있는 셀프서비스 도구를 마련하세요. 그리고 전체를 코드처럼 다루세요. 설정은 버전 관리에 넣고, 알림 규칙은 &ldquo;울려야 할 때 울리는지&rdquo; 테스트하고, 멀티 리전으로 갈 계획이라면 처음부터 반영해 두는 겁니다.</p>
<p>만든 뒤에는 같은 습관으로 살려 둡니다. 계측 코드는 다른 프로덕션 코드와 똑같이 코드 리뷰를 거치고, 직접 만든 메트릭에는 테스트를 붙입니다. 포스트모템 때마다 &ldquo;어떤 모니터링이 있었으면 더 빨리 잡았을까&quot;를 묻고, 그 답은 보고서가 아니라 그 주의 대시보드와 알림에 반영합니다. 그리고 일 년에 몇 번은 전체를 훑어서 아무도 안 여는 걸 지웁니다. 아무도 반응하지 않는 알림은 다음 알림도 무시하도록 사람들을 길들이니까요.</p>

<h2 class="relative group">AWS 위의 소규모 팀이라면 일단 CloudWatch
    <div id="aws-위의-소규모-팀이라면-일단-cloudwatch" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#aws-%ec%9c%84%ec%9d%98-%ec%86%8c%ea%b7%9c%eb%aa%a8-%ed%8c%80%ec%9d%b4%eb%9d%bc%eb%a9%b4-%ec%9d%bc%eb%8b%a8-cloudwatch" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>엔지니어 50명 미만에 AWS를 쓰고 있다면 CloudWatch가 올바른 출발점입니다. 첫날부터 더 화려한 걸 사고 싶은 유혹은 참는 게 좋습니다. EC2, Lambda, RDS, ECS가 전부 자동으로 CloudWatch에 리포트하니 계측 코드 없이도 가시성이 생기고, 소규모 팀 기준 월 10~50달러에 고정 플랫폼 비용도 없습니다. 메트릭, 로그, 트레이스(X-Ray 경유), 알람이 한곳에 모여 있고, 의미 있는 알림과 대시보드 정도는 몇 주가 아니라 반나절이면 만듭니다.</p>
<p>과소평가된 기능이 CloudWatch Logs Insights입니다. Elasticsearch를 따로 세우지 않아도 로그에 쿼리를 날릴 수 있죠:</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">fields @timestamp, @message
</span></span><span class="line"><span class="cl">| filter @message like /ERROR/
</span></span><span class="line"><span class="cl">| stats count() by bin(5m)</span></span></code></pre></div></div>
<p>그 밖의 포인트:</p>
<ul>
<li>복합 알람으로 노이즈를 줄이세요. 에러율이 높고 &ldquo;동시에&rdquo; 응답 시간이 나빠졌을 때 울리게 하는 겁니다. 조건마다 따로가 아니라요.</li>
<li>진짜 가치는 커스텀 메트릭에 있습니다. CloudWatch SDK로 비즈니스 메트릭(가입, 거래, 기능 사용량)을 인프라 메트릭과 나란히 보내세요.</li>
<li>CloudWatch Synthetics는 사용자 여정을 따라가는 카나리 테스트를 스케줄로 돌려줍니다. 체크아웃이 깨진 걸 사용자보다 먼저 알게 되죠.</li>
<li>X-Ray면 최소한의 설정으로 분산 트레이싱이 됩니다. 대부분의 마이크로서비스 구조에는 이걸로 충분합니다.</li>
</ul>
<p>졸업 시점도 명확합니다. 멀티 클라우드로 가거나, 제대로 된 이상 감지가 필요해지거나, 대시보드 커스터마이징이 고통스러워지거나, 팀이 50명을 넘어 본격적인 협업 기능이 필요해질 때입니다.</p>

<h2 class="relative group">팀이 커지면 Datadog
    <div id="팀이-커지면-datadog" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ed%8c%80%ec%9d%b4-%ec%bb%a4%ec%a7%80%eb%a9%b4-datadog" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>Datadog은 비쌉니다. 큰 조직이면 연간 2만~10만 달러 이상이 보통이지만, 규모가 되면 값을 합니다. AWS, Azure, GCP, 온프레미스, 컨테이너, 서버리스, 데이터베이스, 프론트엔드를 하나의 뷰에서 봅니다. Watchdog이 임계값을 손으로 조정하지 않아도 이상 패턴을 잡아주는데, 서비스가 수천 개면 사람이 눈으로 지켜볼 방법이 없으니 이게 큽니다. CloudWatch에 없는 협업 레이어도 갖춰져 있습니다. 팀별 대시보드, RBAC, 공유 조사 노트북, PagerDuty/Opsgenie 연동, 다중 조건 알림, 임계값 돌파 예측, 유지보수 윈도우, 그리고 코드 수준 프로파일링까지 내려가는 APM과 자동 생성 서비스 맵까지요.</p>
<p>도입 순서는 대략 이렇게 잡습니다. 중요한 서비스부터 계측하고, 첫날부터 팀·환경별 태깅을 철저히 합니다. 메트릭 명명 규칙, 대시보드 템플릿, 알림 심각도 레벨, SLO 정의는 초반에 합의하세요. 표준을 나중에 소급 적용하는 건 정말 고역입니다. CI/CD 배포 마커, 인시던트 관리, Slack과 연동하고, 교육 시간도 확보하세요. Datadog은 강력하지만 그냥 봐서는 못 씁니다. 그리고 청구서를 주시하세요. 노이즈 많은 메트릭은 필터링하고, 트래픽 많은 서비스는 트레이스를 샘플링하고, 실제로 쓰는 기능이 뭔지 정기적으로 점검하는 겁니다.</p>
<p>계약 전에 볼만한 대안으로는 New Relic(비슷한 구성, 대용량 트레이싱은 더 쌀 수도), Dynatrace(AIOps가 강하고 금융권에서 인기), Splunk(로그 분석은 최고 수준, 보안팀이 이미 쓰고 있다면 더욱), Grafana Cloud(이미 Prometheus/Loki를 쓴다면 자연스러운 선택)가 있습니다.</p>
<p>실제로는 많은 팀이 하이브리드로 정착합니다. AWS 네이티브 서비스는 자동에 저렴한 CloudWatch로, 애플리케이션 레이어는 Datadog으로, 그리고 CloudWatch 메트릭을 Datadog으로 수집해 통합 뷰를 만드는 거죠. 멋은 없지만, 잘 굴러갑니다.</p>

<h2 class="relative group">CloudWatch vs Datadog 한눈에 비교
    <div id="cloudwatch-vs-datadog-한눈에-비교" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#cloudwatch-vs-datadog-%ed%95%9c%eb%88%88%ec%97%90-%eb%b9%84%ea%b5%90" aria-label="앵커">#</a>
    </span>
    
</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>CloudWatch</th>
					<th>Datadog</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>잘 맞는 팀</td>
					<td>엔지니어 50명 미만, AWS만 쓰는 팀</td>
					<td>서비스가 많고 클라우드를 여러 개 쓰는 큰 조직</td>
			</tr>
			<tr>
					<td>비용 구조</td>
					<td>쓴 만큼 내고 플랫폼 비용 없음. 소규모 팀 기준 월 10~50달러</td>
					<td>큰 조직이면 연간 2만~10만 달러 이상이 보통. 청구서를 계속 지켜봐야 함</td>
			</tr>
			<tr>
					<td>구축 난이도</td>
					<td>EC2·Lambda·RDS·ECS가 자동으로 리포트. 쓸 만한 알림은 반나절이면</td>
					<td>중요한 서비스부터 계측, 태깅·명명 규칙을 초반에 합의, 교육 시간 필요</td>
			</tr>
			<tr>
					<td>멀티 클라우드</td>
					<td>AWS 중심. 멀티 클라우드로 가면 졸업할 때</td>
					<td>AWS·Azure·GCP·온프레미스·컨테이너·서버리스를 한 뷰에서</td>
			</tr>
			<tr>
					<td>이상 감지</td>
					<td>주로 직접 조정하는 알람. 그 이상이 필요해지면 역시 졸업 신호</td>
					<td>Watchdog이 임계값을 손대지 않아도 이상 패턴을 잡아 줌</td>
			</tr>
			<tr>
					<td>협업 기능</td>
					<td>빈약한 편. 50명을 넘으면 아쉬워짐</td>
					<td>팀별 대시보드, RBAC, 공유 노트북, PagerDuty/Opsgenie 연동</td>
			</tr>
			<tr>
					<td>트레이싱</td>
					<td>X-Ray, 최소한의 설정</td>
					<td>코드 수준 프로파일링까지 되는 APM, 자동 생성 서비스 맵</td>
			</tr>
	</tbody>
</table>

<h2 class="relative group">처음부터 시작한다면: 모니터링 시스템 구축 순서
    <div id="처음부터-시작한다면-모니터링-시스템-구축-순서" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%ec%b2%98%ec%9d%8c%eb%b6%80%ed%84%b0-%ec%8b%9c%ec%9e%91%ed%95%9c%eb%8b%a4%eb%a9%b4-%eb%aa%a8%eb%8b%88%ed%84%b0%eb%a7%81-%ec%8b%9c%ec%8a%a4%ed%85%9c-%ea%b5%ac%ec%b6%95-%ec%88%9c%ec%84%9c" aria-label="앵커">#</a>
    </span>
    
</h2>
<p>제가 하는 순서는 이렇습니다.</p>
<ol>
<li>개발자들에게 인시던트 때 뭐가 힘든지 묻습니다.</li>
<li>중요한 플로우의 SLO를 정의합니다. 로그인, 체크아웃, 검색에서 &ldquo;작동한다&quot;는 게 정확히 무슨 뜻인지요.</li>
<li>그 크리티컬 패스부터 계측합니다.</li>
<li>다음으로 분산 트레이싱을 넣습니다. 마이크로서비스 구조에서는 디버깅 투자 대비 효과가 제일 크거든요.</li>
<li>알림은 CPU가 아니라 그 플로우에 걸고, 알림마다 담당 팀으로 보냅니다.</li>
<li>모든 알림에 런북을 붙여서 새벽 3시의 내가 뭘 확인해야 할지 알 수 있게 합니다.</li>
<li>분기마다 리뷰 일정을 잡아 묵은 알림을 지우고, 어긋난 임계값을 고치고, 대시보드가 현재 아키텍처와 맞는지 확인합니다.</li>
</ol>
<p>제가 팀들을 여러 번 건져내야 했던 함정들도 적어둡니다. 도구 과잉(연동되는 3개가 연동 안 되는 6개보다 낫습니다), 베이스라인 없는 그래프(500 rps가 평소 수준인가요, 10배 스파이크인가요?), 사용자와 무관한 것 모니터링(스테이트리스 컨테이너의 디스크 I/O 같은 것), 그리고 모니터링 자체의 이중화 부재. 인시던트 도중에 알림 시스템이 죽으면, 가장 잘 보여야 할 순간에 아무것도 안 보이게 됩니다.</p>
<p>이 중 어느 것도 최고급 도구를 요구하지 않습니다. 필요한 건 개발자가 필요한 정보를, 실제로 보는 곳에서, 빠르게 얻는 것뿐입니다.</p>
]]></content:encoded>
      <media:content url="https://jared.lynskey.co.nz/en/posts/2025/2025-10-13-devops-monitoring/featured.jpeg" medium="image" type="image/jpeg" />
    </item>
  </channel>
</rss>
