본문 바로가기
프로그래밍/Python

파이썬(Python) logging 모듈을 이용한 로그(Log) 남기기

by 부자 꽁냥이 2023. 3. 21.

파이썬(Python)에서는 logging 모듈을 이용하여 특정 상황에서 로그(Log)를 출력하거나 파일로 남기는 등의 작업을 수행할 수 있다. 이번 포스팅에서는 logging 모듈을 이용하여 로그 남기는 방법에 대해서 알아본다.


   logging 모듈 사용법

먼저 logging 모듈 사용법을 단계적으로 알아보고 응용 사례를 살펴보자.

1) Logger 클래스 생성 : getLogger

logging 모듈에서 로그를 남기기 위한 첫 번째 단계는 Logger 클래스를 생성하는 것이다. 이는 getLogger를 이용하면 된다. 이때 name 인자에 원하는 Logger 이름을 지정한다.

 

import logging

logger = logging.getLogger(name='MyLog')

 

logger를 살펴보면 다음과 같이 Logger 클래스, Logger 명, 그리고 경고 레벨이 나온다. 경고 레벨에 대해서는 아래에서 다룬다.

 

 

만약 getLogger에서 name 인자를 지정하지 않거나 빈 문자열(' ')로 지정하면 Logger의 가장 상위에 있는 Root Logger가 생성된다.

 

root_logger = logging.getLogger(name='') ## 또는 logging.getLogger() Root Logger 생성
root_logger

 

 

RootLogger는 가장 상위에 있는 Logger이며 parent를 통해 앞에서 만든 'MyLog'라는 이름을 가진 logger의 부모가 RootLogger임을 알 수 있다.

 

print(logger.parent == root_logger) ## logger의 부모는 Root Logger인가?
print(root_logger)

 


2) Level 설정 : setLevel

여기서 Level이라 함은 경고 수준을 의미하며 가장 낮은 단계인 DEBUG부터 가장 높은 단계인 CRITICAL까지 있다. 또한 각 명칭에 대한 경고 점수가 있으며 경고 수준이 높을수록 점수도 높다. 아래 표는 파이썬 Logging Tutorial를 참고하여 경고 수준과 점수 그리고 각 경고 수준을 언제 사용하는지에 대한 설명을 정리한 것이다.

Logging Level Table

경고 수준을 설정하면 해당 경고 수준보다 낮은 것들은 로그를 남기지 않는다. 즉, 로그 파일로 남기지 않거나 출력하지 않는다는 것이다. 예를 들어 경고 수준을 INFO로 지정한다면 이보다 낮은 수준인 DEBUG는 로그를 남기지 않게 된다.

 

logging의 경고 수준은 setLevel을 이용하여 설정한다. 아래 코드는 경고 수준을 INFO로 설정한 것이다. 초기값은 WARNING이었지만 출력해 보면 INFO로 바뀐 것을 알 수 있다.

 

logger.setLevel(logging.INFO)
print(logger)

 


3) Formatter 설정 : Formatter

로그를 남기기 위한 텍스트 포맷을 설정하는 단계이다. 이는 Formatter 객체를 이용하여 로그를 남길 수 있다. 텍스트 포맷을 설정할 때 사전에 설정된 포맷 속성들이 있는데 가장 많이 사용되는 속성들은 다음과 같다.

LogRecord Attributes

이 외에도 여러가지 속성을 지원하는데 자세한 내용은 Logging Facility for Python- LogRecord attributes부분을 참고하기 바란다.

 

아래 코드는 Formatter를 이용하여 로그의 텍스트 포맷을 지정한 것이며 로그 기록 시간, Logger 이름, 경고 수준을 출력하고 다음 줄에 로그 메시지를 남기게 된다. 로그 기록 시간의 경우 datefmt을 이용하면 원하는 시간 정보를 원하는 형태의 포맷으로 바꿔준다.

 

formatter = logging.Formatter('|%(asctime)s||%(name)s||%(levelname)s|\n%(message)s',
                              datefmt='%Y-%m-%d %H:%M:%S'
                             )

4) Handler 등록 : addHandler

로그 텍스트 포맷까지 지정했으면 핸들러(Handler)를 등록해야 한다. 많이 사용하는 핸들러는 스트림 핸들러와 파일 핸들러가 있다.

a. 스트림 핸들러 : StreamHandler

스트림 핸들러는 콘솔창에 로그 출력을 담당하는 핸들러며 StreamHandler 객체를 통해 생성할 수 있다. 스트림 핸들러를 생성하고 출력할 텍스트 포맷을 지정한 뒤 addHandler를 통해 Logger에 등록한다. 

 

stream_handler = logging.StreamHandler() ## 스트림 핸들러 생성
stream_handler.setFormatter(formatter) ## 텍스트 포맷 설정
logger.addHandler(stream_handler) ## 핸들러 등록

 

