---
title: "필드로 검색할 수 있는 로그 포맷과 로그 레벨로 구조화 로깅하기"
description: "구조화 로깅은 값을 문장 안에 넣는 대신 값마다 자기 필드에 담아요. 주문 1건을 3가지 방법으로 LogBrew에 보내고, 어떤 로그 포맷이 order_id로 다시 찾아지는지 보여 드려요."
canonical: "https://logbrew.co/ko/guides/structured-logging"
last_updated: "2026-11-10"
---

# 필드로 검색할 수 있는 로그 포맷과 로그 레벨로 구조화 로깅하기

구조화 로깅은 값을 문장 안에 넣는 대신 값마다 자기 필드에 담아요. 주문 1건을 3가지 방법으로 LogBrew에 보내고, 어떤 로그 포맷이 `order_id`로 다시 찾아지는지 보여 드려요.

Python 3.13.5 및 logbrew-sdk 0.1.29 환경에서 2026년 11월 10일에 테스트

글 [Furkan Erday](https://github.com/furkanerday). LogBrew 창업자 Furkan Erday. 매일 Claude Code와 Codex로 LogBrew를 만들어요.

Canonical: https://logbrew.co/ko/guides/structured-logging

## `order_id`로 다시 찾을 수 있는 로그 줄

| id를 넣은 위치 | 텍스트 검색 | 필드 필터 |
| --- | --- | --- |
| 메시지 텍스트 안 | 찾음 | 못 찾음 |
| `extra={}` 필드 안 | 못 찾음 | 찾음 |
| `extra={}` 안의 dict 안 | 못 찾음 | 못 찾음 |

주문 1건을 3가지 방법으로 Python 핸들러를 통해 보낸 뒤 검색했어요. 가득 찬 머그잔은 검색으로 그 줄을 찾았다는 뜻이에요.

## 필터할 수 있는 로그 포맷 고르기

### 메시지는 고정하고 값은 필드에 넣기

필드는 이름과 타입을 유지해요. 메시지 텍스트 안의 값은 그냥 부분 문자열일 뿐이에요. Python에서는 필드를 `extra={}`에 넣으세요. 그러면 모든 주문에서 메시지가 같아져요.

```bash
log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
```

### 프로그램이 로그를 읽는다면 JSON 줄로 쓰기

`Formatter` 하위 클래스가 레코드마다 표준 필드와 모든 `extra` 값을 담은 JSON 객체 1개를 1줄로 만들어요. 로그 수집기는 정규식 없이 그 줄을 파싱해요.

```bash
import json
import logging

RECORD_FIELDS = set(vars(logging.LogRecord("", 0, "", 0, "", (), None))) | {"message", "asctime"}


class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        line = {
            "time": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }
        line.update({k: v for k, v in vars(record).items() if k not in RECORD_FIELDS})
        if record.exc_info:
            line["exception"] = self.formatException(record.exc_info)
        return json.dumps(line)


handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])
log = logging.getLogger("taproom")

log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
log.warning("keg below 10%", extra={"keg": "ipa", "percent": 8})
```

출력

```text
{"time": "2026-11-10T08:34:04+0300", "level": "INFO", "logger": "taproom", "message": "order paid", "order_id": "ord_42", "amount_cents": 1999}
{"time": "2026-11-10T08:34:04+0300", "level": "WARNING", "logger": "taproom", "message": "keg below 10%", "keg": "ipa", "percent": 8}
```

### 필드를 LogBrew에 보내기

`LogBrewLoggingHandler`는 레코드마다 `extra` 값을 필드로 보내요. 같은 주문을 메시지 텍스트 안에, `extra` 필드로, 그리고 `extra` 안의 dict로, 3가지 방법으로 보냈어요.

```bash
import logging
import os

from logbrew_sdk import HttpTransport, LogBrewClient, LogBrewLoggingHandler

client = LogBrewClient.create(api_key=os.environ["LOGBREW_SERVER_API_KEY"])
transport = HttpTransport()
log = logging.getLogger("taproom")
log.addHandler(LogBrewLoggingHandler(client, transport, metadata={"service": "taproom"}))
log.setLevel(logging.INFO)

logging.getLogger("taproom.text").info("order %s paid, %d cents", "ord_42", 1999)
logging.getLogger("taproom.fields").info(
    "order paid", extra={"order_id": "ord_42", "amount_cents": 1999}
)
logging.getLogger("taproom.nested").info(
    "order paid", extra={"order": {"id": "ord_42", "amount_cents": 1999}}
)

response = client.flush(transport)
print("HTTP", response.status_code, "accepted", response.accepted_events, "logs")
```

출력

```text
HTTP 200 accepted 3 logs
```

### 필드로 그 줄 다시 찾기

`GET /api/logs`는 필드 경로, 타입, 연산자, 값으로 된 `attribute_filters`를 받아요. `order_id` 필터는 `extra`로 보낸 줄만 찾았어요. `extra` 안의 dict는 버려졌어요. 핸들러가 문자열, 숫자, 불리언, `None`만 남기기 때문이에요.

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode 'attribute_filters=[{"path":"/metadata/order_id","type":"string","operator":"eq","value":"ord_42"}]' \
  | jq -c '.[] | {message, order_id: .attributes.metadata.order_id}'
```

출력

```text
{"message":"order paid","order_id":"ord_42"}
```

## 모든 서비스에서 로그 레벨을 같은 방식으로 쓰기

### 프로덕션에서는 레벨 4개 쓰기

일어난 일에는 `info`, 잘못됐지만 처리된 일에는 `warning`, 실패한 요청이나 작업에는 `error`, 서비스를 멈추는 일에는 `critical`을 쓰세요. `debug`는 로컬 실행용으로 남겨 두세요. LogBrew 핸들러는 DEBUG 레코드를 `info`로 보내요.

### 레벨은 시작할 때 1번만 정하기

레벨을 환경 변수에서 읽으세요. 그러면 호스트 1대만 DEBUG로 돌리고 나머지는 INFO로 둘 수 있어요. Python에서는 `logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"))`로 충분해요.

### 숫자는 숫자로 기록하기

`amount_cents: 1999`는 타입을 유지해서 `amount_cents >= 1000` 필터에 걸려요. `amount_cents: "1999"`를 텍스트로도 보내 봤는데, 숫자 필터는 그 줄을 건너뛰었어요.

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode 'attribute_filters=[{"path":"/metadata/amount_cents","type":"number","operator":"gte","value":1000}]' \
  | jq -c '.[] | {message, amount_cents: .attributes.metadata.amount_cents}'
```

출력

```text
{"message":"order paid","amount_cents":1999}
```

## 직접 테스트해 보니

주문 1건을 3가지 방법으로 Python 핸들러를 통해 보낸 뒤 LogBrew API로 검색했어요. 텍스트 검색 `search=ord_42`는 3줄 중 1줄, id가 메시지 안에 있는 줄만 찾았어요.

`order_id = ord_42` 필터는 `extra={"order_id": "ord_42"}`로 보낸 줄 1개를 찾았어요. `amount_cents >= 1000` 필터도 같은 줄을 찾았어요. 핸들러가 1999를 숫자로 유지했기 때문이에요.

`extra={"order": {...}}`로 보낸 줄은 아무것에도 걸리지 않았어요. 핸들러는 `extra`에서 문자열, 숫자, 불리언, `None`만 보내고 dict나 list는 건너뛰어요.

```bash
curl -sS -G "https://api.logbrew.co/api/logs" \
  -H "Authorization: Bearer $LOGBREW_TOKEN" \
  --data-urlencode "project_id=$LOGBREW_PROJECT" \
  --data-urlencode "since=2026-11-10T05:31:33Z" \
  --data-urlencode "search=ord_42" \
  | jq -c '.[] | {message, order_id: .attributes.metadata.order_id}'
```

출력

```text
{"message":"order ord_42 paid, 1999 cents","order_id":null}
```

## 일반 텍스트와 JSON 로그 포맷

일반 `%(message)s` 포맷은 `extra` 값을 모두 버려요. 아래 줄은 주문 id 없이 `INFO order paid`로 끝나서 나중에 아무도 그 주문을 찾을 수 없어요.

JSON 줄은 필드와 타입을 유지하고, 로그 수집기는 정규식 없이 파싱해요. 사람이 읽기에는 덜 편하니 메시지는 짧게, 필드 이름은 바꾸지 말고 유지하세요.

Python에는 내장 JSON 포매터가 없어요. python/cpython#77691이 2018년에 하나를 요청했지만, 문서에 이미 만드는 법이 있다는 이유로 닫혔어요. 위의 15줄짜리 `Formatter`가 바로 그 방법이에요.

```bash
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("taproom")

log.info("order paid", extra={"order_id": "ord_42", "amount_cents": 1999})
```

출력

```text
2026-11-10 08:34:04,955 INFO order paid
```

## LogBrew가 기록하는 것

로그마다 레벨, 로거 이름, `extra`의 모든 기본 타입 필드, 그리고 핸들러의 `metadata={}` 필드를 유지해요. API는 필드 경로, 타입, 연산자로 필터해요. 텍스트는 `eq`, `ne`, `contains`, `starts_with`를, 숫자는 `gt`, `gte`, `lt`, `lte`를 받아요.

LogBrew는 이름에 `password`, `secret`, `token`, `cookie`, `authorization`이 들어간 필드는 색인하지 않아요. 그래서 필터에 절대 걸리지 않아요. 어쨌든 비밀 정보는 로그 줄에 넣지 마세요.

트레이스 안에서 보낸 로그는 트레이스 id와 스팬 id를 함께 가져요. 그래서 `order_id` 필터가 그 줄을 만든 요청까지 이어져요.

## 자주 묻는 질문

### 구조화 로깅이 뭔가요?

값을 문장 속 단어로 넣는 대신, 값마다 타입이 있는 이름 붙은 필드로 기록하는 로깅이에요. JSON이거나 로그 레코드의 필드예요. 그러면 검색 도구가 부분 문자열이 아니라 필드로 필터해요.

### 로그 레벨 순서는 어떻게 되나요?

DEBUG, INFO, WARNING, ERROR, CRITICAL 순서이고, Python에서는 숫자 10부터 50까지예요. INFO로 설정한 로거는 INFO 이상을 통과시키고 DEBUG는 버려요.

### JSON과 일반 텍스트 중 뭘로 로그를 남겨야 하나요?

수집기, 파일, 검색 도구처럼 프로그램이 줄을 읽는다면 JSON이에요. 사람만 터미널에서 읽는다면 일반 텍스트예요. 둘 다 같은 필드를 유지하세요.

### Python 로그 줄에 필드를 어떻게 더하나요?

로그 호출에 `extra={"order_id": "ord_42"}`를 넘기세요. 표준 포매터는 extra 필드를 무시해요. JSON 포매터나 LogBrew 핸들러는 유지해요.

### LogBrew에서 로그를 필드로 검색할 수 있나요?

네. `GET /api/logs`는 `attribute_filters`를 4개까지 받아요. 각각 필드 경로, 타입, 그리고 `eq`나 `gte` 같은 연산자로 이루어져요.

## 어떤 필드로든 로그 필터하기

LogBrew는 기록한 모든 필드를 유지하고, 그 줄이 속한 트레이스와 이슈 옆에서 필드로 다시 찾아 줘요. 넉넉한 무료 요금제로 시작하세요. 카드 등록은 필요 없어요.

[무료로 시작](https://logbrew.co/ko)

## 출처

- [Python docs, Logging Cookbook](https://docs.python.org/3/howto/logging-cookbook.html)
- [Python docs, LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes)
- [python/cpython#77691](https://github.com/python/cpython/issues/77691)

## 에이전트와 Python 가이드 더 보기

- [Claude Code와 Codex에 MCP 서버 추가하기](https://logbrew.co/ko/guides/mcp-server-claude-code-codex)
- [Claude Code와 Codex에서 AGENTS.md 쓰기](https://logbrew.co/ko/guides/agents-md)
- [Claude overloaded 529 오류와 rate limit 429 오류 해결하기](https://logbrew.co/ko/guides/claude-overloaded-rate-limit-error)
- [Python 로깅을 올바른 로그 레벨과 예외 기록까지 설정하기](https://logbrew.co/ko/guides/python-logging)
- [Claude Code, Codex, JavaScript, Python 오류 해결법](https://logbrew.co/ko/guides)
