데이터를 만지는 사람이라면, 데이터 전처리 혹은 여타 이유로 주기적으로 스크립트를 실행하는 일이 많습니다. 이 때 스크립트의 결과를 로그(log)로 많이 남겨둡니다.

로그란, 작업에 대한 ‘기록’이라고 보시면 되는데요. 좀 더 풀어서 말하면 소프트웨어 실행 중에 발생하는 이벤트 등을 기록한 것입니다. 이러한 로그를 기록하는 행위는 로깅(logging)이라고 합니다.

로그를 남기는 목적은 크게 아래와 같습니다.

  • 한 스크립트 내 존재하는 여러 기능들이 제대로 동작했는지 확인하기 위해서
  • 동작 후의 실행 결과를 남기기 위해서 (기능 동작, 성공, 에러, DB 쿼리에 대한 반환값 등등…)

따라서 문제가 발생했을 때 백업된 내용을 활용하여 복구가 보다 용이해지고, 실행 결과를 계속 모니터링 하지 않아도 된다는 장점이 있습니다.

로깅의 활용은 얼마나 로그를 섬세하고 아름답게 남기느냐에 따라 달려 있는데, 습관이 되지 않으면 사알짝 번거로운(?) 면도 있기 때문에 (절대 번거로우면 안 됩니다만) 스크립트의 목적에 따라 얼마나 촘촘하게 로깅을 할 것인지 정하면 되겠습니다.

이 글에서는 스크립트가 포함된 현재 디렉토리 안에 logs 폴더를 만들어 파일 형태로 로깅하고, 자정이 지나면 자동으로 로그 생성 날짜가 파일명에 붙는 방법을 예시로 설명하겠습니다.


1. 로깅을 가능하게 - 로깅 환경 셋업하기

1) logging 라이브러리

사실 제일 간단한 로깅은 print()를 이용해 콘솔에 출력하는 것입니다. 하지만 print()를 이용하면 파일 형태로 로그를 남기기 어려우니 기본으로 설치되어 있는 logging 라이브러리를 사용하도록 하겠습니다.

logging 라이브러리는 아래와 같은 범주의 구성요소를 제공합니다.

구성요소 설명
logger 응용 프로그램 코드가 직접 사용하는 인터페이스를 노출합니다.
handler (로거에 의해 만들어진) 로그 레코드를 적절한 목적지로 보냅니다.
filter 출력할 로그 레코드를 결정하기 위한 보다 정밀한 기능을 제공합니다.
formatter 최종 출력에서 로그 레코드의 배치를 지정합니다.

2) 로깅의 레벨

로깅은 다섯 가지의 레벨을 가지고 있습니다.

레벨 설명
DEBUG 상세한 정보. 보통 문제를 진단할 때만 사용
INFO 예상대로 작동하는 지에 대한 확인
WARNING 예상치 못한 일이 발생했거나 가까운 미래에 발생할 문제(예를 들어 ‘디스크 공간 부족’)에 대한 표시. 소프트웨어는 여전히 예상대로 작동
ERROR 더욱 심각한 문제로 인해, 소프트웨어가 일부 기능을 수행하지 못함
CRITICAL 심각한 에러. 프로그램 자체가 계속 실행되지 않을 수 있음을 나타냄

로깅의 기본 레벨은 WARNING 입니다.
DEBUG < INFO < WARNING < ERROR < CRITICAL 순이므로 기본 레벨을 재설정하지 않으면 DEBUG, INFO 수준의 로그는 기록되지 않습니다.

import logging
logging.debug('hi')
logging.warning('warn!!!')

WARNING:root:warn!!!!

결과를 보면 DEBUG 레벨은 로깅되지 않았음을 확인할 수 있습니다. 기본 레벨 재설정 방법은 아래에서 설명하도록 하겠습니다.

3) 자신만의 로거(logger) 만들기

로깅은 Logger 클래스의 인스턴스 메서드를 호출하여 수행됩니다.
로거 객체는 아래와 같은 일을 처리합니다.

  • 응용 프로그램이 실행시간에 메시지를 기록 할 수 있도록 여러 메서드를 응용 프로그램 코드에 노출합니다.
  • 로거 객체는 심각도(로그 레벨; 기본 필터링 장치) 또는 필터 객체에 따라 어떤 로그 메시지를 처리할 지 결정합니다.
  • 로거 객체는 관련 로그 메시지를 관심 있는 모든 로그 처리기로 전달합니다.

