JSONL로 구조화된 로깅

JSONL(JSON Lines) 형식을 사용한 구조화된 로깅에 대한 종합 가이드입니다. ELK Stack, Fluentd, CloudWatch, GCP Logging, Azure Monitor와의 통합 방법을 프로덕션 수준의 코드 예제와 함께 배우세요.

최종 업데이트: 2026년 2월

왜 JSONL로 구조화된 로깅을 하는가?

전통적인 일반 텍스트 로그는 사람이 읽을 수 있지만 기계가 안정적으로 파싱하기가 거의 불가능합니다. '2024-01-15 ERROR User login failed for admin'과 같은 줄은 심각도, 이벤트 유형, 사용자 이름을 비구조화된 문자열 안에 묻어버립니다. 모든 로그 수집 도구는 의미를 추출하기 위해 취약한 정규 표현식에 의존해야 하며, 로그 형식의 변경은 파이프라인을 깨뜨립니다.

구조화된 로깅은 잘 정의된 필드를 가진 JSON 객체로 각 로그 항목을 발행하여 이 문제를 해결합니다. JSONL(JSON Lines)은 한 단계 더 나아갑니다: 줄당 하나의 JSON 객체, 래핑 배열 없음, 후행 쉼표 없음. 이를 통해 JSONL 로그는 추가 친화적이고, 스트리밍 가능하며, 모든 주요 로그 플랫폼에서 간단하게 파싱할 수 있습니다. 이 가이드에서는 Python, Node.js, Go에서 JSONL 로그를 생성하는 방법과 ELK, Fluentd, 세 가지 주요 클라우드 제공업체에 전송하는 방법을 배우게 됩니다.

CSV나 XML과 같은 다른 구조화 형식과 비교할 때, JSONL은 유연성, 도구 지원, 사람의 가독성에서 최상의 균형을 제공합니다. 필드는 중첩될 수 있고, 타입이 보존되며, 고정 스키마를 미리 정의할 필요가 없습니다. 이것이 JSONL이 현대 인프라에서 구조화된 로깅의 사실상 표준이 된 이유입니다.

언어별 로깅 라이브러리

대부분의 현대 로깅 라이브러리는 네이티브로 또는 간단한 설정 변경을 통해 구조화된 JSON 출력을 지원합니다. 아래는 Python, Node.js, Go에 대한 권장 라이브러리로, 각각 JSONL 호환 출력을 생성합니다.

Python: structlog

권장

structlog는 Python을 위한 선도적인 구조화된 로깅 라이브러리입니다. 표준 logging 모듈을 래핑하고 기본적으로 JSON 출력을 생성합니다. 프로세서 파이프라인을 통해 직렬화 전에 로그 항목을 보강, 필터링, 변환할 수 있습니다.

Python: structlog
import structlog
# Configure structlog for JSONL output
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.add_log_level,
structlog.processors.StackInfoRenderer(),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.BoundLogger,
context_class=dict,
logger_factory=structlog.PrintLoggerFactory(),
)
log = structlog.get_logger()
# Each call produces one JSONL line
log.info("user.login", user_id="u-42", ip="10.0.0.1")
# {"event":"user.login","user_id":"u-42","ip":"10.0.0.1","level":"info","timestamp":"2026-02-15T08:30:00Z"}
log.error("db.connection_failed", host="db-primary", retries=3)
# {"event":"db.connection_failed","host":"db-primary","retries":3,"level":"error","timestamp":"2026-02-15T08:30:01Z"}

Node.js: pino

최고 속도

pino는 설정 없이 기본적으로 JSONL 출력을 생성하는 가장 빠른 Node.js 로거입니다. 비동기 쓰기와 최소한의 직렬화 경로를 통해 낮은 오버헤드를 달성합니다. pino-pretty 동반 패키지는 로컬 개발을 위한 사람이 읽을 수 있는 출력을 제공합니다.

Node.js: pino
import pino from 'pino';
// pino outputs JSONL by default
const log = pino({
level: 'info',
timestamp: pino.stdTimeFunctions.isoTime,
});
// Each call produces one JSONL line
log.info({ userId: 'u-42', ip: '10.0.0.1' }, 'user.login');
// {"level":30,"time":"2026-02-15T08:30:00.000Z","userId":"u-42","ip":"10.0.0.1","msg":"user.login"}
log.error({ host: 'db-primary', retries: 3 }, 'db.connection_failed');
// {"level":50,"time":"2026-02-15T08:30:01.000Z","host":"db-primary","retries":3,"msg":"db.connection_failed"}
// Child loggers inherit context
const reqLog = log.child({ requestId: 'req-abc-123' });
reqLog.info({ path: '/api/users' }, 'request.start');

