---
title: "Python 로깅을 올바른 로그 레벨과 예외 기록까지 설정하기"
description: "Python logging은 설정하기 전까지 WARNING 이상만 출력해요. 3줄 설정, 로그 레벨 5가지, 예외를 트레이스백과 함께 기록하는 방법을 Python 3.13에서 직접 실행해 보고 정리했어요."
canonical: "https://logbrew.co/ko/guides/python-logging"
last_updated: "2026-11-10"
---

# Python 로깅을 올바른 로그 레벨과 예외 기록까지 설정하기

Python `logging`은 설정하기 전까지 WARNING 이상만 출력해요. 3줄 설정, 로그 레벨 5가지, 예외를 트레이스백과 함께 기록하는 방법을 Python 3.13에서 직접 실행해 보고 정리했어요.

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/python-logging

## 설정 없이, 그리고 LogBrew에서 Python 로그 레벨별로 일어나는 일

| 로깅 호출 | 설정 없이 출력 | LogBrew의 레벨 |
| --- | --- | --- |
| `log.debug()` | 버려짐 | `info`로 합쳐짐 |
| `log.info()` | 버려짐 | `info` |
| `log.warning()` | `WARNING:root:` | `warning` |
| `log.error()` | `ERROR:root:` | `error` |
| `log.critical()` | `CRITICAL:root:` | `critical` |

아무 설정 없이 루트 로거로 5번 호출한 뒤, 같은 5번을 LogBrew 핸들러로 보냈어요. 가득 찬 머그잔은 출력됐거나 자기 레벨 그대로 남았다는 뜻이에요.

## 3줄로 로깅 설정하기

### 첫 로그 호출 전에 basicConfig()를 1번 호출하기

`basicConfig()`는 루트 로거의 레벨과 줄 포맷을 정해요. 아무 설정이 없으면 Python은 `WARNING:root:keg below 10%`처럼 WARNING 이상만 출력하고 DEBUG와 INFO는 버려요.

```bash
import logging

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

log.debug("keg pressure read")
log.info("tap opened")
log.warning("keg below 10%")
log.error("keg empty")
log.critical("cooler offline")
```

출력

```text
2026-11-10 08:34:04,828 INFO taproom tap opened
2026-11-10 08:34:04,828 WARNING taproom keg below 10%
2026-11-10 08:34:04,828 ERROR taproom keg empty
2026-11-10 08:34:04,828 CRITICAL taproom cooler offline
```

### 모듈마다 로거 이름 붙이기

`logging.getLogger(__name__)`는 모듈마다 자기 로거를 만들어 줘요. 그래서 모든 줄에 어느 모듈에서 나왔는지 이름이 남아요. 루트 로거는 `basicConfig()`에만 쓰세요.

### 환경 변수에서 레벨 읽기

`basicConfig()`는 레벨 이름을 문자열로도 받아요. 이 코드를 `LOG_LEVEL=DEBUG`로 실행하면 코드를 바꾸지 않아도 debug 줄이 나타나요.

```bash
import logging
import os

logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO"))
log = logging.getLogger("taproom")
log.debug("keg pressure read")
log.info("tap opened")
```

출력

```text
DEBUG:taproom:keg pressure read
INFO:taproom:tap opened
```

### 다시 설정하려면 force=True 넘기기

루트 로거에 이미 핸들러가 있으면 Python은 2번째 `basicConfig()` 호출을 무시해요. 테스트처럼 핸들러를 바꿔야 할 때는 `force=True`를 넘기세요.

```bash
import logging

logging.basicConfig(level=logging.WARNING)
logging.basicConfig(level=logging.DEBUG)
logging.debug("second basicConfig: not printed")
logging.basicConfig(level=logging.DEBUG, force=True)
logging.debug("basicConfig with force=True: printed")
```

출력

```text
DEBUG:root:basicConfig with force=True: printed
```

## 모든 예외를 트레이스백과 함께 기록하기

### except 안에서 log.exception() 호출하기

`log.exception()`은 ERROR 레벨로 기록하고 트레이스백을 덧붙여요. 다른 레벨에서 같은 효과를 내려면 그 호출에 `exc_info=True`를 넘기세요.

```bash
import logging

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s %(message)s")
log = logging.getLogger("taproom")
taps = {"ipa": 12}

def pour(beer):
    return taps[beer]

try:
    pour("stout")
except KeyError:
    log.exception("pour failed")
```

