[Python]Logging

포동동·2024년 4월 10일
2

[Pythonic]

목록 보기
4/4

logging을 알아본 이유


자동화 툴이나 api를 개발하다보면 내가 짠 코드가 잘 돌아가고 있는지, 어디서 문제인지 알고 싶어질 때가 있다. 파이썬을 처음 배웠을 때부터 최근까지 대부분 vsc의 디버그 모드로 하나씩 뜯어보거나 의심가는 부분에 print를 붙여 출력문을 보곤 했다.
하지만, 작성하는 파일이 많아지거나 계층적 구조를 띄게 되면서 점점 단순한 방법으로는 커버를 할 수가 없어졌다. 따라서 좀 더 알기 쉽고 추후 장애 대응에도 유연하게 대처하기 위해 파이썬의 logging을 알아보기로 했다.


printlogging의 차이


"프로그램이 코드의 그 부분을 통과한지 터미널을 통해 알고싶다" 정도라면 그냥 print문을 쓰는게 편할 것이다. 실제 logging을 쓰려면 아래와 같은 수요 정도는 있어야 한다.

  • 개발자가 안 보고 있을 때도 파일에 출력, 기록해 두고 싶다.
  • 특정한 에러가 일어나면 시스템 관리자의 메일로 alert을 보내고 싶다.
  • 발생한 로그 정보를 그 때마다 HTTP 통신으로 slack 등에 보내고 싶다.

이렇게, 프로그램의 동작 로그를 좀 더 플렉서블하게 출력하고 싶은 경우에는, python에 이미 내장되어있는 logging 모듈을 사용하면 좋다.


logging의 사용법


기본적인 logging의 사용법은 많은 블로그에서 설명하고 있으며, 심지어 파이썬 공식문서에서도 나름 알기 쉽게 쓰여있으니 꼭 한 번 읽어보길 바란다.

간단하게 요약만 하자면,

가장 기본 logger인 root logger를 쓸 때는 아래와 같이 작성하면 된다.

import logging
logging.{로그를 남기고 싶은 수준}("{어떤 에러 메시지를 남길 건지}")

#example
logging.warning("Watch out!")
logging.error("You should fix it!")

기본 logger 말고 커스텀 logger를 쓸 때는 아래와 같이 작성하면 된다.

import logging
logger = logging.getLogger({지정하고 싶은 logger 이름})

#example
logger = logging.getLogger("my_logger")

logger


위의 이미지는 아래와 같은 순서로 이해하면 된다.

  1. logging은 로그 출력을 행하는 인간(혹은 로봇)이다.
  2. logging은 1개의 python.exe에 대해 1명만이 존재한다.
  3. 그림과 같이 python.exe를 사용하여 프로그램을 실행하는 System-A가 있다고 한다.
  4. System-A에서 필요한 로그 출력 설정을(Handler)를 logger에게 맡기고, logger는 그 설정 핸들러에 따라 로그를 출력한다,.
  5. logging은 복수의 핸들러를 보유할 수 있다.

다만, System-B, System-C 등이 추가되면 문제가 생긴다.

docker나 pipenv 등 여러개의 python이 제대로 분리되어있는 환경이 아니라면 위의 2번과 5번 때문에 에러가 생길 수 있다.

왜냐하면, 기본적으로 컴퓨터에는 1개의 python.exe밖에 존재하지 않는데, 여러개의 시스템이 모두 같은 python.exe를 사용하게 되면 System-A에게 속해있는 핸들러와 System-B에게 속해있는 핸들러가 충돌하여 생각지도 못 한 버그가 생길 수도 있기 때문이다.

따라서, 직접 logging을 사용하는 것이 아닌, logger를 사용하는 것이다.

그래서 위에서 얘기한 root logger나 커스텀 logger를 따로 지정해서 사용해야 한다는 것이다.


logger의 출력 레벨 설정


import logging
logger = logging.getLogger("my_logger")

# my_logger가 출력하는 최저출력 레벨을 DEBUG 레벨로 설정
logger.setLevel(logging.DEBUG) 

logger.info("I am info log.")
logger.warning("I am warning log.")

DEBUG=10 / INFO=20 / WARNING=30 / ERROR=40 / CRITICAL=50 이라서 logger.setLevel(10) 이런식으로도 표현할 수 있다.


Handler


핸들러란, logger가 로그 출력을 할 떄 사용하는 도구 같은 것으로, 아래와 같은 종류가 있다.