Go: zerolog

제로 할당

zerolog는 Go를 위한 제로 할당 JSON 로거로, 고처리량 서비스에서 최대 성능을 위해 설계되었습니다. 중간 할당 없이 io.Writer에 직접 쓰므로 지연 시간에 민감한 애플리케이션에 이상적입니다. 체인 API는 인체공학적이면서도 효율적입니다.

Go: zerolog
package main
import (
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
// Configure JSONL output to stdout
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()
// Each call produces one JSONL line
log.Info().
Str("user_id", "u-42").
Str("ip", "10.0.0.1").
Msg("user.login")
// {"level":"info","user_id":"u-42","ip":"10.0.0.1","time":1739608200,"message":"user.login"}
log.Error().
Str("host", "db-primary").
Int("retries", 3).
Msg("db.connection_failed")
}

ELK Stack 통합

ELK Stack(Elasticsearch, Logstash, Kibana)은 가장 널리 배포된 오픈소스 로그 관리 플랫폼입니다. JSONL 로그는 Logstash와 Filebeat가 JSON을 네이티브로 파싱할 수 있어 복잡한 grok 패턴이 필요 없으므로 원활하게 통합됩니다.

Filebeat는 JSONL 로그 파일을 추적하고 Logstash나 Elasticsearch로 전달하는 경량 로그 수집기입니다. json.keys_under_root를 true로 설정하면 JSON 필드가 최상위 Elasticsearch 필드로 승격되어 Kibana에서 직접 검색할 수 있습니다.

Filebeat 설정
# filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/app/*.jsonl
json.keys_under_root: true
json.add_error_key: true
json.message_key: message
json.overwrite_keys: true
output.elasticsearch:
hosts: ["http://elasticsearch:9200"]
index: "app-logs-%{+yyyy.MM.dd}"
# Optional: send to Logstash instead
# output.logstash:
# hosts: ["logstash:5044"]

인덱싱 전에 로그를 변환하거나 보강해야 하는 경우 Logstash가 Filebeat와 Elasticsearch 사이에 위치합니다. json 코덱이 각 JSONL 줄을 자동으로 파싱합니다. mutate 필터를 사용하여 저장 전에 필드 이름을 변경하고, 태그를 추가하거나, 민감한 데이터를 제거합니다.

Logstash 파이프라인
# logstash.conf
input {
beats {
port => 5044
codec => json
}
}
filter {
# Parse timestamp from the JSON field
date {
match => ["timestamp", "ISO8601"]
target => "@timestamp"
}
# Add environment tag
mutate {
add_field => { "environment" => "production" }
remove_field => ["agent", "ecs", "host"]
}
# Route by log level
if [level] == "error" or [level] == "fatal" {
mutate { add_tag => ["alert"] }
}
}
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "app-logs-%{+YYYY.MM.dd}"
}
}

Fluentd & Fluent Bit

Fluentd와 경량 버전인 Fluent Bit은 Kubernetes 환경에서 널리 사용되는 CNCF 졸업 프로젝트입니다. 두 도구 모두 JSONL 로그를 네이티브로 처리하며 Elasticsearch에서 S3, 클라우드 네이티브 로그 서비스까지 거의 모든 대상으로 전달할 수 있습니다.

Fluent Bit은 Kubernetes 및 리소스가 제한된 환경에서 선호되는 로그 수집기입니다. 약 450KB의 메모리를 소비하며 초당 수십만 개의 레코드를 처리할 수 있습니다. json 파서가 별도의 설정 없이 JSONL 입력을 처리합니다.