출력

```text
ERROR taproom pour failed
Traceback (most recent call last):
  File "app.py", line 11, in <module>
    pour("stout")
    ~~~~^^^^^^^^^
  File "app.py", line 8, in pour
    return taps[beer]
           ~~~~^^^^^^
KeyError: 'stout'
```

### 로깅 핸들러로 각 줄을 LogBrew에 보내기

로거에 `LogBrewLoggingHandler`를 추가하세요. 레코드마다 레벨, 로거 이름, 파일, 줄 번호를 가진 LogBrew 로그 1개가 돼요. 예외 이름과 메시지, `extra={}`에 넣은 값도 함께 와요.

```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)

taps = {"ipa": 12}

def pour(beer):
    return taps[beer]

try:
    pour("stout")
except KeyError:
    log.exception("pour failed", extra={"order_id": "ord_42"})

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

출력

```text
HTTP 200 accepted 1 logs
```

### 예외 필드와 함께 로그 다시 읽기

`logbrew read logs --severity error --json`은 `exceptionName`, `exceptionMessage`, 파일, 함수, 줄 번호, 그리고 `order_id`가 담긴 줄을 돌려줘요.

```bash
logbrew read logs --project <project_id> --severity error --since 1h --json | jq '.[0] | {level, message, logger: .attributes.logger, metadata: .attributes.metadata}'
```

출력

```text
{
  "level": "error",
  "message": "pour failed",
  "logger": "taproom",
  "metadata": {
    "exceptionMessage": "'stout'",
    "exceptionName": "KeyError",
    "fileName": "send.py",
    "functionName": "<module>",
    "levelName": "ERROR",
    "levelNumber": 40,
    "lineNumber": 20,
    "module": "send",
    "order_id": "ord_42",
    "processName": "MainProcess",
    "service": "taproom",
    "threadName": "MainThread"
  }
}
```

### 프로세스를 끝내는 크래시 잡기

`install_process_error_capture()`는 `sys.excepthook`에 연결돼요. 아무도 잡지 않은 예외는 `handled: false`가 붙은 `critical` 레벨 이슈가 되고, 프로세스가 코드 1로 끝나기 전에 기록돼요.

```bash
import atexit
import json
import os

from logbrew_sdk import LogBrewClient, RecordingTransport, install_process_error_capture

transport = RecordingTransport.always_accept()
client = LogBrewClient.create(api_key=os.environ["LOGBREW_SERVER_API_KEY"], transport=transport)
install_process_error_capture(client, on_capture_error=print)


@atexit.register
def show_captured_issue() -> None:
    issue = json.loads(transport.last_body())["events"][0]["attributes"]
    print(json.dumps({k: issue[k] for k in ("title", "level")}), json.dumps(issue["exception"]))


kegs = {"ipa": 3}
kegs["stout"] -= 1
```

출력

```text
Traceback (most recent call last):
  File "<stdin>", line 19, in <module>
KeyError: 'stout'
{"title": "KeyError", "level": "critical"} {"type": "KeyError", "mechanism": {"type": "python.excepthook", "handled": false}}
```

## 직접 테스트해 보니

아무 설정 없이 루트 로거로 5번 호출하니 stderr에 3줄이 찍히고 DEBUG와 INFO 호출은 버려졌어요. 기본 줄은 `WARNING:root:keg below 10%`처럼 레벨, 로거 이름, 메시지로 이루어져요.

2번째 `basicConfig()` 호출은 `force=True`를 넘기기 전까지 아무것도 바꾸지 않았어요. LogBrew 핸들러를 거치면 DEBUG와 INFO 레코드는 `info`로, WARNING은 `warning`으로, ERROR는 `error`로, CRITICAL은 `critical`로 도착했어요.

`log.exception()`은 LogBrew 로그에 `exceptionName`과 `exceptionMessage`를 더했어요. 핸들러에 `include_exception_text=True`를 주니 전체 트레이스백이 `exceptionText`로 도착했어요.

```bash
import logging

