_SANDBOX

Python Logging 사용하기: 나도 기록 좀 잘해보자

zingozing 2022. 1. 13. 21:44

결과를 매번 print로 출력한지는 벌써 오래됐다. 파이썬을 쓰면서 기록을 남길 때 logging을 사용한지는 좀 됐는데 문제는 너무 허술하게 배워서 아직까지도 정확하게는 어떻게 쓰는지 모른다. 무능하다 무능해


Logging으로 뭘하냐

  • 기초적인 사용법
  • 구성요소
  • 포맷팅하기
  • 파일로 저장하기
  • 에러가 날 걸 알고 있다. 하지만 코드는 계속 돌아갔으면 한다. 우짜누?

기초적인 사용법

import logging

logging.info("일하는 중")
logging.debug("개발자만 볼 수 있음")
logging.warn("쓰지 마라")
logging.error("조진듯.")

짠 이렇게 하면 여러분 콘솔창에 저 4줄이 나온다. 저렇게만 사용하면

INFO:root:일하는 중

구성요소

Logging 모듈에는 4개의 구성요소가 있다. 하지만 이 표현법을 좋아하지 않는다. 구성 요소라하니까 뭔가 짜임새 있게 짜놓은 모듈을 이해하기 힘들게 만드는 것 같다. "구성 요소"보다는 이 Logging을 customizing하는 방법의 구성 요소라고 표현하는 게 더 정확한 것 같다.

  • Logger 메인이다. 이 Logger가 우리가 원하는 일을 수행해준다.
  • Handlers 얘는 내가 사용하는게 두 가지다: StreamHandler, FileHandler
    • StreamHandler의 경우는 이 스크립트에서 뽑아낸 모든 로그들을 스트림으로 보내준다. 쉽게 말해서 pipe로 bash에서 넘겨줄 수 있다는 뜻.
    • FileHandler는 이 로그들을 하드에 저장한다. 보통 .log 확장자로 저장하게 된다.
  • Filters 모든 로그에는 레벨이 있다. 찍히는 정보에 등급이 있다는 뜻. 공식 홈페이지에는 총 5개로 공지되어 있다. Filters의 역할은 레벨 기준에 따라 그 이상의 심각도를 가진 메시지만 출력할 수 있게 된다. 별도로 설정하지 않으면 WARN 이상의 레벨만 출력된다. INFO로 돌리고 싶으면 .setLevel로 설정해줘야한다.
    • DEBUG 디버깅 용도다. 특별하게 exception이라는 메소드가 있는데 얘는 우리가 평소에 에러 나면 볼 수 있는 에러메세지를 그대로 출력해준다.
    • INFO 코드가 어떻게 돌고있다~ 하는 것 공유한다. 뭐 epoch 별로 뭐가 돌아가고 있는지 찍을 때 쓰자.
    • WARN 하면 안되지만 하고 있을 때, 하지만 코드는 돌 때. Deprecated한 함수 같은 걸 쓸 때 달아주자.
    • ERROR 정말 에러.
    • CRITICAL 제일 심각한 케이스로 코드가 돌아가지 않는 지경의 에러를 찍어주자.
  • Formatter 최종적으로 이 결과물이 어떤 형태로 출력될지 정한다. 가령 [날짜] 돌아가는코드스레드 - 메시지 이런식으로 포맷팅을 하고 싶다면 포맷터에 적어주면 된다.

구성요소 별로 살펴보자.

Logger

기본적으로 logger 클래스가 모든 기록 행위를 담당한다. 그냥 import logging을 하고 logging.log()를 진행해줘도 되지만, 더 자세하게 사용하고 싶으면 처음에는 logger = logging.getLogger() 로 진행할 수 있다. 기록을 하는 방법은 .log()로 할 수 있다. 하지만 이렇게 로깅을 하려면 심각도 레벨을 직접 적어줘야한다. 상술한 5개의 레벨은 각각 10/20/30/40/50의 레벨을 가진다. 그러니까 INFO 레벨의 메시지를 찍고 싶다면 logger.log(10, 메시지)를 해야한다는 거다. 너무 귀찮으니 그냥 logger.info(메시지)로 작성하자. 나머지 레벨도 마찬가지다.

getLogger에서 인자는... 이름인데 별로 생각 없이 __name__을 넣자. 이렇게 하면 현재 파일이 수행되는 스크립트의 이름을 기록하게 된다. 여기에 내가 기록하고 싶은 이름을 넣고 싶으면 문자열로 넣어주면 끝이다.

Handlers

