🔥 Logs Insights: 로그에 쿼리를 발행하는 법

2049자
22분

16:9 가로 표지 일러스트레이션. 흰 배경, 둥근 모서리, 절제된 그림자. 위쪽에 AWS 오렌지 #ff9900 리본 배너에 'CloudWatch Logs Insights' 제목을 적었다. 가운데에는 슬레이트 그레이 #1e293b LogGroup 직사각형 컨테이너가 놓여 있고, 그 안에 가로 로그 라인 세그먼트가 여러 줄 깔려 있다. 오른쪽에서 슬레이트 손잡이가 달린 투명한 돋보기가 떠 있고, 거기서 세 가닥의 색 다른 광선이 로그 라인을 가로로 훑는다. 에메랄드 그린 #10b981 광선은 'filter', 앰버 #f59e0b 광선은 'parse', AWS 오렌지 #ff9900 광선은 'stats'를 가리킨다. 광선이 라인을 훑는 동작이 명확히 그려져, 인덱스 lookup이 아니라 스캔이라는 비유가 직관적으로 보인다. 오른쪽 아래에 작은 슬레이트 텍스트 배지로 'pay per GB scanned'를 적은 흰 배경, 둥근 모서리, 절제된 그림자, 깔끔한 산세리프 글꼴의 프로페셔널 IT 표지

한 쿼리가 500 GB를 스캔하면 CloudWatch 가격표 위에서 $2.50을 청구서에 적는다. 결과가 1만 건이든 0건이든 같다. Logs Insights의 청구는 스캔한 GB 단위이지 결과 줄 단위가 아니다.

Logs: 구조화 로그와 인덱싱 관점에서 "자유 텍스트는 인덱스가 아니라 Logs Insights 스캔 대상"이라는 한 줄을 정의해 두었다. 그 한 줄이 쿼리의 형태, 한 번에 받는 한도, 청구서가 자라는 메커니즘까지 다 정한다.

파이프라인 한 줄로 시작한다

Logs Insights 쿼리 문법의 형태가 단순하다. 파이프 |로 명령을 잇는 한 줄. 가장 자주 쓰는 쿼리는 다섯 명령을 잇는다.

fields @timestamp, @message, @logStream
| filter status >= 500
| parse @message /user_id=(?<user>\S+)/
| stats count() as errors by user
| sort errors desc
| limit 20
fields @timestamp, @message, @logStream
| filter status >= 500
| parse @message /user_id=(?<user>\S+)/
| stats count() as errors by user
| sort errors desc
| limit 20

fields로 보고 싶은 컬럼을 정하고, filter로 줄을 거르고, parse로 자유 텍스트에서 필드를 꺼내고, stats로 모으고, sortlimit로 마무리한다. 여기까지가 90%다. 나머지는 공식 명령 18종 안에서 골라 쓴다. display, dedup, unmask, unnest, lookup, pattern(반복 텍스트 구조 찾기), diff(직전 같은 길이 윈도와 비교), anomaly(ML 기반 이상 탐지), filterIndex(아래에서 다시), SOURCE, join, subqueries. 마지막 둘은 2025년에 들어왔다.

16:9 가로 파이프라인 다이어그램. 흰 배경, 둥근 모서리, 절제된 그림자. 위쪽 제목 'Logs Insights: pipeline of commands separated by |'. 왼쪽에서 오른쪽으로 다섯 개의 둥근 직사각형 노드가 굵은 슬레이트 #475569 화살표 위에 'parse' 라벨로 놓여 있다. 첫 노드 'fields' 슬레이트 #475569 fill, 두 번째 'filter' 에메랄드 그린 #10b981 fill에 'reduce rows' 캡션, 세 번째 'parse' 앰버 #f59e0b fill, 네 번째 'stats' AWS 오렌지 #ff9900 fill에 'aggregate' 캡션, 다섯 번째 'sort' 슬레이트 다크 #1e293b fill, 마지막에 작은 라이트 그레이 'limit 20' 노드가 붙어 있다. 노드들 사이를 잇는 화살표 위에 모두 '|' 파이프 글자를 적었다. 아래에 에메랄드 강조 배지 'fields | filter | parse | stats | sort | limit'가 한 줄로 깔린 프로페셔널 IT 다이어그램