가장 크게는 구성 및 메시지 전송의 일을 합니다.

보통은 raw logging 보다는 로거를 직접 만들어 사용합니다.
로거 이름은 사용자가 원하는 어떤 이름이든 설정할 수 있습니다.

myLogger = logging.getLogger('test')
myLogger.setLevel(logging.DEBUG) # 로깅 수준 지정
myLogger.debug('hi')
myLogger.warning('warn!!!!')

DEBUG:test:hi
WARNING:test:warn!!!!

‘test’라는 이름으로 새로운 로거를 생성하고, test 로거의 기본 로깅 레벨을 제일 낮은 DEBUG 수준으로 설정했습니다.
보시다시피 아까는 출력되지 않았던 DEBUG, WARNING 모두 출력됐음을 알 수 있습니다.
또, 로거를 만든 이름으로 로그 메시지가 남겨지는 것을 확인할 수 있습니다.

4) 핸들러 (handler) 설정하기

핸들러 객체는 (로그 메시지의 레벨을 기반으로) 적절한 로그 메시지를 처리기의 지정된 대상(예를 들어 파일, 메일 등등)으로 전달하는 역할을 합니다. 로거 객체는 addHandler() 메서드를 사용하여 0개 이상의 처리기 객체를 자신에게 추가 할 수 있습니다.

예를 들어, 응용 프로그램은 모든 로그 메시지를 로그 파일로 보내고, 에러(error)와 그 이상의 모든 로그 메시지를 표준 출력으로 보내고, 모든 심각한 에러(critical) 메시지를 전자 메일 주소로 보낼 수 있습니다. 이 시나리오에서는 각 처리기가 특정 레벨의 메시지를 특정 위치로 보내는 3개의 개별 처리기가 필요합니다.

핸들러의 가장 큰 역할은 목적지 설정이라고 생각하면 됩니다.

TimedRotatingFileHandler

이 글에서는 파일 형태로 로그를 남길 수 있는 핸들러 중 자정이 되면 자동으로 로그 파일이 생성된 날짜가 파일명에 기록되는 기능을 가진 ‘TimedRotatingFileHandler’를 사용할 것입니다. 이 핸들러는 logging.handlers 모듈을 호출하면 활성됩니다.

이 핸들러는 사용자가 설정한 특정 시간 간격의 디스크 로그 파일 회전을 지원합니다. wheninterval을 따라 회전이 일어납니다.

파라미터 설명 기본 값
filename 저장할 로그 파일의 이름  
when interval의 유형 지정 ‘h’
interval 간격 1
backupCount 최대 백업할 로그 파일의 개수 0

(파라미터에 대한 자세한 설명은 이 문서를 참조하세요.)

when 파라미터를 이용해 time interval을 지정할 수 있습니다. 하루가 지나면 날짜를 기록하게 만들 것이므로 when='midnight'으로 설정하겠습니다.

[핸들러명].suffix에 원하는 로그 파일명의 포맷을 지정할 수 있습니다.

import logging.handlers
file_handler = logging.handlers.TimedRotatingFileHandler(
  filename='test', when='midnight', interval=1,  encoding='utf-8'
  )
file_handler.suffix = 'log-%Y%m%d' # 파일명 끝에 붙여줌; ex. log-20190811

myLogger.addHandler(file_handler)

5) 출력 포맷 설정하기

로깅은 확인하는 사람이 보기 편한 기록이어야 하므로, 출력 형태도 사용자 설정이 가능합니다. 어떤 형태로 로깅할 것인지 사용자가 포맷을 설정해 줄 수 있습니다.

이 글에서는 시간 - 레벨 - [파일명:라인 넘버] 메시지 의 형태로 기록하도록 하겠습니다. logging.Formatter를 이용해 포맷을 만들어 준 다음, 핸들러에 할당해줍니다.

formatter = logging.Formatter(
  '%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] %(message)s'
  )
file_handler.setFormatter(formatter)

# 2019-08-11 - INFO - [test.py:37] Hello! This is Yurim's Blog.


