Logo

파이썬 로깅 설정 - logger, handler, formatter

지난 포스팅에서 파이썬의 logging 내장 모듈을 이용해서 정말 기본적인 로깅 방법에 대해서 살펴보았습니다. 이번 포스팅에서는 애플리케이션 규모가 커짐에 따라 어떻게 효과적으로 로깅을 설정해야 하는지에 대해서 다뤄보도록 하겠습니다.

핵심 컴포넌트

로깅 설정을 제대로 하기 위해서는 먼저 로깅 시스템을 구성하는 핵심 컴포넌트를 이해하는 것이 중요합니다.

먼저 가장 로깅 시스템의 가장 근간이 되는 로거(logger)는 로그 메시지를 남기기 위해서 우리가 직접 사용하는 프로그래밍 인터페이스를 제공합니다. 우리는 로거를 통해서 debug(), info(), warning(), error()와 같은 메서드를 호출해서 로그 메시지를 로깅 시스템에 전달할 수 있습니다. 모든 로거는 이름을 가지며, 최상위(root) 로거의 자식이 됩니다. 이 로깅의 계층 개념에 대해서는 밑에서 예제를 통해 추가 설명을 드리겠습니다.

로거가 로그를 남기라고 전달을 하면, 로깅 시스템의 어디선가에서는 이를 받아서 처리를 해줘야겠죠? 이 부분은 핸들러(handler)가 담당합니다. 우리는 핸들러를 통해서 로그 메시지를 다양한 방식으로 처리할 수 있습니다. 예를 들어, 단순히 콘솔이나 파일에 출력할 수도 있고, 이메일로 보내거나, 외부 로깅 서비스로 전달할 수도 있습니다. 하나의 로거에는 여러 개의 핸들러를 설정할 수 있기 때문에, 하나의 로그 메시지를 여러 가지 방식으로 처리할 수 있습니다.

마지막으로 로그 메시지를 어떤 형태(format)로 남기냐는 문제가 남는데요. 이 부분은 포맷터(formatter)를 통해서 명시할 수 있습니다. 우리는 포맷터를 통해서 로그 메시지 뿐만 아니라 로그 발생 시각, 로그 심각도, 함수 이름, 라인 번호 등도 함께 기록할 수 있습니다. 포맷터를 핸들러에 설정을 해주면 핸들러는 포맷터에 설정되어 있는 형태대로 로그 메시지를 처리해줍니다.

참고로, 이 부분은 비단 파이썬에 국한되지 않고, 대부분 언어들의 로깅 시스템에서 차용하고 있는 범용적인 개념입니다.

코드로 로깅 설정

위에서 설명드린 핵심 개념을 이해하기 위해서 먼저, 간단하게 코드로 로깅 설정을 해보겠습니다.

우선, logging 모듈을 임포트합니다.

import logging

다음, 두 개의 포맷터를 생성하겠습니다. 첫 번째 포맷터는 간단한 형태를 가지고, 두 번째 포맷터는 복잡한 형태를 가집니다.

simple_formatter = logging.Formatter("[%(name)s] %(message)s")
complex_formatter = logging.Formatter(
    "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] - %(message)s"
)

다음, 이전 단계에서 생성한 포맷터를 사용하여 두 개의 핸들러를 생성하겠습니다. 첫 번째 핸들러는 로그를 콘솔에 출력해주는데, 간단한 형태의 포맷터를 사용하며, DEBUG 레벨 이상의 로그를 처리해줍니다. 두 번째 핸들러는 로그를 파일에 출력해주는데, 복잡한 형태의 포맷터를 사용하며, ERROR 레벨 이상의 로그를 처리해줍니다.

console_handler = logging.StreamHandler()
console_handler.setFormatter(simple_formatter)
console_handler.setLevel(logging.DEBUG)

file_handler = logging.FileHandler("error.log")
file_handler.setFormatter(complex_formatter)
file_handler.setLevel(logging.ERROR)

다음, 최상위(root) 로거에 이전 단계에서 생성한 핸들러 두 개를 연결해주도록 하겠습니다. 최상위 로거는 WARNING 심각도 이상의 로그만 남기도록 설정하였습니다.

root_logger = logging.getLogger()
root_logger.addHandler(console_handler)
root_logger.addHandler(file_handler)
root_logger.setLevel(logging.WARNING)

마지막으로, 특정 모듈을 위한 로거에 대한 설정을 해보겠습니다. parent 모듈은 INFO 레벨 이상의 로그만 남기도록, parent.child 모듈은 DEBUG 레벨 이상의 로그만 남기도록 하였습니다.

parent_logger = logging.getLogger("parent")
parent_logger.setLevel(logging.INFO)

child_logger = logging.getLogger("parent.child")
child_logger.setLevel(logging.DEBUG)

이런 식으로 모듈 별로 로깅 레벨을 다른 게 설정해줌으로써, 특정 모듈에 대해서는 다른 모듈에 비해서 좀 더 자세한 로그를 남길 수 있습니다. 여기서 parent 로거와 parent.child 로거에 별도로 핸들러 설정을 하지 않아도 되는 이유는 자식 로거의 로그 메시지는 부모 로거로 전파(propogate)되기 때문입니다.