아래 코드는 앞에서 다루었던 모든 코드를 통합했다. Logger 생성, 경고 수준 설정, formatter 설정 스트림 핸들러 등록까지 완료한 다음 info를 통하여 로그를 출력한다. 경고 수준을 INFO로 설정했기 때문에 로그가 출력된다.

 

import logging

logger = logging.getLogger(name='MyLog')

logger.setLevel(logging.INFO) ## 경고 수준 설정

formatter = logging.Formatter('|%(asctime)s||%(name)s||%(levelname)s|\n%(message)s',
                              datefmt='%Y-%m-%d %H:%M:%S'
                             )

stream_handler = logging.StreamHandler() ## 스트림 핸들러 생성
stream_handler.setFormatter(formatter) ## 텍스트 포맷 설정
logger.addHandler(stream_handler) ## 핸들러 등록

for i in range(1, 6):
    logger.info(f'{i} 번째 접속')

 

만약 info대신 그보다 한 단계 아래인 debug를 사용하면 출력되지 않는다.

 

## 아무것도 출력 안됨
for i in range(1, 6):
    logger.debug(f'{i} 번째 접속')

 

b. 파일 핸들러 : FileHandler

파일 핸들러는 로그를 출력하는게 아닌 로그 파일을 남겨준다. 파일 핸들러는 FileHandler 객체를 통해 생성할 수 있다. 이때 로그 파일 이름을 포함한 경로를 설정해야 하며 같은 파일에 로그 메시지를 계속 이어나가고 싶다면 mode='a', 기존 파일에 있는 내용을 지우고 새로 시작하고 싶다면 mode='w'로 설정하면 된다. 나머지 등록 과정은 스트림 핸들러와 동일하다. 

 

file_handler = logging.FileHandler('test.log', mode='w') ## 파일 핸들러 생성
file_handler.setFormatter(formatter) ## 텍스트 포맷 설정
logger.addHandler(file_handler) ## 핸들러 등록

 

아래 코드는 Logger 생성, 경고 수준 설정, formatter 설정 스트림 핸들러 등록까지 완료한 다음 info를 통하여 로그 메시지를 로그 파일에 남긴다.

 

import logging

logger = logging.getLogger(name='MyLog')

logger.setLevel(logging.INFO) ## 경고 수준 설정

formatter = logging.Formatter('|%(asctime)s||%(name)s||%(levelname)s|\n%(message)s',
                              datefmt='%Y-%m-%d %H:%M:%S'
                             )

file_handler = logging.FileHandler('test.log') ## 파일 핸들러 생성
file_handler.setFormatter(formatter) ## 텍스트 포맷 설정
logger.addHandler(file_handler) ## 핸들러 등록

for i in range(1, 6):
    logger.info(f'{i} 번째 접속')

 

지정한 경로에 들어가면 test.log 파일이 있는 것을 확인할 수 있으며 파일을 열어보면 아래와 같이 로그 메시지가 남겨진 것을 확인할 수 있다.


5) 중복 출력 방지

로그를 남기려고 할 때 때때로 똑같은 메시지가 두 가지 이유에서 중복 출력되는 경우가 있다. 첫 번째는 Logger가 메모리에 계속 저장되어 있어서 발생하는 중복이 있고 두 번째는 자식에서 부모 Logger로 전파됨에 따라 중복 출력이 되는 경우가 있다. 이를 해결하는 방법에 대해서 알아보자.

 

a. Logger가 메모리에 남겨져 있어 발생하는 중복

같은 이름을 가진 Logger를 반복해서 생성하거나 주피터 상에서 로그를 남기는 코드를 두 번 이상 실행할 때 핸들러를 등록하게 되면 새로 생긴 Logger가 아닌 같은 이름을 가진 기존 객체에 계속해서 등록된다. 이러한 상태에서 로그를 출력하게 되면 아래와 같이 중복이 발생한다.

 

 

Logger에 핸들러가 어떤 것들이 있는지 살펴보자.

 

logger.handlers

 

 

살펴보니 역시나 두개의 핸들러가 있었다. 따라서 로그 메시지가 두 번 중복되었다. 이때에는 Logger를 생성하고 나서 핸들러가 이미 있는지 확인하고 만약 하나 이상의 핸들러가 있다면 이를 제거해 주면 중복은 해결된다.

 

if logger.hasHandlers(): ## 핸들러 존재 여부
    logger.handlers.clear() ## 핸들러 삭제

 

위 코드를 아래와 같이 추가해주면 중복이 발생되지 않는 것을 확인할 수 있다.

 

import logging

logger = logging.getLogger(name='MyLog')

logger.setLevel(logging.INFO) ## 경고 수준 설정

formatter = logging.Formatter('|%(asctime)s||%(name)s||%(levelname)s|\n%(message)s',
                              datefmt='%Y-%m-%d %H:%M:%S'
                             )

if logger.hasHandlers(): ## 핸들러 존재 여부
    logger.handlers.clear() ## 핸들러 삭제