Fluent Bit 설정
[SERVICE]
Flush 5
Daemon Off
Log_Level info
Parsers_File parsers.conf
[INPUT]
Name tail
Path /var/log/app/*.jsonl
Parser json
Tag app.*
Refresh_Interval 5
[FILTER]
Name modify
Match app.*
Add cluster production-us-east-1
Add service my-api
[OUTPUT]
Name es
Match app.*
Host elasticsearch
Port 9200
Index app-logs
Type _doc
Logstash_Format On

Fluentd는 Fluent Bit에 비해 500개 이상의 커뮤니티 플러그인으로 더 풍부한 플러그인 생태계를 제공합니다. 고급 라우팅, 버퍼링, 변환이 필요할 때 사용하세요. in_tail 플러그인에 format json을 사용하면 JSONL 파일을 네이티브로 읽습니다.

Fluentd 설정
# fluentd.conf
<source>
@type tail
path /var/log/app/*.jsonl
pos_file /var/log/fluentd/app.pos
tag app.logs
<parse>
@type json
time_key timestamp
time_format %Y-%m-%dT%H:%M:%S%z
</parse>
</source>
<filter app.logs>
@type record_transformer
<record>
hostname "#{Socket.gethostname}"
environment production
</record>
</filter>
<match app.logs>
@type elasticsearch
host elasticsearch
port 9200
index_name app-logs
logstash_format true
<buffer>
@type memory
flush_interval 5s
chunk_limit_size 5m
</buffer>
</match>

클라우드 로깅 플랫폼

세 가지 주요 클라우드 제공업체 모두 JSONL 형식의 로그를 수용하고 JSON 필드를 검색 가능하고 필터링 가능한 속성으로 자동 파싱합니다. 이를 통해 자체 로그 인프라를 운영할 필요 없이 동일한 구조화된 쿼리 기능을 제공합니다.

AWS CloudWatch Logs

AWS

CloudWatch Logs는 JSON 로그 항목을 자동으로 파싱하여 모든 필드를 CloudWatch Logs Insights로 쿼리할 수 있게 합니다. 애플리케이션이 ECS, Lambda 또는 CloudWatch 에이전트가 있는 EC2에서 실행될 때, JSONL 출력은 추가 설정 없이 구조화된 데이터로 인덱싱됩니다. Logs Insights SQL과 유사한 구문으로 수백만 개의 로그 항목을 초 단위로 쿼리할 수 있습니다.

AWS CloudWatch Logs
# CloudWatch Logs Insights query examples
# Find all errors in the last hour
fields @timestamp, level, event, user_id
| filter level = "error"
| sort @timestamp desc
| limit 100
# Aggregate error counts by event type
fields event
| filter level = "error"
| stats count(*) as error_count by event
| sort error_count desc
# P99 latency by endpoint
fields path, duration_ms
| filter ispresent(duration_ms)
| stats pct(duration_ms, 99) as p99 by path
| sort p99 desc

GCP Cloud Logging

GCP

Google Cloud Logging(이전 Stackdriver)은 JSON 페이로드를 최상급 지원으로 구조화된 로그 항목으로 처리합니다. Cloud Run, GKE 또는 Cloud Functions에서 stdout에 JSONL을 쓰면 로깅 에이전트가 각 줄을 구조화된 LogEntry로 파싱합니다. severity 필드는 Cloud Logging 심각도 레벨에 직접 매핑되고, jsonPayload 필드는 인덱싱되어 검색 가능해집니다.

GCP Cloud Logging
# Python example for Cloud Run / Cloud Functions
import json
import sys
def log(severity: str, message: str, **fields):
"""Write a JSONL log entry compatible with GCP Cloud Logging."""
entry = {
"severity": severity.upper(), # Maps to Cloud Logging severity
"message": message,
**fields,
}
print(json.dumps(entry), file=sys.stdout, flush=True)
# GCP automatically parses these as structured logs
log("INFO", "user.login", user_id="u-42", ip="10.0.0.1")
log("ERROR", "db.timeout", host="db-primary", latency_ms=5200)
# Query in Cloud Logging:
# jsonPayload.user_id = "u-42"
# severity >= ERROR

Azure Monitor Logs

Azure

Azure Monitor는 Azure Monitor Agent 또는 직접 API 수집을 통해 JSONL 로그를 수집합니다. Application Insights는 JSON 트레이스와 사용자 정의 이벤트를 자동으로 파싱합니다. KQL(Kusto Query Language)은 집계, 시계열 분석, 이상 감지를 지원하는 강력한 구조화된 로그 데이터 쿼리를 제공합니다.

Azure Monitor Logs
// KQL queries for JSONL logs in Azure Monitor
// Find recent errors
AppTraces
| where SeverityLevel >= 3
| extend parsed = parse_json(Message)
| project TimeGenerated, parsed.event, parsed.user_id,
parsed.error_message
| order by TimeGenerated desc
| take 100
// Error rate over time (5-minute buckets)
AppTraces
| where SeverityLevel >= 3
| summarize ErrorCount = count() by bin(TimeGenerated, 5m)
| render timechart
// Top error events
AppTraces
| where SeverityLevel >= 3
| extend parsed = parse_json(Message)
| summarize Count = count() by tostring(parsed.event)
| top 10 by Count

JSONL 로깅 모범 사례

서비스 전반에 걸쳐 일관된 규칙을 따르면 JSONL 로그를 더 쉽게 쿼리, 상관 분석, 알림을 설정할 수 있습니다. 이 모범 사례는 하루에 수십억 개의 로그 항목을 처리하는 프로덕션 시스템에서 도출되었습니다.

일관된 로그 레벨 사용

표준 심각도 등급을 채택하고 모든 서비스에서 일관되게 사용하세요. 가장 일반적인 규칙은 다섯 가지 레벨을 사용합니다: 개발 진단을 위한 DEBUG, 정상 운영을 위한 INFO, 복구 가능한 문제를 위한 WARN, 주의가 필요한 실패를 위한 ERROR, 복구 불가능한 충돌을 위한 FATAL. pino의 10-60 등급과 같은 숫자 레벨은 수집 시점에 이 문자열 이름으로 매핑하여 균일한 쿼리를 가능하게 합니다.

일관된 로그 레벨 사용
{"level":"debug","event":"cache.miss","key":"user:42","timestamp":"2026-02-15T08:30:00Z"}
{"level":"info","event":"request.completed","method":"GET","path":"/api/users","status":200,"duration_ms":45,"timestamp":"2026-02-15T08:30:01Z"}
{"level":"warn","event":"rate_limit.approaching","client_id":"c-99","current":950,"limit":1000,"timestamp":"2026-02-15T08:30:02Z"}
{"level":"error","event":"payment.failed","order_id":"ord-123","error":"card_declined","timestamp":"2026-02-15T08:30:03Z"}
{"level":"fatal","event":"startup.failed","reason":"missing DATABASE_URL","timestamp":"2026-02-15T08:30:04Z"}

필드 이름 표준화

모든 서비스에 걸쳐 공통 필드의 공유 스키마를 정의하세요. 필드 이름에는 snake_case를, 타임스탬프에는 ISO 8601을, 상관 식별자에는 일관된 명명을 사용합니다. 최소한 모든 로그 항목에는 timestamp, level, event(기계가 읽을 수 있는 이벤트 이름), service가 포함되어야 합니다. 요청 범위 항목에는 추적을 위해 request_id와 user_id를 포함해야 합니다.

필드 이름 표준화
// Recommended field schema
{
"timestamp": "2026-02-15T08:30:00.000Z", // ISO 8601, always UTC
"level": "info", // debug|info|warn|error|fatal
"event": "order.created", // dot-notation event name
"service": "order-api", // emitting service
"version": "1.4.2", // service version
"request_id": "req-abc-123", // correlation ID
"trace_id": "4bf92f3577b34da6", // distributed trace ID
"user_id": "u-42", // authenticated user
"duration_ms": 120, // numeric, not string
"order_id": "ord-789", // domain-specific context
"items_count": 3 // domain-specific context
}

성능 고려사항

구조화된 로깅은 모든 로그 호출에 직렬화 오버헤드를 추가합니다. 고처리량 서비스에서는 이것이 상당해질 수 있습니다. 비동기 로깅(pino, zerolog)을 사용하여 직렬화를 핫 경로에서 분리하세요. INFO 레벨에서 큰 객체나 요청/응답 본문을 로깅하지 마세요. 대량의 디버그 로그에는 샘플링을 사용하세요. 로컬 파일에 쓰고 네트워크를 통해 동기적으로 보내는 대신 사이드카(Filebeat, Fluent Bit)가 전송을 처리하게 하세요.

비동기 로거(pino, zerolog)를 사용하여 JSON 직렬화 중 메인 스레드 블로킹을 방지하세요.

INFO 레벨에서 전체 요청이나 응답 본문을 절대 로깅하지 마세요. DEBUG 레벨을 사용하고 문제 해결 시에만 활성화하세요.

대량 이벤트를 샘플링하세요. 모든 헬스 체크 요청 대신 100개 중 1개를 로깅하세요.

로그를 로컬 파일에 쓰고 Filebeat나 Fluent Bit로 비동기적으로 전송하세요. 로그 경로에서 동기적 네트워크 호출을 피하세요.

최대 로그 줄 크기(예: 16KB)를 설정하여 과대한 항목이 파이프라인을 막는 것을 방지하세요.

logrotate 또는 내장 라이브러리 로테이션으로 로그 파일을 순환하여 디스크 소진을 방지하세요.

온라인에서 JSONL 로그 검증하기

무료 브라우저 기반 도구로 JSONL 로그 파일을 검사, 검증, 변환하세요. 업로드 불필요, 모든 처리는 로컬에서 이루어집니다.

브라우저에서 JSONL 로그 검사하기

최대 1GB의 JSONL 로그 파일을 보고, 검색하고, 검증하세요. 즉시 로딩, 클라이언트 측 처리, 100% 프라이빗.

자주 묻는 질문

JSONL 로깅 — 구조화된 로그, ELK/Fluentd & 클라우드 수집 | jsonl.co