쿼리를 짜기 전에 자동으로 채우는 필드가 다섯 개 있다. CloudWatch Logs는 모든 이벤트에 다섯 개 system 필드를 자동 발행한다. @timestamp(이벤트 타임스탬프), @message(원문), @logStream(스트림 이름), @log(account-id:log-group-name 식별자), @ingestionTime(CloudWatch가 받은 시각). 이 다섯이 모든 쿼리의 출발점이다. 거기에 Lambda Log Group은 @requestId, @duration, @billedDuration, @memorySize 같은 필드를 자동으로 더 풀어 준다. 그래서 Lambda에서 한 함수의 cold start 비용을 보고 싶을 때 parse 한 줄 없이 바로 filter @type = "REPORT" | stats max(@billedDuration) by @logStream처럼 들어갈 수 있다. 자동으로 풀린 필드는 콘솔의 Fields 패널에 그대로 떠 있어 클릭으로도 쿼리에 추가한다.

16:9 가로 카테고리 그리드 다이어그램. 흰 배경, 둥근 모서리, 절제된 그림자. 위쪽 제목 'Logs Insights QL command surface: 18 commands by role'. 세 개의 세로 컬럼을 가로로 둔다. 왼쪽 컬럼은 AWS 오렌지 #ff9900 헤더 'Core 6: 90% of queries' 아래에 모노스페이스 알약 6개를 세로로 쌓아 둔다. fields, filter, parse, stats, sort, limit. 가운데 컬럼은 슬레이트 #475569 헤더 'Auxiliary' 아래 작은 알약 5개. display, dedup, unmask, unnest, lookup. 오른쪽 컬럼은 에메랄드 그린 #10b981 헤더 'Specialty + 2025' 아래 알약 7개에 짧은 부제. pattern (text shapes), diff (vs previous window), anomaly (ML), filterIndex (skip non-indexed), SOURCE (account-scope), join (NEW 2025), subqueries (NEW 2025). 'NEW 2025' 둘에는 작은 앰버 #f59e0b 별 배지를 붙인다. 하단 슬레이트 캡션 'all commands chain with the | pipe character'

콘솔에서 쿼리를 처음 띄우면 시간 윈도가 직전 1시간으로, limit은 20으로 들어간다. 이게 기본값이다. 그 설정은 콘솔의 시간 선택기에서 직접 옮기거나, StartQuery API의 startTime/endTime으로 정한다. Logs Insights는 2018년 11월 27일 re:Invent에서 GA 발표했고, 그 이후 AWS는 명령을 위처럼 쌓아 왔다.

자유 텍스트는 스캔이다: parse와 field indexes

쿼리가 빠르거나 느린 이유가 여기에 있다. Logs Insights는 검색 인덱스 데이터베이스가 아니라 스캔 엔진이다. CloudWatch란 무엇인가: AWS 모니터링의 중심 도구에서 "풀텍스트 검색은 OpenSearch와 Elasticsearch가 맡는다"고 짚은 그 사실이 여기서 가격으로 직결한다. 스캔이라는 말은 시간 윈도와 LogGroup 목록 안의 모든 이벤트를 한 번 읽는다는 뜻이다. 더 정확히 말하면, 한 번 읽고 메모리에서 파이프라인 명령을 적용한다.