stream_handler = logging.StreamHandler() ## 스트림 핸들러 생성
stream_handler.setFormatter(formatter) ## 텍스트 포맷 설정
logger.addHandler(stream_handler) ## 핸들러 등록

for i in range(1, 6):
    logger.info(f'{i} 번째 접속')

 

 

b. 부모 Logger 전파를 통한 중복

이는 getLogger에서 name 인자를 통해 자식 Logger를 생성했을 때 발생한다. 이는 logger라는 자식 Logger 뿐만 아니라 부모 Logger인 Root Logger까지 로그 메시지가 전파되어 출력되기 때문에 중복 메시지가 발생한 것이다.

 

아래 코드는 자식 Logger와 부모 Logger 모두 같은 텍스트 포맷을 가진 스트림 핸들러를 등록한 뒤 로그 메시지를 출력한다.

 

import logging

logger = logging.getLogger(name='MyLog') ## 자식 Logger
root_logger = logger.parent ## 부모 Logger

logger.setLevel(logging.INFO) ## 경고 수준 설정

formatter = logging.Formatter('|%(asctime)s|==|%(name)s||%(levelname)s|\n%(message)s',
                              datefmt='%Y-%m-%d %H:%M:%S'
                             )

if logger.hasHandlers(): ## 핸들러 존재 여부
    logger.handlers.clear() ## 핸들러 삭제

stream_handler1 = logging.StreamHandler() ## 스트림 핸들러 생성
stream_handler1.setFormatter(formatter) ## 텍스트 포맷 설정
logger.addHandler(stream_handler1) ## 핸들러 등록
stream_handler2 = logging.StreamHandler() ## 스트림 핸들러 생성
stream_handler2.setFormatter(formatter) ## 텍스트 포맷 설정
root_logger.addHandler(stream_handler2) ## 핸들러 등록

for i in range(1, 6):
    logger.info(f'{i} 번째 접속')

 

코드를 실행하면 로그 메시지가 중복으로 발생한 것을 알 수 있다.

 

 

 이때에는 다음과 같이 자식 Logger에서 부모 Logger로 로그 메시지가 전파되지 않게 하면 된다. 

 

logger.propagate = False

 

위 코드를 추가한 다음 로그를 출력하면 더 이상 중복이 발생하지 않는다.

 

import logging

logger = logging.getLogger(name='MyLog') ## 자식 Logger
root_logger = logger.parent ## 부모 Logger

logger.setLevel(logging.INFO) ## 경고 수준 설정
logger.propagate = False ## 전파 방지

formatter = logging.Formatter('|%(asctime)s|==|%(name)s||%(levelname)s|\n%(message)s',
                              datefmt='%Y-%m-%d %H:%M:%S'
                             )

if logger.hasHandlers(): ## 핸들러 존재 여부
    logger.handlers.clear() ## 핸들러 삭제

stream_handler1 = logging.StreamHandler() ## 스트림 핸들러 생성
stream_handler1.setFormatter(formatter) ## 텍스트 포맷 설정
logger.addHandler(stream_handler1) ## 핸들러 등록
stream_handler2 = logging.StreamHandler() ## 스트림 핸들러 생성
stream_handler2.setFormatter(formatter) ## 텍스트 포맷 설정
root_logger.addHandler(stream_handler2) ## 핸들러 등록

for i in range(1, 6):
    logger.info(f'{i} 번째 접속')

 


6) 예제

아래 코드는 로그 메시지를 경고 수준 INFO 이상인 경우에 출력하고 DEBUG 이상인 경우에 로그 파일을 남긴다.

 

import logging

def prevent_duplicate(logger):
    ## 로그 메시지 중복 방지
    logger.propagate = False
    if logger.hasHandlers():
        logger.handlers.clear()

logger1 = logging.getLogger(name='MyLog1') 
logger1.setLevel(logging.INFO) ## 경고 수준 설정
prevent_duplicate(logger1)

logger2 = logging.getLogger(name='MyLog1') 
logger2.setLevel(logging.DEBUG) ## 경고 수준 설정
prevent_duplicate(logger2)

formatter = logging.Formatter('|%(asctime)s|==|%(name)s||%(levelname)s|\n%(message)s')

stream_handler = logging.StreamHandler() ## 스트림 핸들러 생성
stream_handler.setFormatter(formatter) ## 텍스트 포맷 설정
logger1.addHandler(stream_handler) ## 핸들러 등록

file_handler = logging.FileHandler('debug.log', mode='w') ## 파일 핸들러 생성
file_handler.setFormatter(formatter) ## 텍스트 포맷 설정
logger2.addHandler(file_handler) ## 핸들러 등록

for i in range(1, 6):
    message = f'{i} 번째 접속'
    logger1.info(message)
    if i%2 == 0:
        logger2.debug('짝수 번째 접속')

 


댓글


맨 위로