찍힌 정보들을 어떻게 처리하느냐이다. 기본적으로 StreamHandler가 들어가있다. 그래서 별일 없이 로그를 찍으면 콘솔에 찍히는 거다. 이걸 파일로 저장하고 싶으면 FileHandler를 사용하면 된다.

  • handler = FileHandler(파일이름) 을 사용하면 handler 객체가 만들어진다. 보통 "~.log"로 지정한다.
  • logger.addHandler(handler)를 수행하면 이 logger에 FileHandler를 추가하는 것이다. 이러면 콘솔에도 찍히고 위에서 지정한 로그 파일에도 저장된다.
  • 특정 handler를 지우고 싶으면 logger.removeHandler(handler)를 수행하면 된다. 기본적으로 콘솔에는 출력을 안하고 파일에만 저장하고 싶으면 (현재까지 알아본 거는) removeHandler로는 안되고 loggeing.basicConfig를 통해 수행할 수 있다. 여기서 filename을 지정해주면 StreamHandler는 알아서 작동 안한다 (=콘솔에는 안찍히고 .log파일만 생성한다.)

Filters

특정 레벨의 필터만 나오게 설정한다. 이건 logger.setLevel()을 통해 수행할 수 있다. 숫자를 넣어줘도 되고 혹은 logging.INFO 같이 넣어줘도 관계 없다. 애초에 logging.INFO에는 20이 저장되어 있다. 이러면 지정한 레벨 이상의 메시지만 저장/전송한다.

Formatter

어떤 방식으로 정보를 출력할지 지정한다. 문자열 안에 %(요소)를 적어주면 된다. 이 요소에 대한 설명은 아래 링크에 있다.
LogRecord Attributes
[01/13/2022 20:39:55] INFO - main: On my way 로 출력하고 싶으면 formatter를 "[%(asctime)s] %(levelname)s - %(name)s: %(message)s"로 설정해주면 된다. 이 Formatter는 setFormatter() 메소드로 Handler 객체에 달아준다.

여기서 한 번 꼬아서 생각하면, .log 파일과 콘솔에 출력할 때 다르게 레벨이나 포맷팅을 설정하고 싶으면 어떻게할까? 각 StreamHandler와 FileHandler에게 다른 .setFormatter, .setLevel을 걸어주면 된다. 그런데 Default로 StreamHandler가 설정되어 있어도 만들어낸 logger에서 StreamHandler를 찾을 수가 없다. 읭...

logging.basicConfig

이 모든 과정을 좀 쉽게 만들어주는 게 있다. 바로 몇 개의 configuration을 한 번에 셋업할 수 있는 basicConfig라는 메소드다. 여기서 file_name을 설정하면 해당 파일로 로그를 저장하고 StreamHandler를 사용하지 않는다. 동시에 사용하고 싶으면 후에 StreamHandler를 직접 추가해주면 된다.

import logging

logging.basicConfig(
    filename="time.log",
    format="[%(asctime)s] %(levelname)s - %(name)s: %(message)s",
    datefmt="%m/%d/%Y %H:%M:%S",
    level=logging.INFO,
)
logger = logging.getLogger(__name__)

stream_handler = logging.StreamHandler()
console_format = logging.Formatter("[%(asctime)s] %(levelname)s - %(message)s")
stream_handler.setLevel(logging.WARN)
stream_handler.setFormatter(console_format)

logger.addHandler(stream_handler)

이해해보면 쉽다.

설정 저장하기

이 모든 과정을 한 번 수행하면 좋은데 프로젝트마다 넣어줘야 하니까 귀찮을 수 있다. (좋은 말로 할 때 복붙하자) 현재 로깅 설정을 바로 저장하는 방법은 없는 것 같다. 나도 저장하고 쓰는 편은 아니라 (그냥 코드 복붙 ㅋ;) 잘 몰랐는데 보통 두 가지를 사용한다 - .conf, .json. 이 파일을 직접 로드해서 dict 형태의 객체를 logging.config.dictConfig(config)로 불러오거나 파일의 경로를 문자열로 logging.config.fileConfig(path)로 넣어줘도 된다. 파일의 형식은 아래 같이 만든다.

에러도 찍고 싶은데 코드는 안 멈추고 싶어 아니 증말

try-except 구문을 떠올리자. except쪽에서 에러를 처리할 때 코드 중단 없이 실행하는 코드를 넣을 수 있다. 그런데 에러 메시지 자체를 저장하고 싶은 경우가 있다. 이 때 logger.error()로 설정하는 게 아니라, logger.exception으로 지정하면 에러 메시지 전체를 저장할 수 있다. 유후~ 이건 DEBUG 레벨로 처리되기 때문에 보통은 보이지 않는다. ERROR랑 다르다!

try:
	42 / 0
except Exception as e:
	logger.exception(e)

휴 나도 이해 못하고 대충 쓰고 살았다가 정리해놓으니까 좀 머리가 안 아파졌다. 프로젝트에는 잘 넣어놓고 정작 연구 코드 짤 떄는 이 모듈을 잘 몰라서 제대로 안 넣었다. 이제 제대로 정리해야지

참고로 wandb 모듈에 생기는 .log도 같이 적용되는 것 같다. 이건 자세히 알아봐야겠다.

'_SANDBOX' 카테고리의 다른 글

맥북 세팅하기  (0) 2022.01.09
GitHub Actions로 README.md 자동화하기  (2) 2022.01.04