16:9 가로 3-lane 비교 다이어그램. 흰 배경, 둥근 모서리, 절제된 그림자. 위쪽 제목 'Free text vs structured JSON vs field-indexed: three speeds of scan'. 세 개의 세로 lane을 가로로 나란히 둔다. 왼쪽 lane은 소프트 레드 #ef4444 헤더 'Free text: slow scan' 아래 코드 박스 'GET /api/users 200 35ms'와 그 아래에 낮은 위치를 가리키는 속도계 아이콘과 'parse + filter every query' 라벨. 가운데 lane은 슬레이트 #475569 헤더 'Structured JSON: auto-flattened' 아래 코드 박스 '{ status: 500, latency_ms: 35 }'와 중간 위치를 가리키는 속도계와 'filter status >= 500 directly' 라벨. 오른쪽 lane은 에메랄드 그린 #10b981 헤더 'Field-indexed: filterIndex' 아래 작은 인덱스 카드 아이콘과 코드 박스 'filterIndex status'와 가장 빠른 위치를 가리키는 속도계와 'skip groups + skip events without index' 라벨. 하단 슬레이트 캡션 'pricing is per GB scanned: less scan = less bill'

그래서 메시지가 자유 텍스트면 매 쿼리마다 parse 한 줄이 따라붙는다. parse @message /status=(?<status>\d+)/ 같은 정규식 캡처가 한 단계, 거기서 만든 statusfilter가 한 단계, 그렇게 쌓이는 단계 수가 그대로 시간이 된다. parse는 정규식 외에 글로브 패턴도 받아서 잘 정해진 형식의 줄에는 정규식보다 더 빨리 들어간다. Logs: 구조화 로그와 인덱싱 관점에서 본 옵션, 메시지를 JSON으로 적으면 Logs Insights가 자동으로 평탄화해 filter status >= 500을 바로 받는다. 같은 데이터의 운영 가능성이 그래서 한쪽에서만 산다. 파싱이 쿼리 시점이 아니라 운영 시작 시점에 한 번으로 끝나기 때문이다.

스캔 자체를 줄이는 옵션이 2024년 11월에 들어왔다. CloudWatch Logs Insights에 field indexes와 filterIndex 명령을 추가한 것. 자주 쓰는 동등 비교 필드(status, customerId 같은)에 인덱스를 만들어 두면 쿼리는 두 단계로 스캔 범위가 더 좁다. (a) 인덱스가 있는 LogGroup만 스캔에 포함, (b) 그 안에서도 인덱스된 필드가 있는 이벤트만 읽는다. filterIndex는 한 번에 LogGroup name prefix를 다섯 개까지 받아 최대 10,000개 LogGroup을 한 쿼리로 스캔할 수 있다. 단, @message, @timestamp, @log는 인덱스를 못 만든다(그 셋은 본디 자유 텍스트이거나 메타). 그리고 인덱스가 만들어진 시점 이후 들어온 이벤트에만 효과가 붙는다. 과거 로그는 그대로 풀스캔이다.

한 쿼리가 받는 한도

쿼리가 한 번에 받는 분량에 숫자가 명확하게 적혀 있다.

16:9 가로 한도 대시보드 다이어그램. 흰 배경, 둥근 모서리, 절제된 그림자. 위쪽 제목 'StartQuery limits per call and per region'. 3 컬럼 2 행으로 6장의 stat 카드가 격자로 깔린다. 카드 1 슬레이트 #475569: 큰 숫자 50 + 캡션 'log groups per query'. 카드 2 슬레이트: 큰 숫자 10000 + 캡션 'rows max - Row limit exceeded error'. 카드 3 에메랄드 그린 #10b981: 큰 텍스트 '1 hour' + 캡션 'default time window'. 카드 4 AWS 오렌지 #ff9900: 큰 숫자 100 + 캡션 'concurrent Logs Insights QL on Standard log class since 2026-03'. 카드 5 소프트 레드 #ef4444: 큰 숫자 5 + 캡션 'concurrent on IA log class'. 카드 6 앰버 #f59e0b: 큰 텍스트 '10 per sec' + 캡션 'StartQuery and GetQueryResults rate'. 하단 슬레이트 캡션 'interactive plus dashboard plus scheduled queries share the same slot pool'

StartQuery API 한 호출에 최대 50개 LogGroup이 들어간다. 결과는 limit을 안 정하면 10,000줄에서 끊고, limit 50000처럼 그보다 큰 숫자를 두면 Row limit exceeded. Maximum: 10000 에러로 거부한다. 결과가 더 필요한 경우는 시간 윈도를 좁혀 여러 번 부르거나, stats로 집계해서 줄 수를 줄인 뒤 받는다.