핸들러 인스턴스logger에 부여되는 기능
StreamHandler터미널에 출력
FileHandler파일로 출력
SMTPHandler특정 메일주소로 송신출력
HTTPHandlerHTTP서버로 출력

기본적인 사용법은 아래와 같다.

import logging
logger = logging.getLogger("my_logger")
logger.setLevel(logging.DEBUG)

# StreamHandler를 인스턴스화
st_handler = logging.StreamHandler()
# StreamHandler의 최저출력 레벨을 DEBUG로 설정
st_handler.setLevel(logging.DEBUG)

# FileHandler를 인스턴스화
fl_handler = logging.FileHandler(filename="sample.log", encoding="utf-8")
# FileHandler의 최저출력 레벨을 WARNING으로 설정
fl_handler.setLevel(logging.WARNING)

# 인스턴스화 한 핸들러를 my_logger에게 전달
logger.addHandler(st_handler)
logger.addHandler(fl_handler)

logger.info("I am info log.")
logger.warning("I am warning log.")

Handler가 출력하는 출력포맷


너무 많기 때문에 링크를 남긴다. 예시 코드는 아래와 같다.

import logging
logger = logging.getLogger("mny_logger")
logger.setLevel(logging.DEBUG)

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

st_handler = logging.StreamHandler()
st_handler.setLevel(logging.DEBUG)
# StreamHandler에서 나오는 로그의 포맷을 format에 저장한 형태로 설정
st_handler.setFormatter(logging.Formatter(format))

fl_handler = logging.FileHandler(filename="sample.log", encoding="utf-8")
fl_handler.setLevel(logging.WARNING)
# FileHandler에서 나오는 로그의 포맷을 format에 저장한 형태로 설정
fl_handler.setFormatter(logging.Formatter(format))

logger.addHandler(st_handler)
logger.addHandler(fl_handler)

logger.info("I am info log.")
logger.warning("I am warning log.")

SMTPHandlerHTTPHandler


import logging

# SMTPHandler
logger = logging.getLogger()
logger.addHandler(SMTP_SSLHandler(
								mailhost=['{이메일 서버}', '{포트번호}'],
                                fromaddr='{보내는이 이메일 주소}',
                                toaddrs=['{받는이 이메일 주소}'],
                                subject='{이메일 제목}',
                                credentials=['{유저 네임}', '{유저 비번}'],
                                secure=() # SSL 또는 TLS를 사용하려면 ('tls',) 또는 ('ssl',)로 설정 )
                                ) 
import logging

# HTTPHandler(Slack을 예시로)
import logging
import logging.handlers

SLACK_TOKEN = 'xoxb-YOUR_BOT_TOKEN-COMES_HERE'

class SlackHandler(logging.handlers.HTTPHandler):
  def __init__(self, token, channel='#general', emoji=True):
    super().__init__(host='slack.com', url='/api/chat.postMessage', secure=True)
    self.token = token
    self.channel = channel
    self.emoji = emoji

  def mapLogRecord(self, record):
    if self.formatter is None:  # Formatter가 설정되지 않은 경우
      text = record.msg
    else:
      text = self.formatter.format(record)
    
    emoji = (
      '' if self.emoji == False else
      ':bug:' if record.levelname == 'DEBUG' else
      ':pencil2:' if record.levelname == 'INFO' else
      ':warning:' if record.levelname == 'WARNING' else
      ':no_entry:' if record.levelname == 'ERROR' else
      ':rotating_light:' if record.levelname == 'CRITICAL' else
      ''
    )
    
    return {
      'token': self.token,
      'channel': self.channel,
      'text': f'{emoji} {text}',
      'as_user': True,
    }

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

formatter = logging.Formatter(
  fmt='%(asctime)s *%(module)s* : %(message)s',
  datefmt='%H:%M:%S',
)

slack_handler = SlackHandler(SLACK_TOKEN)
slack_handler.setFormatter(formatter)
logger.addHandler(slack_handler)

logger.debug('디버깅 로그입니다.')
logger.info('일반 로그입니다.')
logger.warning('경고 로그입니다.')
logger.error('에러 로그입니다.')
try:
  1/0
except:
  logger.exception('예외처리 로그입니다.')

위의 코드는 여기서 가져왔다.


참고한 페이지

https://docs.python.org/ko/3/howto/logging.html
https://docs.python.org/ko/3/library/logging.handlers.html#
https://hwangheek.github.io/2019/python-logging/
https://jh-bk.tistory.com/40

profile
완료주의

0개의 댓글