만쥬의 개발일기

이번에는 python의 로깅을 자세히 파고들어봤다.

지난번 tqdm에 이어서 파이썬 로깅 라이브러리인 rich를 실제 프로젝트에 적용해보았다.

 

우선 가상환경에 라이브러리를 설치한다.

pip install rich

 

다음 명령어로 설치 여부를 확인해 줄 수 있다.

python -m rich

로깅 포맷

logging 모듈을 사용할 때에는 로깅 메시지의 포맷을 커스터마이징 할 수 있다.

사용 가능한 어트리뷰트는 공식 문서에 자세히 나와있다.

ex)

"%(asctime)s - %(levelname)s — %(funcName)s:%(lineno)d — %(message)s"

# 위 포매팅 설정 결과는 아래와 같다.
# 2021-07-01 12:29:53,182 - INFO - <module>:1 - hello world

로깅 핸들러

핸들러(Handler)는 로깅 메시지를 특정 대상으로 전달하는 수단이다. logging 모듈을 통해 생성하는 Logger 객체에 원하는 만큼의 핸들러를 추가할 수 있다.

 

각각의 핸들러에는 별도의 설정을 통해 다른 포맷의 메시지를 전달할 수도 있다.

일반적으로 많이 사용하는 핸들러는 FileHandlerStreamHandler 이다.

  • FileHandler : 디스크 내 파일에 로깅 메시지를 전달하여 저장한다.
  • StreamHandler : 스트림(ex. 콘솔창)에 로그 메시지를 전달해 출력한다.

본 글에서는 rich 모듈을 이용하여 StreamHandler를 대체한다.

로깅 인스턴스 생성하기

현재의 목표는 다음과 같다.

  1. 콘솔에 출력될 StreamHandler로 rich 모듈의 RichHandler를 추가한다.
  2. 특정 경로에 저장하는 FileHandler를 추가한다.
  3. FileHandler에는 여러 파일이 추가될 수 있고, 같은 파일을 접근할 때 중복이 일어나지 않도록 한다.

예시 코드는 다음과 같다.

Logger/myLogger.py

import logging
import logging.handlers

from rich.logging import RichHandler

RICH_FORMAT = "[%(filename)s:%(lineno)s] >> %(message)s"
FILE_HANDLER_FORMAT = "[%(asctime)s]\t[%(levelname)s] %(message)s\t>>[%(filename)s:%(funcName)s:%(lineno)s]"

def set_logger(log_path) -> logging.Logger:
    logging.basicConfig(
        level=logging.INFO,
        format=RICH_FORMAT,
        handlers=[RichHandler(rich_tracebacks=True)]
    )
    logger = logging.getLogger(log_path)

    if len(logger.handlers) > 0:
        return logger

    file_handler = logging.FileHandler(log_path, mode="a", encoding="utf-8")
    file_handler.setFormatter(logging.Formatter(FILE_HANDLER_FORMAT))
    logger.addHandler(file_handler)

    return logger

def handle_exception(exc_type, exc_value, exc_traceback):
    logger = logging.getLogger("rich")

    logger.error("Unexpected exception",
                 exc_info=(exc_type, exc_value, exc_traceback))

 

set_logger(log_path): 이 함수는 로거를 설정하고 반환한다.

로그를 파일과 콘솔에 출력하며, 파일 핸들러는 지정된 log_path 파일에 로그를 기록한다.

만약 이미 핸들러가 추가되어 있다면 새로운 핸들러를 추가하지 않는다.

그리고 이 함수는 설정된 로거 객체를 반환한다.

 

logging.basicConfig() 에서는 로거의 기본 설정을 한다.

 

handle_exception 함수는 예외가 발생했을 때 해당 예외 정보를 로그에 기록한다.

만약 이 함수를 적지 않으면, 예외는 파일에 저장되지 않고 stream handler를 통해서만 작성된다.

 

또 위 코드에서 다음 부분이 굉장히 중요하다.

    logger = logging.getLogger(log_path)

    if len(logger.handlers) > 0:
        return logger

위 코드 없이 로거를 사용 중, 한 파일에서 찍혀야 할 로그가 여러 파일에서 찍히기도 했고 같은 로그가 여러번 찍히는 현상도 발생했다. ex) 로그1, 로그2, 로그2, 로그3, 로그3, 로그3, ...

 

이는 set_logger를 호출할 때마다 addHandler를 호출해 계속해서 logger 인스턴스를 중첩해 만들었기 때문이었다.

 

따라서 각각의 파일마다 하나의 로거 인스턴스를 생성해주기 위해 logger = logging.getLogger(log_path) 이 코드를 추가 했고, 한 로그 핸들러가 여러번 생성되지 않도록 하기 위해 다음 if 문을 추가했다.

 

reference

profile

만쥬의 개발일기

@KangManJoo

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!