동시 쿼리 슬롯이 그 다음이다. Standard log class는 region당 Logs Insights QL 동시 100개까지로 2026년 3월에 늘었다(이전은 30). IA log class는 5다. 한 가지 함정. 콘솔에서 사람이 띄운 interactive 쿼리와 dashboard widget, scheduled query, alarm-triggered query가 같은 슬롯을 나눠 쓴다. 자동화 쿼리가 슬롯을 다 채우면 사람이 콘솔에서 쿼리를 못 시작하고 MaxConcurrentLimitExceededException 에러가 돌아온다. dashboard 위젯 한 페이지에 8개를 박아 두고 30초마다 자동 갱신을 켜 두는 흔한 구성에서 그 일이 자주 일어난다. API 자체의 호출 빈도도 따로 제한한다. StartQueryGetQueryResults 각각 region당 초당 10회.

저장된 쿼리(saved queries)는 region당 1,000개, 한 쿼리 안 파라미터 20개까지가 firm 한도다. 자주 쓰는 쿼리는 콘솔의 Queries 패널에 저장하면 되는데, 1,000개 한도는 한 사람이 도달하기 힘들고 팀 전체로 누적될 때만 신경 쓴다.

청구서가 결정되는 지점

여기서 한 줄이 살아남는다. Logs Insights 가격은 스캔한 GB다. US East 기준 $0.005/GB. 같은 데이터에 같은 결과를 얻어도 파이프라인 순서로 청구서가 다르다.

16:9 가로 비용 비교 다이어그램. 흰 배경, 둥근 모서리, 절제된 그림자. 위쪽 제목 'Filter first vs stats first - same data, different cost'. 가는 슬레이트 점선 분할선이 좌우 두 패널을 구분한다. 왼쪽 패널은 소프트 레드 #ef4444 헤더 'Stats first - expensive' 아래 세로 파이프라인을 그린다. 큰 둥근 박스 'fields' → 큰 박스 'stats by status (full scan)' → 박스 'filter status >= 500'에 소프트 레드 'all rows scanned' 배지. 패널 하단에 큰 stat: '500 GB scanned = $2.50'. 오른쪽 패널은 에메랄드 그린 #10b981 헤더 'Filter first - cheap' 아래 세로 파이프라인. 'fields' → 'filter status >= 500'에 에메랄드 'reduced rows' 배지 → 작은 'stats by status (partial scan)'. 패널 하단 stat: '50 GB scanned = $0.25'. 두 패널을 가로질러 깔리는 슬레이트 캡션 'Logs Insights applies commands in written order - put filter first'

# 비싸다. 모든 줄에 stats를 한 번씩
fields @timestamp, status
| stats count() by status
| filter status >= 500

# 싸다. filter가 먼저 줄 수를 줄인다
fields @timestamp, status
| filter status >= 500
| stats count() by status
# 비싸다. 모든 줄에 stats를 한 번씩
fields @timestamp, status
| stats count() by status
| filter status >= 500

# 싸다. filter가 먼저 줄 수를 줄인다
fields @timestamp, status
| filter status >= 500
| stats count() by status

Logs Insights 엔진은 명령을 쓴 순서로 적용한다. filter를 가장 앞에 두는 한 줄이 가장 큰 절감이다. 그 다음 절감이 filterIndex. 인덱스 있는 필드가 있다면 filter 부분에 filterIndex를 둔다. 둘 다 못 쓰는 경우(자유 텍스트와 인덱스 없는 그룹)에서는 시간 윈도를 좁히는 게 마지막 카드다. 스캔은 시간 × LogGroup 크기에 정확히 비례한다. 한 시간 윈도로 안 끝나는 경우는 24시간을 한 번 보지 말고 여섯 번에 나눠 보는 식으로 가는 게 비용도 시간도 같이 잡아 준다.