즉, parent.child 로거의 로그 메시지는 parent 로거로 전파되고, 이는 다시 최종적으로 최상위 로거로 전파됩니다. 따라서 parent 로거와 parent.child 로거가 남기는 로그는 취상위 로거에 연결되어 있는 두 개의 핸들러에 의해서 처리될 것입니다.

만약에 자식 로거에도 핸들러 설정을 해주고 싶다면, 해당 로거의 propogate 속성을 False로 설정하여 부모 로거로의 전파를 차단하면 됩니다. 그러지 않으면, 의도치 않게 로그가 자식 핸들러와 부모 핸들러에 의해서 중복 처리가 될 것입니다.

파일로 로깅 설정

코드로 로깅 설정을 하면 유지보수가 까다로울 수 있기 때문에 실제 프로젝트에서는 별도의 파일을 이용해서 로깅 설정을 하는 경우가 많습니다. 위에서 코딩한 로깅 설정은 다음과 같이 파일로 변환을 할 수 있습니다.

  • logging.conf
[formatters]
keys=simple,complex

[formatter_simple]
format=[%(name)s] %(message)s

[formatter_complex]
format=%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] - %(message)s

[handlers]
keys=console,file

[handler_console]
class=StreamHandler
args=(sys.stdout,)
formatter=simple
level=DEBUG

[handler_file]
class=FileHandler
args=("error.log",)
formatter=complex
level=ERROR

[loggers]
keys=root,parent,child

[logger_root]
level=WARNING
handlers=console,file

[logger_parent]
qualname=parent
level=INFO
handlers=

[logger_child]
qualname=parent.child
level=DEBUG
handlers=

그 다음, logging.config.fileConfig() 함수에 파일 경로를 넘겨주기면 하면 됩니다.

import logging
import logging.config

logging.config.fileConfig("logging.conf")

파일로 로깅 설정할 때 자세한 문법은 아래 파이썬 공식 레퍼런스를 참고바라겠습니다.

사전으로 로깅 설정

파이썬의 공식 가이드에 따르면 파이썬의 내장 자료구조인 사전(dictionary)을 사용해서 로깅 설정을 하는 것이 권장되고 있습니다. 위에서 코드나 파일로 했던 로깅 설정을 사전으로 옮긴 후에 logging.config.fileConfig() 함수에 사전 객체를 넘겨주기면 하면 됩니다.

import logging
import logging.config

config = {
    "version": 1,
    "formatters": {
        "simple": {"format": "[%(name)s] %(message)s"},
        "complex": {
            "format": "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] - %(message)s"
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "simple",
            "level": "DEBUG",
        },
        "file": {
            "class": "logging.FileHandler",
            "filename": "error.log",
            "formatter": "complex",
            "level": "ERROR",
        },
    },
    "root": {"handlers": ["console", "file"], "level": "WARNING"},
    "loggers": {"parent": {"level": "INFO"}, "parent.child": {"level": "DEBUG"},},
}

logging.config.dictConfig(config)

사전으로 로깅 설정할 때 자세한 문법은 아래 파이썬 공식 레퍼런스를 참고바라겠습니다.

로깅 설정 테스트

위에서 여러 가지 방법으로 설정한 로깅 시스템을 이용해서 로그를 남겨보도록 하겠습니다.

if __name__ == "__main__":
    root_logger = logging.getLogger()
    root_logger.debug("디버그")
    root_logger.info("정보")
    root_logger.error("오류")

    parent_logger = logging.getLogger("parent")
    parent_logger.debug("디버그")
    parent_logger.info("정보")
    parent_logger.error("오류")

    child_logger = logging.getLogger("parent.child")
    child_logger.debug("디버그")
    child_logger.info("정보")
    child_logger.error("오류")

콘솔 핸들러는 간단한 포맷터가 설정되어 있기 때문에 콘솔에는 다음과 같이 간단한 형태의 로그가 출력이 됩니다. 최상위 로거는 ERROR 레벨 이상의 로그만 남기는 반면에, parent.child 모듈에 대해서는 DEBUG 레벨까지 로그가 남는 것을 확인할 수 있습니다.

  • 콘솔
[root] 오류
[parent] 정보
[parent] 오류
[parent.child] 디버그
[parent.child] 정보
[parent.child] 오류

파일 핸들러는 ERROR 레벨 이상의 로그만 처리하도록 설정해놨기 때문에, 아래와 같이 파일에는 에러 로그만 남는 것을 알 수 있습니다.

  • 파일 (error.log)
2020-03-07 17:14:56,940 ERROR [root] [main.py:32] - 오류
2020-03-07 17:14:56,940 ERROR [parent] [main.py:37] - 오류
2020-03-07 17:14:56,940 ERROR [parent.child] [main.py:42] - 오류

마치면서

이상으로, 코드, 파일, 사전을 이용해서 파이썬 애플리케이션의 로깅 설정을 하는 방법에 대해서 알아보았습니다.