파이썬 로그 시스템
파이썬 패키지를 개발 중이야. 프로젝트가 좀 커져서 로그 시스템을 좀 넣어야할 것 같아. 지금까지는 print문으로 했어. console 스크립트들도 포함되어 있어 참고로. 어떻게 하는게 일반적이고 좋은 방법인지 알려줘.
좋은 질문입니다. 프로젝트가 커지면 print()로는 로그 관리가 점점 힘들어지고, 유지보수나 디버깅에 비효율적이 됩니다. 파이썬에서는
일반적으로 logging 모듈을 사용하는 것이 표준이며, 다음과 같은 기준으로 설계하는 것이 좋습니다:
✅ 1. logging 모듈을 사용하는 이유
-
로그 수준(Level)을 나눌 수 있음:
DEBUG,INFO,WARNING,ERROR,CRITICAL -
콘솔 출력과 파일 저장을 동시에 할 수 있음
-
서드파티 툴과 연동하기 쉬움 (예: Sentry, Logstash 등)
-
스레드/프로세스 환경에서도 안정적
✅ 2. 기본적인 설정 예시
import logging
import sys
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG) # 개발 중엔 DEBUG, 배포 시엔 INFO 이상
formatter = logging.Formatter(
'[%(asctime)s] %(levelname)s - %(name)s - %(message)s'
)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
file_handler = logging.FileHandler('your_package.log')
file_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
위 코드는 모든 로그를 콘솔과 파일로 동시에 출력하며, 추후 모듈 내에서
logger.debug(),logger.info()등으로 사용 가능하게 됩니다.
✅ 3. 콘솔 스크립트와 연동할 때
스크립트 진입점(예: main() 함수)에서 로그 초기화를 수행하세요:
def main():
import your_package.logging_setup # 로그 초기화
logger = logging.getLogger(__name__)
logger.info("Script started.")
혹은 entry_points에서 사용하는 CLI 툴이 있다면, CLI 진입점에서도 로그 초기화를 포함시키면 됩니다.
✅ 4. 모듈 간 로그 분리
각 모듈 파일에서 다음처럼 작성하세요:
# your_package/some_module.py
import logging
logger = logging.getLogger(__name__)
이러면 로그 이름이
your_package.some_module식으로 명시되므로, 어떤 파일에서 찍은 로그인지 명확하게 추적 가능합니다.
✅ 5. 설정을 모듈로 분리하면 더 좋음
your_package/logging_setup.py 같은 파일에 설정 코드를 몰아두고, 패키지 초기화 시점에 호출하도록 구성하는 것이
유지보수에 유리합니다.
🔁 요약
-
logging모듈 사용은 사실상 필수 -
로그 포맷, 핸들러(Console/File), 레벨 설정을 분리
-
각 모듈마다
logger = getLogger(__name__)사용 -
진입점에서 설정 호출
-
환경에 따라 로깅 설정을 바꾸려면
logging.config.dictConfig도 고려할 수 있음
추가적으로 콘솔 스크립트가 많다면, CLI 도구로 click이나 argparse를 쓰고, 로그 레벨을 인자로 조절할 수 있게 해두는
것도 일반적인 패턴입니다.
원하시면 예제 CLI 스크립트도 보여드릴 수 있습니다.
logging_setup은 어디어디 해야하지?
좋은 질문입니다. logging_setup을 어디서, 어떻게 호출할지가 사실상 핵심인데, 아래 경우들을 나눠서 보겠습니다.
✅ 1. 패키지 내부 코드 (import 돼서 사용되는 모듈들)
이 경우는 로깅 초기화를 직접 하지 않습니다. 그냥 각 모듈에서 logger =
logging.getLogger(__name__)만 해놓습니다.
# your_package/module_a.py
import logging
logger = logging.getLogger(__name__)
def do_something():
logger.debug("doing something")
로깅 설정은 다른 데서 한 번만 하면 됩니다. 모듈에서는 설정을 "사용"만 하는 구조로 가야 합니다.
✅ 2. 스크립트 진입점 (예:main.py, CLI 스크립트)
여기서 한 번만 logging_setup.setup_logging() 같은 걸 호출하면 됩니다. 예:
# your_package/__main__.py
from your_package.logging_setup import setup_logging
def main():
setup_logging()
# 그 다음부터는 로거 쓰면 됨
import logging
logger = logging.getLogger(__name__)
logger.info("Script started.")
이 구조를 쓰면,
-
모듈 안에서는 로깅 설정 몰라도 됨
-
여러 CLI 진입점이 있어도 공통
setup_logging()만 호출하면 일관성 유지 가능
✅ 3. 패키지 초기화 시점에 넣을 수 있나? (__init__.py)
이건 비추 입니다. 이유는 다음과 같습니다:
-
import할 때마다 로그 설정이 바뀌면 예측 불가 -
사용자 프로젝트에서 로그 설정을 다르게 하고 싶을 수도 있음
-
로깅 설정은 사이드이펙트 크기 때문에 명시적으로 제어하는 게 좋음
✅ 4. 구조 예시
your_package/
├── __init__.py
├── logging_setup.py ← 여기서 setup_logging 정의
├── cli.py ← CLI 진입점
└── module_a.py ← 내부 로직
# logging_setup.py
import logging
import sys
def setup_logging(level=logging.INFO):
logger = logging.getLogger()
if logger.handlers:
return # 중복 설정 방지
logger.setLevel(level)
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('[%(asctime)s] %(levelname)s - %(name)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
# cli.py
from your_package.logging_setup import setup_logging
def main():
setup_logging()
# ...
🔁 정리
| 위치 | 해야 할 일 |
|---|---|
일반 모듈 (module_x.py) |
getLogger(__name__)만 써라 |
| CLI 진입점, 스크립트 | setup_logging() 딱 한 번 호출 |
__init__.py |
로깅 설정하지 말 것 |
| 여러 스크립트가 있다면 | 공통 logging_setup.py를 만들어 다 같이 사용 |
필요하시면 setuptools entry_points 구조에서도 어떻게 설정하는지 예시로 보여드릴 수 있습니다.
사용자 옵션에 따라 로그 수준을 조절할 수 있어? entry에서
예, 사용자 옵션에 따라 로그 레벨 조절은 충분히 가능하고 , 실제로 많은 CLI 도구들이 그렇게 합니다. 일반적으로는
--verbose 또는 --log-level 옵션을 사용합니다.
아래에 예시를 드리겠습니다.
✅ 1. argparse로 로그 레벨 설정 받기
# cli.py
import argparse
from your_package.logging_setup import setup_logging
import logging
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"--log-level",
default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="Set the logging level",
)
return parser.parse_args()
def main():
args = parse_args()
setup_logging(level=getattr(logging, args.log_level.upper()))
logger = logging.getLogger(__name__)
logger.info("Started with log level %s", args.log_level)
실행 예시
$ python -m your_package.cli --log-level DEBUG
✅ 2. click으로 하는 경우 (더 직관적인 CLI)
# cli.py
import click
import logging
from your_package.logging_setup import setup_logging
@click.command()
@click.option(
"--log-level",
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
default="INFO",
help="Set logging level."
)
def main(log_level):
setup_logging(level=getattr(logging, log_level.upper()))
logger = logging.getLogger(__name__)
logger.debug("This is a debug message")
logger.info("This is an info message")
✅ 3. setup_logging 함수에서 level 인자를 받게 설계
이미 앞에서 정의한 함수 구조가 여기에 맞게 되어 있어야 합니다:
# logging_setup.py
def setup_logging(level=logging.INFO):
root_logger = logging.getLogger()
if root_logger.handlers:
return # 중복 설정 방지
root_logger.setLevel(level)
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('[%(asctime)s] %(levelname)s - %(name)s - %(message)s')
handler.setFormatter(formatter)
root_logger.addHandler(handler)
✅ 4. entry_points로 연결 시에도 적용됨
setup.py 또는 pyproject.toml에 다음처럼 등록되어 있다면:
[project.scripts]
mytool = "your_package.cli:main"
유저는 그냥 CLI로 다음과 같이 쓸 수 있습니다:
$ mytool --log-level DEBUG
🔁 결론
-
가능할 뿐 아니라 표준적인 사용 방식
-
argparse나click을 통해 CLI 옵션으로 로그 레벨 입력받기 -
setup_logging(level=...)식으로 전달 -
entry_points에서도 동작 잘함
필요하시면 click 기반 CLI 템플릿도 만들어 드릴 수 있습니다.
pytest 같은데서 돌릴 땐 어떻게 되지?
아주 중요한 포인트를 짚으셨습니다. pytest처럼 테스트 환경에서 로깅이 어떻게 동작하느냐 는 생각보다 까다롭고, 잘못 구성하면
로그가 안 보이거나 테스트 로직과 충돌합니다. 아래 정리해드리겠습니다.
✅ 기본적으로 pytest는 로깅을 캡처함
pytest는 테스트 실행 중에 stdout, stderr, logging 모두를 캡처 해서 테스트 실패 시에만
보여주도록 되어 있습니다. 그래서 일반적으로는 logger.info() 등을 써도 콘솔에 바로 안 나옵니다.
✅ 해결 방법 요약
| 목표 | 방법 |
|---|---|
| 로그를 테스트 중 항상 출력 | pytest -s 또는 pytest --capture=no |
| 로그 레벨을 조절하고 싶다 | --log-cli-level=DEBUG 등 사용 |
| 로그 설정 충돌 피하고 싶다 | 테스트용 로깅 설정 따로 구성하거나 caplog fixture 사용 |
✅ 방법 1: 테스트 실행 시 로그를 콘솔에 출력
pytest -s
또는
pytest --capture=no --log-cli-level=DEBUG
✅ 방법 2: caplog을 이용한 테스트 내 로그 확인
caplog은 pytest의 built-in fixture로, 테스트 중 발생한 로그를 수집합니다.
# test_something.py
import logging
def test_log_output(caplog):
logger = logging.getLogger("your_package.module_x")
with caplog.at_level(logging.INFO):
logger.info("Hello from test")
assert "Hello from test" in caplog.text
✅ 방법 3: 테스트 실행 시 setup_logging() 실행?
기본적으로는 하지 않는 게 낫습니다.
-
이유: 테스트 실행 전에 로깅 설정하면
pytest의 내부 로그 캡처 방식과 충돌 가능 -
하지만 특정 모듈을 직접 테스트할 때 로그가 필요하다면 ,
setup_logging()을 명시적으로 불러도 됩니다
# test_setup.py
from your_package.logging_setup import setup_logging
setup_logging(level=logging.DEBUG)
단, 이 설정은 전역 영향을 주므로 주의해야 합니다. pytest.ini 또는 conftest.py에서 공통 설정을 하거나,
caplog처럼 테스트 스코프 안에서 제어하는 것이 더 안정적입니다.
✅ 옵션: pytest.ini 설정에 로그 옵션 추가
pytest.ini를 프로젝트 루트에 두고 다음처럼 설정할 수 있습니다:
# pytest.ini
[pytest]
log_cli = true
log_cli_level = INFO
log_format = [%(asctime)s] %(levelname)s - %(message)s
log_date_format = %Y-%m-%d %H:%M:%S
이렇게 하면 별도로 setup_logging() 호출 없이도, pytest가 실행될 때 로그를 출력하게 됩니다.
🔁 정리
-
pytest는 기본적으로 로그를 캡처하므로 바로 출력되진 않음 -
콘솔에 로그를 보려면
-s,--capture=no,--log-cli-level등을 써야 함 -
테스트 중 로그 확인은
caplog를 쓰는 게 가장 깔끔함 -
setup_logging()은 테스트용 별도로 구성하거나 최소한으로 사용하는 게 좋음 -
공통 설정은
pytest.ini에서 컨트롤 가능
테스트 환경용 setup_test_logging() 함수를 따로 만드는 것도 하나의 방법입니다. 원하시면 그런 형태도 예시로 드릴 수
있습니다.
setup을 안하면 그냥 stdout으로 나오나?
아니요, setup_logging() 같은 설정을 명시적으로 하지 않으면 로그는 대부분의 경우 “아무 데도 안 나옵니다.”
(즉, stdout에도 안 나옵니다).
이유는 다음과 같습니다:
✅ 기본 logging 동작 (설정 안 했을 때)
파이썬의 logging 모듈은 내부적으로 다음 조건에서만 로그를 출력합니다:
-
루트 로거에 핸들러가 연결되어 있어야 함
-
로거의 로그 레벨보다 낮지 않은 메시지만 출력
-
기본적으로는 아무 핸들러도 없음 → 출력 안 됨
예를 들어, 아래 코드는 print()는 출력되지만 logger.debug()는 콘솔에 안 나옵니다:
import logging
logger = logging.getLogger(__name__)
logger.debug("this won't show") # 출력 안 됨
print("this will show") # 출력됨
✅ 예외: logging.basicConfig()를 어디선가 호출한 경우
logging.basicConfig(level=logging.INFO)
이걸 어딘가에서 한 번이라도 호출했다면, stdout으로 출력됩니다. 하지만:
-
모듈 import 순서에 따라 불안정하게 동작함
-
중복 호출하면 무시됨 (
basicConfig()는 한 번만 적용됨)
그래서 basicConfig()는 복잡한 프로젝트에서는 피해야 하는 방식입니다. 대신, 아예 StreamHandler와
Formatter를 명시적으로 지정하는 구조로 가는 게 낫습니다 (지금까지 설명드린 방식처럼요).
✅ 그럼 테스트에서 setup_logging() 안 하면?
-
caplog같은 pytest fixture로는 내부 로그는 잡히지만, 콘솔 출력은 없음 -
setup_logging()을 하지 않으면, 모듈 내logger.info()는 그냥 사라진다고 보면 됩니다
🔁 요약
| 상태 | 결과 |
|---|---|
| 아무 설정도 안 한 경우 | 로그 출력 안 됨 |
basicConfig()만 호출한 경우 |
단순한 로그 출력 가능 (stdout) |
setup_logging() 명시적으로 구성 |
원하는 방식으로 핸들러/포맷 등 제어 가능 |
pytest 실행 중 |
캡처됨 (콘솔에 바로 출력 안 됨) |
그래서 현실적으로는:
-
테스트 외부에서는
setup_logging()이 필요 -
테스트 내부에서는
caplog나pytest.ini사용 -
basicConfig()는 임시 스크립트 말고는 피하는 게 좋음
필요하시면 테스트용 minimal logging 구성도 예시로 드릴 수 있습니다.
테스트할 때도 로그 메시지를 보고 싶을 것 같은데 나는
네, 그 말씀이 맞습니다. 테스트 중에도 로그 메시지를 보고 싶은 경우가 많고 , 디버깅/문제 재현/원인 추적에 굉장히 유용합니다. 다만 pytest의 기본 설정이 로그를 캡처해서 감추기 때문에, 명확하게 설정 을 해줘야 합니다.
아래에 현실적으로 가장 안정적인 설정 방법 3가지를 정리해 드리겠습니다:
✅ 방법 1: pytest.ini 또는 pyproject.toml에 로깅 출력 설정
가장 추천하는 방식 입니다. 프로젝트 전체 테스트에서 항상 로그를 출력해줍니다.
pytest.ini 예시:
[pytest]
log_cli = true
log_cli_level = INFO
log_format = [%(asctime)s] %(levelname)s - %(name)s - %(message)s
log_date_format = %Y-%m-%d %H:%M:%S
이 설정의 효과:
-
logger.info()등 로그 메시지가 테스트 실행 중 콘솔에 출력됨 -
setup_logging()을 호출하지 않아도 됨 (pytest가 root logger를 설정함)
레벨을 DEBUG로 설정하면 너무 많을 수 있으니, INFO 정도로 시작하는 것이 좋습니다.
✅ 방법 2: pytest 실행 시 CLI 옵션으로 지정
테스트 실행 시 직접 로그 설정을 넘길 수 있습니다:
pytest --log-cli-level=DEBUG
또는 로그 캡처도 끄고 싶다면:
pytest --capture=no --log-cli-level=DEBUG
이 방법은 테스트를 수동으로 실행할 때 유용 하지만, 자동화된 CI 환경에는 반영되지 않으니
pytest.ini가 더 낫습니다.
✅ 방법 3: setup_logging()을 테스트 코드에서 직접 호출
테스트 전용의 간단한 로그 설정을 만들어 직접 불러줄 수도 있습니다:
# conftest.py
import pytest
from your_package.logging_setup import setup_logging
@pytest.fixture(autouse=True, scope="session")
def configure_logging():
setup_logging(level="INFO")
이렇게 하면 테스트를 실행할 때 한 번만
setup_logging()이 호출됩니다.
⚠️ 단점:
-
pytest.ini와 충돌 가능 -
caplog를 쓸 때 간섭할 수 있음
✅ 번외: 테스트 내에서 특정 로그만 보고 싶을 때 (caplog)
테스트 내에서 로그 출력 여부를 검증하고 싶다면 caplog fixture를 쓰면 됩니다:
def test_logging_behavior(caplog):
from your_package import something
with caplog.at_level("INFO"):
something.do_work()
assert "expected log message" in caplog.text
🔁 결론: 당신 같은 경우엔?
프로젝트 전체에 로그 출력이 필요한 경우라면 → pytest.ini에 설정하는 방법이 가장 깔끔하고 안전합니다.
즉시 해결하려면 아래처럼 하세요:
-
pytest.ini생성 (없으면) -
아래 내용 추가:
[pytest]
log_cli = true
log_cli_level = INFO
- 이제 그냥
pytest만 쳐도 로그 출력됩니다.
원하시면 pytest.ini 외에도 CI 환경에서 이 설정을 반영하는 방법도 알려드릴 수 있습니다.
저거는 그러면 pytest가 logging 패키지 설정을 덮어쓰는거야?
예, 맞습니다.pytest는 내부적으로 logging 설정을 “덮어씁니다.”
좀 더 정확히 말하면:
✅ pytest의 로깅 동작 핵심 요약
-
log_cli = true를 설정하거나--log-cli-level같은 옵션을 사용하면 -
pytest가 root logger에
StreamHandler(sys.stderr)를 등록하고 포맷/레벨을 설정합니다 -
당신이
setup_logging()같은 걸 미리 호출했다면?
👉 대부분의 경우 pytest 설정이 그것을 덮어씁니다 (특히 handler를 추가하는 부분에서)
🔬 실제 내부 동작 (간단화된 형태)
pytest 내부적으로는 다음과 같은 방식으로 작동합니다:
root_logger = logging.getLogger()
root_logger.setLevel(log_cli_level)
handler = StreamHandler(sys.stderr)
handler.setFormatter(user_configured_formatter)
root_logger.addHandler(handler)
즉,
-
root_logger.setLevel(...)도 바꿔버림 -
handler를 추가함 -
이미 존재하는 핸들러가 있을 경우 충돌 가능
🧠 그러면 어떻게 해야 안전할까?
상황 A. pytest에서만 로그 설정하고 싶다
→ pytest.ini + log_cli=true로 끝. setup_logging() 안 써도 됨.
상황 B. setup_logging()도 쓰고 싶다 (예: CLI 실행 시)
→ setup_logging()에서 pytest 실행 시는 건너뛰는 로직 을 넣는 게 안전합니다.
import logging
import sys
import os
def setup_logging(level=logging.INFO):
if any("PYTEST_CURRENT_TEST" in k for k in os.environ):
# pytest 실행 중이면 설정 안 함
return
root_logger = logging.getLogger()
if root_logger.handlers:
return # 이미 설정된 경우 생략
root_logger.setLevel(level)
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('[%(asctime)s] %(levelname)s - %(name)s - %(message)s')
handler.setFormatter(formatter)
root_logger.addHandler(handler)
🔁 결론
-
네,
pytest는 기본적으로logging설정을 덮어씁니다 -
log_cli관련 옵션이 있으면 확실히 root logger 설정을 바꿉니다 -
setup_logging()이 있는 경우, pytest 실행 환경에서는 생략하거나 안전하게 조절해야 충돌 방지 됩니다
필요하시면 setup_logging()과 pytest.ini가 잘 공존하는 구조를 템플릿으로 만들어 드릴 수도 있습니다.
종합 요구 사항: 1. 로그는 파일 + 표준 출력 모두한다. 표준 출력 시에는 수준별로 색깔을 다르게 한다. 파일에는 색깔 포매팅을 넣지 않는다. 2. pytest 시에도 로그 표준 출력을 한다. 단 (-s) 옵션 줬을 때만
좋습니다. 말씀하신 요구사항은 실무에서도 꽤 자주 등장하는 고급 로깅 설정입니다. 말씀하신 두 가지 요구사항을 충족시키는 전체 구조를 아래와 같이 설계할 수 있습니다:
✅ 요구사항 정리
-
로그는 파일 + 콘솔 모두 출력
-
콘솔 출력에는 색상 적용 (레벨별로 다르게)
-
파일에는 색상 없이 저장
-
pytest 실행 시에는
-s옵션을 줬을 때만 콘솔 로그 출력 (기본은 출력 안 함)
✅ setup_logging.py: 로깅 설정 모듈
# your_package/logging_setup.py
import logging
import sys
import os
class ColorFormatter(logging.Formatter):
COLORS = {
'DEBUG': '\033[94m', # 파랑
'INFO': '\033[92m', # 초록
'WARNING': '\033[93m', # 노랑
'ERROR': '\033[91m', # 빨강
'CRITICAL': '\033[95m', # 마젠타
}
RESET = '\033[0m'
def format(self, record):
levelname = record.levelname
if levelname in self.COLORS:
record.levelname = f"{self.COLORS[levelname]}{levelname}{self.RESET}"
return super().format(record)
def setup_logging(level=logging.INFO, log_file="your_package.log"):
root_logger = logging.getLogger()
if root_logger.handlers:
return # 중복 방지
root_logger.setLevel(level)
# 공통 포맷
fmt = '[%(asctime)s] %(levelname)s - %(name)s - %(message)s'
datefmt = '%Y-%m-%d %H:%M:%S'
# 파일 핸들러 (색 없음)
file_handler = logging.FileHandler(log_file)
file_handler.setFormatter(logging.Formatter(fmt, datefmt))
root_logger.addHandler(file_handler)
# pytest 실행 중이면 표준 출력 핸들러 추가 조건 확인
is_pytest = "PYTEST_CURRENT_TEST" in os.environ
if not is_pytest or (is_pytest and sys.stdout.isatty()):
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setFormatter(ColorFormatter(fmt, datefmt))
root_logger.addHandler(stream_handler)
✅ 설명
-
ColorFormatter: 콘솔 출력용. ANSI 색코드로 수준별 색상 처리 -
FileHandler: 로그 파일 저장, 색상 없음 -
sys.stdout.isatty():pytest -s로 실행했을 때만True가 됩니다. 따라서-s옵션이 있을 때만 콘솔 출력하도록 제어 가능 -
PYTEST_CURRENT_TEST환경변수: pytest 실행 여부를 판단할 수 있는 표준적이고 안전한 방법
✅ 사용 예시
# your_package/__main__.py or cli.py
from your_package.logging_setup import setup_logging
import logging
def main():
setup_logging()
logger = logging.getLogger(__name__)
logger.info("Started something")
✅ pytest에서 사용 시
# 콘솔 로그 안 나옴 (파일에만 저장됨)
pytest
# 콘솔 로그 출력됨 (isatty가 True)
pytest -s
이 구조는 CLI 사용 과 테스트 환경 을 모두 안정적으로 커버합니다.
✅ 보너스: 로그 파일 경로를 디폴트 외로 바꾸고 싶다면?
setup_logging(log_file="logs/output.log") 식으로 인자를 받아 처리하면 됩니다.
또는 ENV 값으로 파일 위치를 바꿀 수도 있습니다.
필요하시면 이 구조를 패키지 전체에 반영하는 디렉터리 템플릿까지 만들어 드릴 수 있습니다.
pytest에서 setup_logging을 해줘야해?
결론부터 말씀드리면:
❌pytest에서는 별도로 setup_logging()을 호출할 필요가 없습니다.
(단, 테스트 중 로그를 “파일로 저장”하고 싶다면 호출해야 합니다.)
🔍 왜 기본적으로 호출할 필요가 없나?
당신이 설정한 setup_logging() 함수는 다음 조건을 체크합니다:
is_pytest = "PYTEST_CURRENT_TEST" in os.environ
if not is_pytest or (is_pytest and sys.stdout.isatty()):
# 콘솔 핸들러 추가
즉, pytest -s로 실행하면 콘솔 핸들러를 추가하고,
그 외의 경우엔 콘솔 핸들러를 추가하지 않습니다.
하지만 이 코드는 자동으로 호출되지 않습니다.
따라서 pytest가 알아서 setup_logging()을 실행해주지는 않기 때문에 “파일로도 남기고 싶다”거나, 특정
포맷/수준으로 통일하려면 명시적으로 설정을 불러야 합니다.
✅ 정리: 호출 여부 기준
| 상황 | setup_logging() 호출 여부 |
|---|---|
pytest -s에서 콘솔 로그만 보고 싶다 |
❌ 호출 안 해도 됨 (pytest 자체 로그 출력 기능 사용) |
| 로그를 파일에도 저장하고 싶다 | ✅ 호출해야 함 (pytest는 파일로 저장 안 함) |
| 콘솔 출력 포맷/색상 커스터마이징이 필요하다 | ✅ 직접 설정 필요 |
로그 메시지를 테스트에서 검증하고 싶다 (caplog) |
❌ pytest 내부 캡처만으로 충분 |
🧪 추천: 테스트 시 자동으로 설정하려면 conftest.py
# conftest.py (테스트 루트 디렉토리)
import pytest
from your_package.logging_setup import setup_logging
@pytest.fixture(scope="session", autouse=True)
def _init_logging():
setup_logging()
단, 이 방식은 항상 로깅이 파일로 기록되므로, 테스트 격리성을 중요시하는 환경에선 조심해야 합니다.
✍️ 결론
-
pytest에서 콘솔 출력만 원한다면 :-s옵션 +setup_logging()은 호출하지 않아도 된다 -
파일 로그까지 남기고 싶거나 포맷을 통일하고 싶다면:
setup_logging()명시 호출 필요 -
자동화를 원한다면
conftest.py에서 초기화 가능
추가로 caplog와 setup_logging()을 병행할 때 생기는 문제도 다룰 수 있습니다. 원하시면 말씀 주세요.
이해가 잘 안되네. pytest에서는 setup_logging 호출을 아얘 안할건데 # pytest 실행 중이면 표준 출력 핸들러 추가 조건 확인 is_pytest = "PYTEST_CURRENT_TEST" in os.environ if not is_pytest or (is_pytest and sys.stdout.isatty()): stream_handler = logging.StreamHandler(sys.stdout) stream_handler.setFormatter(ColorFormatter(fmt, datefmt)) root_logger.addHandler(stream_handler) 이 코드는 왜 있는거야? 그리고 toml 파일 설정해야한다고 했지 않아? 설정법도 한번에 다 정리해줘
좋습니다. 아주 핵심적인 질문이십니다. 지금 정확히 짚고 넘어가야 할 부분이 두 가지입니다:
✅ 질문 1: setup_logging() 안 쓸 거면, 거기 있는 is_pytest 조건문은 왜 있나?
정확히 보셨습니다.
setup_logging() 자체를 pytest에서 호출하지 않는다면, 거기에 있는 is_pytest 분기:
is_pytest = "PYTEST_CURRENT_TEST" in os.environ
if not is_pytest or (is_pytest and sys.stdout.isatty()):
...
→ 전혀 실행되지 않습니다.
즉, pytest 실행 여부를 고려한 분기는setup_logging()을 테스트 환경에서 실행할 경우에만 의미가 있습니다.
그러므로 다음과 같은 결론이 납니다:
당신이 테스트에서는
setup_logging()을 호출하지 않기로 했다면, 저is_pytest체크 로직은 필요 없습니다.
✅ 질문 2: 그럼 로그 설정은 어디서 해야 하냐? pytest 실행 시엔?
setup_logging() 없이도 pytest에서 로그를 출력하고 싶다면, pyproject.toml이나
pytest.ini에 로깅 설정을 넣으면 됩니다.
✅ 추천 방식 (당신 요구사항 기준)
🧾 pyproject.toml 또는 pytest.ini에 아래 설정을 추가
📌 예시: pyproject.toml
[tool.pytest.ini_options]
log_cli = true
log_cli_level = "INFO"
log_cli_format = "[%(asctime)s] %(levelname)s - %(name)s - %(message)s"
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
📌 또는 pytest.ini 버전
[pytest]
log_cli = true
log_cli_level = INFO
log_cli_format = [%(asctime)s] %(levelname)s - %(name)s - %(message)s
log_cli_date_format = %Y-%m-%d %H:%M:%S
✅ 위 설정의 의미
| 옵션 | 역할 |
|---|---|
log_cli |
표준 출력으로 로그를 출력함 (-s 없이도 가능) |
log_cli_level |
출력할 최소 로그 수준 |
log_cli_format |
로그 포맷 지정 |
log_cli_date_format |
날짜 포맷 지정 |
즉, 이 설정만 있으면:
-
pytest실행 시,setup_logging()없이도 로그가 콘솔에 출력됨 -
별도 코드 불필요
-
로그 파일 기록은 없음 (이건
setup_logging()을 써야 함)
🔁 최종 정리
| 목적 | 해야 할 일 |
|---|---|
| 테스트 시 콘솔 출력만 하고 싶다 | pyproject.toml 또는 pytest.ini에 설정 추가 (log_cli = true) |
| 테스트 시 로그를 파일로 저장하고 싶다 | setup_logging()을 conftest.py에서 호출 (예: autouse fixture) |
| 테스트 시에도 색상 있는 출력 원한다 | setup_logging()을 직접 호출해야 가능 (ANSI 색은 pytest 설정에선 불가) |
CLI 실행 시에는setup_logging() 필수 |
콘솔 + 파일 로그 모두 설정됨 |
🧠 결론
당신이 설정한 setup_logging() 안의 is_pytest 조건문은,
"pytest에서setup_logging()을 호출할 경우에만 의미 있음".
그런데 당신은 pytest에서setup_logging()을 호출 안 하기로 했으므로,
→ 그 코드는 의미 없음
→ 지워도 됨 , 더 깔끔해짐
필요하시면 pyproject.toml, conftest.py, setup_logging.py 전체 파일 구조 예시도 만들어드릴 수
있습니다.
이렇게 하면 pytest -s 했을 때만 출력돼?
아니요.
pytest.ini나 pyproject.toml에 log_cli = true를 설정하면,
pytest -s 없이도 항상 로그가 출력됩니다.
✅ 다시 말해:
[pytest]
log_cli = true
log_cli_level = INFO
이 설정은 다음과 같이 작동합니다:
| 명령어 | 로그 출력됨? | 설명 |
|---|---|---|
pytest |
✅ 출력됨 | 로그 캡처를 끄고 콘솔에 직접 출력 |
pytest -s |
✅ 출력됨 | -s는 stdout 캡처도 해제함 |
pytest --capture=no |
✅ 출력됨 | -s와 유사 |
즉, log_cli = true는 pytest가 자체적으로 stdout/stderr를 로그 전용 핸들러로 대체하여 콘솔 로그
출력 을 항상 활성화 시키는 설정입니다. -s 여부와 무관합니다.
❗ 당신의 요구사항 다시 보면
"pytest 시에도 로그 표준 출력을 한다. 단 (-s) 옵션 줬을 때만"
이걸 만족하려면:
-
log_cli = true를 쓰면 안 됩니다. (항상 출력되니까) -
대신,
setup_logging()에서sys.stdout.isatty()가 True일 때만 핸들러를 붙이는 방식을 직접 호출 해야 합니다.
✅ 진짜로 -s일 때만 로그 출력하는 방식 정리
-
pytest.ini/toml은 아예 설정 안 함 (log_cli = falseor 생략) -
로그 출력은 오직
setup_logging()을 통해 하되, 조건을 이렇게 둠:
import os
import sys
def setup_logging(...):
if "PYTEST_CURRENT_TEST" in os.environ:
if not sys.stdout.isatty():
return # pytest + -s 아님 → 출력 X
# 그 외엔 일반 로그 설정
conftest.py또는 테스트에서 이걸 직접 호출
# conftest.py
import pytest
from your_package.logging_setup import setup_logging
@pytest.fixture(autouse=True, scope="session")
def _init_logging():
setup_logging()
🔁 요약
| 방법 | -s일 때만 출력? |
비고 |
|---|---|---|
pytest.ini + log_cli=true |
❌ 항상 출력됨 | 편하긴 하지만 요구사항 불일치 |
setup_logging() + isatty() 조건 |
✅ 만족 | 직접 호출 필요 |
✅ 최종 결론
당신이 원한 정확한 동작 (오직 -s일 때만 출력) 을 구현하려면,
pytest.ini설정은 쓰지 말고,setup_logging()내부에isatty()조건을 사용해야 합니다.
필요하시면 그 조건 포함한 확정 코드 템플릿 드릴 수 있습니다.
보통은 어떻게 하지
좋습니다. 이 질문은 본질적입니다.
"현실적인 Python 프로젝트에서 테스트 시 로그를 어떻게 다루느냐"는
실제 팀/오픈소스/라이브러리들이 어떤 트레이드오프를 택했는지를 봐야 이해가 됩니다.
✅ 실무/오픈소스에서 보통 하는 방식 요약
| 방식 | 선택 이유 | 단점 |
|---|---|---|
pytest 설정(log_cli = true)으로 항상 출력 |
간단함, 설정 한 줄이면 끝 | 로그가 너무 많아질 수 있음. 테스트 결과 보기 힘듦 |
| 테스트에서는 로그 숨기고 실패 시만 출력 (기본) | 테스트 결과 깔끔, CI에 적합 | 실패 전까지 로그 확인 어려움 |
조건부 출력 (-s일 때만 출력) |
필요한 상황에만 출력 가능 | 구현 복잡도 증가, pytest와 충돌 주의 |
| 로그 파일로만 기록, 콘솔 출력 없음 | CI에서 아카이브 가능 | 개발 중 디버깅 불편 |
| caplog fixture 사용으로 개별 테스트에서 로그 확인 | 테스트 검증 가능 | 설정은 간단, 하지만 일일이 caplog 사용 필요 |
✅ 일반적인 권장 흐름
-
기본은 로그를 안 보이게 한다
→pytest기본 동작 그대로 (캡처된 상태) -
로컬 개발자가 보고 싶으면
-s또는--log-cli-level=INFO사용
→ 필요할 때만 출력. 깔끔함과 디버깅 둘 다 확보 -
CI 환경에선 로그 파일로 저장
→ 문제가 생기면 로그 파일을 분석 가능 -
모듈은 항상
getLogger(__name__)만 쓰고, 로깅 설정은 진입점 또는 테스트 init에서 함
→ 설정 위치를 명확히 하고 예측 가능하게 함
✅ 오픈소스 예시들
-
requests,urllib3,pydantic등 대형 라이브러리들:-
내부에서
logging.getLogger(__name__)만 씀 -
로그 설정은 전적으로 사용자에게 맡김
-
테스트 중 로그 출력 없음 (
caplog사용하거나, 실패 시 출력)
-
-
pytest자체 테스트:log_cli는 사용 안 하고, 필요할 경우만 명시적으로 로그 출력
-
CLI 도구 (e.g.
poetry,black):-
CLI 실행 시
setup_logging()명시 호출 -
테스트에서는 로그 안 찍음 or 파일로만 기록
-
🧠 실무에서 이걸 어떻게 해석해야 하나?
“로컬 디버깅 편의” vs “CI/테스트 깔끔함” 사이에서 밸런스를 잡아야 합니다.
-
로컬에서는
-s나--log-cli-level=DEBUG로 출력 가능하게 두고, -
기본 동작은 출력 안 하되,
-
파일 기록은 해두면 나중에 추적할 수 있고,
-
setup_logging()은 명시적으로 호출하는 구조로 하면 예측 가능하고 제어 가능 합니다.
✍️ 결론
보통은
pytest.ini에log_cli = true를 넣고 항상 출력하게 하거나, 아예 안 하고 -s 옵션에 맡깁니다.
당신처럼 “-s일 때만 출력”을 엄격히 원하는 경우는 커스텀 조건을 넣는 팀/개인들이 일부 존재합니다. 하지만 기본/일반은 아닙니다.
—
필요하시면 "가장 보편적이고 실용적인 구성" 예제를 전체 프로젝트 구조로 정리해드릴 수 있습니다.
caplog은 어떻게 쓰는거야?
좋습니다. caplog은 pytest가 제공하는 로그 캡처 전용 fixture 입니다.
테스트 중 발생한 logging 메시지를 수집하고 검증 할 수 있게 해줍니다.
✅ caplog을 언제 쓰나?
-
특정 로그 메시지가 발생했는지 검증하고 싶을 때
-
stdout이나 파일 로그가 아닌, 메모리 내 로그 버퍼를 확인 하고 싶을 때
-
setup_logging()같은 설정 없이도 동작함
✅ caplog 기본 사용법
# test_module.py
import logging
logger = logging.getLogger("my_module")
def some_function():
logger.info("Running some_function")
def test_logging_output(caplog):
with caplog.at_level(logging.INFO):
some_function()
assert "Running some_function" in caplog.text
설명:
-
caplog을 fixture로 인자로 받음 -
caplog.at_level(...)로 캡처할 로그 레벨 설정 -
caplog.text: 전체 캡처된 로그 문자열 -
개별 로그 레코드는
caplog.records에서 리스트로 접근 가능
✅ caplog.text vs caplog.records
caplog.text: 문자열로 모든 로그 모아보기
print(caplog.text)
# 예: [2025-07-21 00:00:00] INFO - my_module - Running some_function
caplog.records: logging.LogRecord 객체 리스트
for record in caplog.records:
assert record.levelname == "INFO"
assert record.message.startswith("Running")
✅ 여러 로거에서 발생한 로그도 다 캡처됨
def test_multi_logger(caplog):
import logging
logger1 = logging.getLogger("a")
logger2 = logging.getLogger("b")
with caplog.at_level("WARNING"):
logger1.warning("Warn from a")
logger2.error("Error from b")
assert "Warn from a" in caplog.text
assert "Error from b" in caplog.text
✅ 사용 시 주의점
| 항목 | 내용 |
|---|---|
| 로그 레벨 안 맞으면 안 잡힘 | with caplog.at_level(...) 안 쓰면 기본 WARNING 이상만 잡힘 |
setup_logging()이 caplog와 충돌 가능 |
root_logger.setLevel(...)이 pytest의 캡처를 방해할 수 있음 |
로그 포맷, 색상 등은 caplog와 무관 |
오직 메시지, 수준, 로거 이름 등 내용 중심 검증용 |
✅ 실제 테스트 검증 예시 (정리된 형태)
def test_error_logged(caplog):
import logging
logger = logging.getLogger("my_module")
with caplog.at_level(logging.ERROR):
logger.error("Something failed!")
assert any("Something failed!" in msg for msg in caplog.messages)
-
caplog.messages:record.getMessage()결과 리스트 (색상, 포맷 없음) -
이 방식이 가장 단순하고 안전함
🔁 요약
| 기능 | caplog 방법 |
|---|---|
| 로그 수집 | caplog.text, caplog.records, caplog.messages |
| 레벨 지정 | with caplog.at_level(logging.INFO): |
| 포맷 없이 메시지만 보기 | caplog.messages |
| 복수 로거 확인 | 자동으로 다 캡처됨 |
필요하시면 caplog을 활용한 고급 테스트 예제 (예: 로그가 안 나와야 한다든가, 특정 로거만 필터링한다든가) 도 만들어 드릴 수
있습니다.
유저한테 알려주기 위한 info도 logging써서 전달해도되나?
좋은 질문입니다.
"사용자에게 알려줄 메시지도logging.info()로 출력해도 되는가?"
→ 정답은: 상황에 따라 다르지만, 일반적으로는"되긴 하지만 조심해야 한다"** 입니다.**
✅ 언제 logging.info()로 사용자 메시지를 출력해도 괜찮나?
-
CLI 도구 에서, stdout으로 메시지를 간단히 보여줄 때
-
디버그/진행상황 정도 를 보여줄 때 ("Processing…", "Done!" 등)
-
스크립트 실행 로그와 사용자 메시지가 구분되지 않아도 괜찮을 때
예:
logger = logging.getLogger(__name__)
logger.info("All files processed successfully.")
→ 콘솔에 찍히긴 하지만, 로그 포맷이 [2025-07-21 10:00:00] INFO - ... 식이면 사용자가 "이게 뭐지?" 할 수
있음
❌ 언제는 logging이 부적절한가?
-
명확한 사용자 커뮤니케이션 이 필요한 경우:
-
에러 메시지, 경고, 주의사항
-
UX 메시지 ("입력이 잘못되었습니다", "업데이트가 완료되었습니다")
-
-
로그 포맷이 사용자를 혼란시킬 수 있는 경우
[시간] LEVEL - logger - 메시지형태로 나오면 사용자는 그게 "로그"인지, "출력"인지 헷갈립니다
✅ 그럼 어떻게 구분하면 좋은가?
✔ 구조 분리 방식 (추천)
def notify_user(msg):
print(msg) # 사용자에게 보여줄 메시지
def main():
logger.info("Starting processing")
notify_user("🔄 작업을 시작합니다...")
이렇게 print()나 사용자 출력 전용 함수(click.echo() 등)를 따로 두는 것이 일반적입니다.
✅ 실제 예: click CLI의 경우
import click
import logging
logger = logging.getLogger(__name__)
@click.command()
def main():
logger.debug("Command started")
click.echo("✅ 사용 준비 완료!")
-
logger.debug()→ 로그 파일 또는 디버깅용 -
click.echo()→ 사용자에게 직접 전달할 메시지 (포맷 없음)
✅ 혼합 사용도 가능 (주의해서)
logger.info("Fetching user info") # 로그 기록용
print("📦 사용자 정보 가져오는 중...") # 사용자 전달용
이렇게 로그와 사용자 메시지를 구분해서 이중 출력 하는 것도 하나의 패턴입니다.
다만 메시지가 중복되거나 복잡해지면 함수로 추상화하는 게 좋습니다.
🔁 정리
| 상황 | logging.info() 사용 적합성 |
|---|---|
| 내부 상태 기록 | ✅ 적합 |
| 사용자 피드백 | ⚠️ 조건부 (간단한 안내 정도는 OK) |
| 에러/경고/UX 메시지 | ❌ 부적합 — print 또는 별도 함수 권장 |
| CLI UX 친화성 중요 | ❌ 로그 포맷은 혼란을 줄 수 있음 |
✅ 결론
"사용자에게 직접 전달할 메시지"는
print()나click.echo()로 하고,
logging은 시스템/개발자/디버깅을 위한 메시지 기록용으로 사용하는 것이 일반적이고 명확합니다.
필요하시면 CLI용 출력 유틸 (echo_success, echo_error, echo_info) 같은 것도 만들어 드릴 수
있습니다.
click.echo랑 print의 차이는?
좋은 질문입니다.
click.echo()와 print()는 겉보기에는 비슷하지만, CLI 환경에서 신뢰성과 이식성 면에서 중요한 차이점 들이
있습니다.
✅ 기본 차이 요약
| 항목 | print() |
click.echo() |
|---|---|---|
| 출력 대상 | sys.stdout 고정 |
sys.stdout 또는 sys.stderr (선택 가능) |
| 유니코드 처리 | 환경 따라 깨질 수 있음 | 깨짐 방지, 자동 인코딩 처리 |
| CLI 환경 대응 | 제한적 (TTY 확인 X) | shell 환경, 리디렉션 등 대응 더 좋음 |
| 사용 목적 | 범용 Python 출력 | CLI 툴용 사용자 메시지 출력에 최적화 |
| 테스트/리디렉션 대응 | 수동 처리 필요 | click.testing.CliRunner와 통합됨 |
✅ 유니코드/인코딩 처리
print("✅ 작업 완료") # 일부 윈도우/리눅스 환경에서 깨짐 가능
click.echo("✅ 작업 완료") # click이 내부적으로 안전한 인코딩 사용
특히 Windows + cmd 환경에서는
print()가 깨질 가능성이 있음
click.echo()는locale.getpreferredencoding()을 기반으로 처리해서 훨씬 안전함
✅ stderr 지원
click.echo("오류 발생", err=True)
→ 이 메시지는 sys.stderr로 나가므로, 표준 출력과 분리되어 리디렉션 시 구분 가능
✅ 테스트 통합 (중요)
click.echo()는 click.testing.CliRunner와 완벽하게 호환됩니다.
from click.testing import CliRunner
def test_cli_output():
runner = CliRunner()
result = runner.invoke(main_command)
assert "작업 완료" in result.output
반면
print()는 stdout 직접 캡처해야 하고 테스트가 복잡해짐
✅ 결론
| 용도 | 추천 방식 |
|---|---|
| 간단한 스크립트 출력 | print()도 OK |
| CLI 툴 사용자 메시지 | ✅ click.echo()가 더 안전하고 강력 |
| 유니코드/에러 메시지 분리/테스트 연동 등 필요 | 무조건 click.echo() |
🔁 요약 문장
print()는 그냥 찍는 함수고,click.echo()는 CLI 도구용으로 더 견고하게 만든 출력 함수 입니다.
CLI를 만들고 있다면, 사용자에게 보여줄 메시지는click.echo()가 정석입니다.
필요하시면 echo_info(), echo_error() 같은 헬퍼 함수도 드릴 수 있습니다.