2. 현재 디렉토리에 로그 파일 남기기

1) 현재 디렉토리 이름 가져오기

Python의 기본 모듈인 os를 호출하여 스크립트가 존재하는 현재 디렉토리의 이름을 가져오도록 하겠습니다.

import os 
current_dir = os.path.dirname(os.path.realpath(__file__))
  1. __file__: 현재 파일의 위치를 리턴
  2. os.path.realpath(): 현재 파일의 절대 경로를 리턴
  3. os.path.dirname(): 현재 파일의 디렉토리 이름만을 추출

위와 같은 순서로 동작하게 됩니다.

2) 현재 파일명 가져오기

로그 파일의 제목을 ‘log-[filename]’과 같은 형태로 남기려고 합니다. 그러기 위해서 파일명을 절대값으로 지정해줘도 되지만, 파일명이 수정됐을 경우의 번거로움을 방지하기 위해 현재 파일명을 변수로 관리할 것입니다.

current_file = os.path.basename(__file__)
current_file_name = current_file[:-3]  # xxxx.py
LOG_FILENAME = 'log-{}'.format(current_file_name)
  1. os.path.basename(): 입력 받은 경로의 기본 이름을 리턴 (=경로 없이 파일명+확장자만 리턴)
  2. Python 파일의 확장자는 ‘.py’로 동일하기 때문에 파일명의 뒤에서 네번째 자리까지만 파싱

3) 현재 경로에 로그 파일을 저장할 폴더 만들어주기

현재 경로와 현재 파일명까지 모두 가져왔다면, 마지막으로 로그 파일을 저장해 둘 폴더를 따로 만들어보겠습니다.

log_dir = '{}/logs'.format(current_dir)
if not os.path.exists(log_dir):
    os.makedirs(log_dir)
  1. 위에서 받아 온 현재 경로를 이용해 로그의 디렉토리를 명시적으로 생성
  2. ‘logs’ 폴더가 존재하지 않을 경우에만 os.makedirs()를 이용해 로그 폴더 생성

여기까지 작업하면 모든 셋팅은 기본적으로 완료되었습니다.
하나의 스크립트로 표현해보도록 하겠습니다.

기본 가정

  • 스크립트명: test.py
  • 오늘 날짜: 2019년 8월 11일
import logging
import logging.handlers
import os

# 현재 파일 경로 및 파일명 찾기
current_dir = os.path.dirname(os.path.realpath(__file__))
current_file = os.path.basename(__file__)
current_file_name = current_file[:-3]  # xxxx.py
LOG_FILENAME = 'log-{}'.format(current_file_name)

# 로그 저장할 폴더 생성
log_dir = '{}/logs'.format(current_dir)
if not os.path.exists(log_dir):
    os.makedirs(log_dir)

# 로거 생성
myLogger = logging.getLogger('test') # 로거 이름: test
myLogger.setLevel(logging.DEBUG) # 로깅 수준: DEBUG

# 핸들러 생성
file_handler = logging.handlers.TimedRotatingFileHandler(
  filename=LOG_FILENAME, when='midnight', interval=1,  encoding='utf-8'
  ) # 자정마다 한 번씩 로테이션
file_handler.suffix = 'log-%Y%m%d' # 로그 파일명 날짜 기록 부분 포맷 지정 

myLogger.addHandler(file_handler) # 로거에 핸들러 추가
formatter = logging.Formatter(
  '%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] %(message)s'
  )
file_handler.setFormatter(formatter) # 핸들러에 로깅 포맷 할당
# 2019-08-11 - INFO - [test.py:37] Hello! This is Yurim's Blog.
  • 이제 위 설정이 포함된 스크립트를 실행할 때 마다 (자정 단위로 로테이션 하도록 설정했으니 데일리 실행이겠습니다)
  • ‘logs’ 폴더 안에 ‘log-test’라는 이름의 로그 파일이 생기고
  • 하루가 지나면 ‘test.log-20190811’과 ‘test’ 파일 두 개가 생겨 있을 것입니다!


도움이 될 만한 자료

  • 로깅 HOW TO
    • logging 모듈에 대해 아주 자세하고 친절하게 잘 나와 있으니 필요시에 참고하세요.