IA log class는 같은 가격으로 Logs Insights 쿼리가 들어가지만 pattern, diff, unmask 세 명령은 못 쓴다. 2026년 3월에 IA에서도 OpenSearch PPL과 SQL이 가능해진 부분은 그 다음. 사후 분석 로그를 IA에 두고도 같은 쿼리 도구가 닿는다.

자연어로 쿼리를 만드는 Query generator2024년 6월에 AWS가 GA로 풀었다(2023-11 preview의 후속). 2025년 8월에는 OpenSearch PPL과 SQL까지 자연어로 받게 범위를 한 단계 더 넓혔다. "지난 1시간 동안 5xx 에러를 가장 많이 낸 사용자 상위 10명" 같은 한국어, 영어 자연어를 받아 콘솔이 쿼리 한 줄을 채워 넣는다. 도구 사용은 무료이고, 만들어진 쿼리를 실제 실행할 때만 위 스캔 GB를 청구한다. 쿼리 짜는 게 막히는 상황에서 쓸 만하다.

Insights가 못 하는 영역

같은 콘솔 안에서 쿼리 언어가 셋이 산다. Logs Insights QL 옆에 OpenSearch PPL과 OpenSearch SQL이 같이 떠 있다는 점이 작은 단서다. SQL 익숙한 팀은 PPL/SQL로 들어가고, AWS 표준 쪽이 익숙한 팀은 QL로 들어간다. PPL은 JOIN, Subquery, Cidrmatch, Flatten, JSON 함수 같은 명령을 같은 콘솔에서 받게 2025년 6월에 확장됐다. Logs Insights QL의 joinsubqueries도 같은 시기에 같이 들어와서, 두 언어 모두 관계형 쿼리 형식을 한 단계 더 받는다.

그런데 여기서 한 가지 한계가 드러난다. 수 TB와 수십억 줄 규모의 풀텍스트 인덱스 검색이 필요한 경우는 Logs Insights가 맡는 영역이 아니라 OpenSearch Service가 맡는 영역이다. 인덱스 없이 스캔만으로는 그 규모의 자유 텍스트 쿼리가 비용도 시간도 안 맞는다. OpenSearch는 ingest 단계에서 색인을 만들어 두는 대신 클러스터 비용을 매월 고정으로 받는다. 같은 데이터, 다른 청구 형식. 실시간 라이브 스트림은 또 다른 영역. CloudWatch Live Tail이 시간 단위 고정 단가로 서 있다. Logs Insights가 받는 건 윈도 기준의 지난 데이터다. 콘솔의 다이얼이 "지난 1시간 / 직전 24시간 / 사용자 윈도"인 이유가 거기 있다.

filter를 가장 먼저 둔다

프로덕션에서 매번 같이 두는 결심 묶음을 적는다. filter를 가장 먼저 둔다. 인덱스 있는 필드가 있으면 그 부분에 filterIndex. 시간 윈도는 필요한 만큼만 연다. 메시지를 JSON으로 적은 그룹은 parse를 빼고 들어간다. 자주 쓰는 쿼리는 콘솔에서 저장하고 dashboard widget에 두는 대신, 스캔 GB가 큰 쿼리는 직접 띄우는 곳에만 둔다. 다섯 줄짜리 규칙이지만 청구서가 다르게 나오는 핵심은 결국 이 다섯 줄이 결정한다.

참고 자료

YouTube 영상

채널 보기
직교성과 벡터 투영 | 선형대수학
행렬의 기본 연산 - 행렬 덧셈, 스칼라 곱, 전치 | 선형대수학
트라이(Trie)를 이용한 자동 완성 알고리즘 | Trie 자료구조 이야기
벡터의 정의와 덧셈 연산 | 선형대수학
스칼라 곱셈과 내적의 기하학적 의미 | 선형대수학
마지막편, 트라이 노드를 50% 이상 줄이는 방법? 압축 트라이 성능 분석 | Trie 자료구조 이야기
내적의 기하학적 의미와 코사인 유사도 원리 | 선형대수학
숫자 하나가 AI 모델의 운명을 바꾼다? | 선형대수학