![[Python] 파일에 로그를 기록하는 Logger 클래스 만들기](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDUC07%2FbtsDRoa2FeN%2FwgTP7eN6Fzyz42jkua1gZK%2Fimg.png)
🧐 개요
이번 포스트는 Python 프로그래밍 언어로 클래스를 구현하여 파일에 로그를 기록하는 커스텀 클래스를 작성합니다. 일반적으로 운영체제, 오픈 소스 또는 기업형 소프트웨어를 사용하는 경우에는 로그 파일이 적재 적소에 자동 생성 및 적재됩니다. 하지만 프로젝트 또는 특정 목적의 아키텍처를 직접 설계하는 경우에는 로그 데이터가 자체적으로 생기지 않기 때문에 직접 '로그 데이터를 생성하는 로직을 추가'해 주어야 합니다.
Python의 logging 모듈과 logger
Python 환경에는 로그를 출력하고 파일로 저장할 수 있는 logging 모듈이 기본으로 제공됩니다(즉 pypi 패키지를 설치할 필요가 없습니다). logging 모듈을 통해 로그를 입력할 수도 있지만, 하위 모듈인 logger을 인스턴스화 하여 특정 부분에 대한 로그 데이터만 별도로 관리할 수도 있습니다.
클래스의 형태로 구현하는 이유
해당 스크립트에서 다루는 기능은 별도의 함수 3개로 구성해도 사용에 있어 큰 문제가 없습니다. 그럼에도 불구하고 해당 기능을 클래스로 구현하는 것에는 다음과 같은 이유가 있습니다.
- Python은 함수 내에서 외부 라이브러리의 모듈을 호출할 때 와일드카드(*)를 사용할 수 없습니다. 따라서 로그 관련 기능을 위해 복수의 함수가 필요할 경우, 해당 함수를 스크립트 최상단에 호출하거나 일일히 하나씩 호출해 사용해야 합니다.
- 로그를 관리하는 로직은 다양한 형태로 구현될 수 있으므로, 기능의 확장성을 고려한 프로그래밍이 필요합니다. 로거를 정의하고 핸들러를 다루는 기능은 거의 반드시 사용되는 기능이기 때문에, 해당 클래스를 상속받아 사용할 경우 클래스의 형태를 유지하면서 가독성을 높이는 방향으로 확장이 가능합니다.
🖍️ 스크립트 작성하기
구현할 기능
해당 스크립트에서는 이하의 기능들을 구현하고 있습니다.
- 로그 레벨을 지정하여 logger 클래스 생성하기
- logger 클래스에 file handler 추가하기
- (사용이 완료된) logger 클래스의 file handler 삭제하기
전체 스크립트
import logging
class Logger:
def __init__(self, name: str, level: str = None, config:dict = None):
# set dictConfig
self.config = config
if config != None:
from logging.config import dictConfig
dictConfig(config)
# set logger name & level
self.name = name
self.level = level
self.log_levels = {
None: logging.INFO,
"debug": logging.DEBUG,
"warning": logging.WARNING,
"error": logging.ERROR,
"critical": logging.CRITICAL
}
self.logger = logging.getLogger(name)
self.logger.setLevel(self.log_levels.get(self.level, logging.INFO))
print("successfully created logger") # test
def add_file_handler(self, log_dir: str):
from datetime import datetime
import os
# check log dir
os.makedirs(log_dir) if not os.path.exists(log_dir) else True
# define & add file handler
log_format = "%(asctime)s - %(levelname)s - %(message)s"
file_handler = logging.FileHandler(f"{log_dir}/{datetime.now().strftime('%Y-%m-%d_%H')}.log", encoding="utf-8")
file_handler.setFormatter(logging.Formatter(log_format))
self.logger.addHandler(file_handler)
def remove_logger(self):
# close handler
for handler in self.logger.handlers[:]:
handler.close()
self.logger.removeHandler(handler)
# remove filter
for filter_ in self.logger.filters[:]:
self.logger.removeFilter(filter_)
전체 스크립트의 구성은 위와 같습니다.
이하에서 각 부분의 내용을 자세히 다루도록 하겠습니다.
클래스 초기화
def __init__(self, name: str, level: str = None, config:dict = None):
# set dictConfig
self.config = config
if config != None:
from logging.config import dictConfig
dictConfig(config)
# set logger name & level
self.name = name
self.level = level
self.log_levels = {
None: logging.INFO,
"debug": logging.DEBUG,
"warning": logging.WARNING,
"error": logging.ERROR,
"critical": logging.CRITICAL
}
self.logger = logging.getLogger(name)
self.logger.setLevel(self.log_levels.get(self.level, logging.INFO))
print("successfully created logger") # test
클래스를 초기화하는 함수 내에서는 logger의 이름과 레벨을 설정하여 logger을 구성하고 있습니다. 주요 고려 사항들을 정리하면 다음과 같습니다.
- 운영 환경에서는 INFO 레벨의 로그가 디폴트 값으로 사용되고, 그 외의 로그 레벨은 특수 목적 및 개발 과정에서 사용된다고 합니다. 따라서 클래스의 정의 단계에서 로그 레벨을 별도로 입력하지 않을 경우 INFO 레벨을 적용하도록 설정하고 있습니다.
- logger에 로그 레벨을 지정할 경우 logging 함수를 거쳐야 하기 때문에, 해당 부분을 변수로 입력받을 경우 해당 클래스를 사용하는 외부 함수에도 logging 모듈을 중복 추가해주어야 하는 문제가 있습니다. 따라서 로그 레벨과 텍스트를 매핑하는 딕셔너리를 구성하고 이를 기반으로 로그 레벨을 설정하고 있습니다.
File Handler 추가
def add_file_handler(self, log_dir: str):
from datetime import datetime
import os
# check log dir
os.makedirs(log_dir) if not os.path.exists(log_dir) else True
# define & add file handler
log_format = "%(asctime)s - %(levelname)s - %(message)s"
file_handler = logging.FileHandler(f"{log_dir}/{datetime.now().strftime('%Y-%m-%d_%H')}.log", encoding="utf-8")
file_handler.setFormatter(logging.Formatter(log_format))
self.logger.addHandler(file_handler)
해당 함수에서는 입력받은 디렉토리 내부에 로그 파일을 생성하는 file handler을 구성 및 추가하고 있습니다. 파일을 관리하는 방식에는 제한이 없지만, 저는 주로 API 서버 내에서 logger을 사용하기 때문에 (파일의 크기가 커지는 것을 염두하여)날짜_시간 단위로 로그 파일을 관리하고 있습니다.
logger을 통해 작성된 로그는 매핑되어있는 file handler가 없으면 터미널 환경에서만 출력되고 사라집니다. 서버 또는 아키텍터 내에서 발생한 로그들을 아카이브하기 위해서는 file handle을 추가해야 합니다. 참고로 Timed Rotating과 같이 기능별로 여러 종류의 handler 모듈이 제공되기 때문에 모듈을 바로 사용할 수도 있습니다.
Logger 제거
def remove_logger(self):
# close handler
for handler in self.logger.handlers[:]:
handler.close()
self.logger.removeHandler(handler)
# remove filter
for filter_ in self.logger.filters[:]:
self.logger.removeFilter(filter_)
해당 함수에서는 logger에 연결된 file hander 및 filter을 제거하고 있습니다.
logger을 사용하는 스크립트가 종료되어도 file handler은 곧바로 종료되지 않고 일정 시간 간격을 두고 종료됩니다. API 서버의 미들웨어 내에서 logger 및 file hander을 사용할 경우, 다수의 로그 파일이 열려져 있는 것으로 해석되어 'Too Many Opened Files' 에러가 발생할 수도 있습니다. 따라서 번거롭더라도 Logger 내부의 handler 및 filter 등을 직접 제거하는 로직을 사용해야 합니다.
테스트
if __name__ == "__main__":
logger = Logger(name="demo", level="warning")
logger.add_file_handler(log_dir="/Users/kimdohoon/git/study/python-logging/log")
logger.logger.info("demo_info")
logger.logger.warning("demo_warning")
logger.remove_logger()
클래스와 메서드가 정상 동작하는지 확인해보도록 하겠습니다.
logger의 설정 레벨을 'WARNING'으로 설정하고, INFO 등급의 로그와 WARNING 등급의 로그를 입력하였습니다.

테스트 시점의 날짜 및 시간으로 로그 파일이 작성되었습니다.

INFO 레벨의 로그는 기록되지 않고, WARNING 레벨의 로그는 정상 기록되었습니다.
해당 스크립트에서는 파일로 수행하는 로그 작성 기능을 구현했으나, 다양한 기능들을 추가하여 로그를 보다 세부적으로, 또는 효과적으로 관리할 수 있습니다.
📝 마치며
로그 파일 내에는 매우 많고 다양한 정보들이 포함되어 있습니다. 개발 단계에서 로그를 확인하지 않고 진행하는 것은 불가능하다고 이야기가 나올 정도이니까요. 시스템을 운영하는 과정에서는 크고 작은 오류가 항상 발생하기 때문에, 시스템의 상태 정보 및 변화 추이 등을 로그 데이터로 기록해 관리 및 모니터링하면서 보다 안정적인 운영 방식을 도입할 수 있을 것입니다.
'프로그래밍 이모저모 > Python' 카테고리의 다른 글
[Python] No module named 'distutils' 에러 해결하기 (0) | 2023.12.28 |
---|
발자취를 로그처럼 남기고자 하는 초보 개발자의 블로그