logging.debug("keg pressure read")
logging.info("tap opened")
logging.warning("keg below 10%")
logging.error("keg empty")
logging.critical("cooler offline")
```

출력

```text
WARNING:root:keg below 10%
ERROR:root:keg empty
CRITICAL:root:cooler offline
```

## 로그 레벨 5가지와 쓰는 때

DEBUG는 버그를 고치는 동안 필요한 값을 위한 레벨이에요. 케그 압력, SQL 문장, 다시 시도 횟수 같은 것들이요. 프로덕션에서는 꺼 두세요.

INFO는 탭을 열었다거나 주문이 결제됐다처럼 프로그램이 한 일을 기록해요. WARNING은 케그가 10% 아래로 내려간 것처럼 잘못됐지만 처리된 일을 기록해요. ERROR는 케그가 빈 것처럼 실패한 요청이나 작업을 기록해요. CRITICAL은 냉각기가 꺼진 것처럼 프로그램을 멈추는 일을 기록해요.

예외는 처리하는 곳에서 1번만 기록하세요. 호출 스택의 모든 단계에서 줄을 남기면 실패 1건에 알림 3개가 가요.

```bash
import logging

for name in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
    print(name, logging.getLevelName(name))
print("root logger level:", logging.getLevelName(logging.getLogger().level))
```

출력

```text
DEBUG 10
INFO 20
WARNING 30
ERROR 40
CRITICAL 50
root logger level: WARNING
```

## LogBrew가 기록하는 것

레코드마다 레벨, 로거 이름, 파일 이름, 함수, 줄 번호, 모듈을 가진 로그 1개가 돼요. `extra={}`의 기본 타입 값은 필터로 거를 수 있는 필드가 되고, 핸들러의 `metadata={}`는 위의 `service`처럼 모든 줄에 필드를 더해요.

예외가 있으면 로그에 `exceptionName`과 `exceptionMessage`가 붙어요. 트레이스백 텍스트까지 저장하려면 핸들러에 `include_exception_text=True`를 넘기세요.

`install_process_error_capture()`는 잡히지 않은 예외를 `critical` 레벨 이슈로 바꿔요. 예외 타입, 스택 프레임, 메커니즘 `python.excepthook`이 함께 남아요.

## 자주 묻는 질문

### Python의 기본 로깅 레벨은 뭔가요?

WARNING이에요. `basicConfig()` 호출이 없으면 루트 로거는 WARNING, ERROR, CRITICAL을 `WARNING:root:message` 형태로 stderr에 출력하고 DEBUG와 INFO는 버려요.

### logging.basicConfig가 아무 효과가 없는 이유는 뭔가요?

루트 로거에 이미 핸들러가 있어서예요. 앞선 `basicConfig()` 호출이나 라이브러리가 붙였을 수 있고, 그러면 Python은 호출을 무시해요. 핸들러를 바꾸려면 `force=True`를 넘기세요.

### logging.error와 logging.exception은 뭐가 다른가요?

`log.exception()`은 `exc_info=True`를 넘긴 `log.error()`예요. ERROR 레벨로 기록하고 트레이스백을 덧붙여요. `except` 블록 안에서만 호출하세요.

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

DEBUG 10, INFO 20, WARNING 30, ERROR 40, CRITICAL 50이에요. 로거는 레코드 숫자가 설정한 레벨 이상일 때 통과시켜요.

### Python에서 print()와 logging 중 뭘 써야 하나요?

`logging`을 쓰세요. `print()`에는 레벨도, 타임스탬프도, 로거 이름도, 핸들러도 없어요. `logging`을 쓰면 모듈별로 줄을 켜고 파일이나 LogBrew로 보낼 수 있어요.

## 모든 Python 예외를 트레이스백과 함께 보기

LogBrew Python SDK는 로그 줄마다 레벨, 로거, 파일, 줄 번호를 함께 보내고, 잡히지 않은 예외를 스택 트레이스가 담긴 이슈로 바꿔요. 넉넉한 무료 요금제로 시작하세요. 카드 등록은 필요 없어요.

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

## 출처

- [Python docs, Logging HOWTO](https://docs.python.org/3/howto/logging.html)
- [Python docs, logging module](https://docs.python.org/3/library/logging.html)
- [python/cpython#78078](https://github.com/python/cpython/issues/78078)

## 에이전트와 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)
- [필드로 검색할 수 있는 로그 포맷과 로그 레벨로 구조화 로깅하기](https://logbrew.co/ko/guides/structured-logging)
- [Claude Code, Codex, JavaScript, Python 오류 해결법](https://logbrew.co/ko/guides)
