- Published on
django logging setting 으로 로그 남기는 방법 aws cloudwatch slack api 포함
- Authors
- Name
- hongreat
- ✉️hongreat95@gmail.com
Django의 Logging 설정은 프로젝트 내에서 로그를 분기하고 setting 이 매우 편합니다.
파이썬의 Logging 을 통해 로그의 레벨, 핸들러, 포맷 등을 정의하기 때문에 핸들러를 이용한 설계가 쉽게 구성되어있어, 사용하는데 매우 간편합니다.
다만 추상화 된 부분이 존재하기 때문에 커스텀 로그를 남기려면 문서를 잘 참고해야 합니다.
이번 글에서는 Django Logging 를 이용해 logging 하는 방법과 aws, slack 등과 같은 서드파티를 함께 사용하는 방법을 기록합니다.
1. Django LOGGING on Settings
Django 에서 기본적으로 제공하는 settings
파일에 LOGGING
변수가 정의 됩니다.
해당 부분에서 로깅 핸들러를 맵핑할 수도, 로그레벨을 지정할 수도, 로그 방식을 지정할 수도 있는 등의 컨트롤 타워 역할을 하게 됩니다.
아래는 기본적인 가이드 입니다.
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'formatter_name': {
'format': '[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
},
},
'filters': {
'filter_name': {
'()': 'path.to.custom.Filter',
},
},
'handlers': {
'handler_name': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'formatter_name',
'filters': ['filter_name'],
},
'file': {
'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': 'path/to/logfile.log',
'formatter': 'formatter_name',
},
},
'loggers': {
'django': {
'handlers': ['handler_name'],
'level': 'INFO',
'propagate': True,
},
'myapp': {
'handlers': ['file'],
'level': 'DEBUG',
'propagate': False,
},
}
}
1.1 setting 구성
version
은1
로 설정하면 됩니다. (오래전부터 version 1 로 사용되어 업데이트가 없어서 그대로 사용.)disable_existing_loggers
은 기본 로거를 비활성화할지 여부를 결정해주는데, 이때 기본 로거란 Django 기본 로그를 의미하며 일반적으로False
로 설정해서 기존 로거를 유지 하는것이 편리하다고 생각합니다.formatters
는 로그 메시지의 형식을 정의합니다.format
은 로그 포맷,datefmt
는 날짜 형식을 지정합니다.filters
특정 조건을 만족하는 로그만 처리하도록 필터링합니다. (커스텀 가능)handlers
로그 메시지를 출력할 위치 혹은 방식 등을 지정할 수 있는 핵심 요소 이며, 커스텀 또한 가능합니다. (이번 글에서 이것을 활용한 서드파티 요소와 연동도 기록합니다.)StreamHandler
: 콘솔에 일반적으로 출력되는 형태 입니다.FileHandler
: 파일에 기록되며,filename
옵션으로 파일 경로 지정하면 로그 파일에 로그가 적재되는 형태 입니다.- 그 외(
SMTPHandler,RotatingFileHandler
,TimedRotatingFileHandler
)등이 있는데 이메일로 발송하는 것 혹은 시간이나 규칙등에 따라서 로그 파일을 변경할지 와 같은 핸들러도 기본적으로 존재하니 나중에 다뤄보겠습니다.
loggers
가장 중요하다고 할 수 있는 부분입니다.- 특정 로거에 대해 핸들러, 레벨, 전파 여부를 설정합니다.
- 이 부분에서 Django 기본 로거 Ex(
'django'
,'django.request'
) 등을 설정 할 수 도 있고 앱별 로거를 추가할 수 있습니다. 각 로직에서 get_logger(pusedo code) 방식으로 끌어오는 것이 바로 이부분 입니다. - 참고로 예전 글에서 다룬 적 있는데 로그는 중요도에 따라 레벨을 설정하며 낮은 레벨부터
DEBUG
-INFO
-WARNING
-ERROR
-CRITICAL
순으로 배치됩니다.
2. 사용방법
2.1 특정 앱에 로거 설정
앱별 로그를 독립적으로 관리하려면 loggers
에 앱 이름을 추가합니다.
아래의 myapp
로거는 DEBUG
레벨 이상의 로그를 콘솔과 파일에 기록하며, 상위 로거로 전파되지 않습니다.
'myapp': {
'handlers': ['console', 'file'],
'level': 'DEBUG',
'propagate': False,
}
2.2 로깅 레벨에 따른 지정 혹은 맵핑
일단, 기본적으로 python 에서 사용하는 logging
모듈을 그대로 사용합니다.
logging 에 대한 사용이 제한되는 layer 는 따로 없습니다.
import logging
logger = logging.getLogger('myapp')
logger.debug(f"{result}")
그 의미는 log 를 남겨야하고, 남기는 방식은 전적으로 개발단에서 의사결정이 되는 것을 의미하며
실제로 view
, serializer
, model
혹은 util
등 어디에서나 사용하고 각자 컨벤션을 맞춰두는 것이 좋겠습니다.
def process_order(order_id):
logger.debug(f"Order ID: {order_id}")
try:
logger.info(f"Order {order_id} success")
except Exception as e:
logger.error(f"Order {order_id} failed {order_id}: {e}", exc_info=True)
2.3 파일 로그 기록 형태
모자이크 되긴했지만, 파일 형태로 로그를 남기면 아래처럼 파일 상에 로그가 남습니다.
2.4 format 에서 사용된 로깅 변수
settings
에서 detailed 라는 자세한 정보를 담는 로그를 세팅하는 예시 입니다.
"formatters": {
"detailed": {
"format": "[{asctime}] {levelname} {name}:{lineno} - {message} // {funcName} || {threadName} || {processName}",
"style": "{",
},
format 으로 사용가능한 변수들은 문서에 나와 있습니다. (하단 레퍼런스 링크에 더 자세하게 기록되어 있습니다.)
asctime
: 로그 메시지가 생성된 시간을 출력합니다. 기본 형식은YYYY-MM-DD HH:MM:SS,mmm
이며,datefmt
옵션을 사용해 날짜 형식을 지정할 수 있습니다.levelname
: 로그 레벨을 출력합니다. 예를 들어DEBUG
,INFO
,WARNING
,ERROR
,CRITICAL
과 같은 레벨 이름이 여기에 포함됩니다.name
: 로거의 이름을 출력합니다. 보통logging.getLogger("logger_name")
에서 지정한 이름이 출력됩니다.lineno
: 로그가 발생한 코드 상의 라인(줄) 번호를 출력합니다.message
: 로그를 남길 때 작성하는 메시지 문자열이 여기에 포함됩니다.pathname
: 로그가 발생한 파일의 전체 경로를 출력합니다.filename
: 로그가 발생한 파일 이름을 출력합니다.funcName
: 로그가 발생한 함수 이름을 출력합니다.threadName
: 로그가 발생한 스레드 이름을 출력합니다.processName
: 로그가 발생한 프로세스 이름을 출력합니다.
3. 커스텀 로깅
3.1 AWS Cloud Watch
AWS에는 Cloud watch 라는 Log 리소스가 존재하며, watchtower
라는 라이브러리에 CloudWatchLogHandler
클래스를 통해 이미 쉽게 로깅할 수 있도록 구현 되어있습니다.
핸들러 부분은 아래처럼 세팅하면 됩니다.
"handlers": {
"cloudwatch_log_info": {
"level": "INFO",
"class": "watchtower.CloudWatchLogHandler",
"boto3_session": boto3_client_object
"log_group": "log-group-name",
"stream_name": "{strftime:%Y-%m-%d}",
"formatter": "detailed",
},
},
위에서 세팅한 핸들러 정보를 아래 처럼 logger에 맵핑하면 각 layer에서 로깅을 사용할 수 있게 됩니다.
"loggers": {
"log_name_1": {
"handlers": [
"cloudwatch_log_info",
],
"level": "INFO",
},
3.2 Slack
3.2.1 Slack에서 채널 세팅
webhook_url 을 발급받아야하고 이는 slack 에서 진행합니다.
slack 메뉴바에 왼쪽 하단 부에 앱추가
라는 부분이 존재합니다.
여기서 hook
을 검색해서 incoming webhooks 를 진입합니다.
slack 에 추가를 눌러서 진입합니다.
채널에 포스트 하는 것은 채널에 알림을 발송하는 것을 의미하며, 지정된 채널에 알림이 발송되게 됩니다.
앱을 추가하게 되면 웹후크 URL
이라는 것이 나오는데, 아래에서 사용할 webhook_url
과 동일합니다.
여기서 발급되는 URL을 로직에서 사용합니다.
AWS는 라이브러리를 통해 로깅하는 방식이 매우 간단했지만, slack 과 같은 서드파티 서비스에는 직접 핸들러(Handler)를 커스터마이징 하고 slack에서 제공하는 webhook app 을 연동해줘야 합니다.
3.2.2 Slack Custom Handler
settings
에서 먼저 커스텀 핸들러를 사용하려면()
를 키값으로 두고 사용합니다.- slack 에서 발급받은
webhook_url
을 세팅합니다.
"handlers": {
"slack": {
"()": SlackHandler,
"level": "DEBUG",
"webhook_url": "https://hooks.slack.com/services/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", # SlackHandler에서 요구하는 매개변수
"formatter": "detailed",
},
()
키를 사용해 커스텀 핸들러를 지정하는 방식은 Python 표준 라이브러리의 logging.config.dictConfig 문서에서 handlers
섹션의 설명에 나와 있습니다.
참고로..Django 공식 문서에는 "class"
키와 "()"
키의 차이점에 대한 구체적인 설명은 없습니다.
그렇지만, Python의 dictConfig
구조를 따르는 것은 Django가 Python 표준 로깅 설정 방식을 그대로 사용하기 때문에, 동일한 방식으로 커스텀 핸들러를 설정할 수 있는 것 입니다.
class: The fully qualified name of the handler class. (): If this key is present, it is assumed to be a callable which will return a handler instance when called. It is evaluated using eval().
즉 정리하면
"class"
키는 기본 핸들러 클래스를 문자열로 지정합니다."()"
키는 커스텀 핸들러 클래스를 직접 참조하거나 팩토리 함수로 사용할 때 사용합니다.
이제 핸들러 차례는 생각 보다 간단합니다.
logging.Handler
를 오버라이딩 하고, setting 에서 설정한 webhook_url 을 인자로 넘겨줍니다.
class SlackHandler(logging.Handler):
def __init__(self, webhook_url):
super().__init__()
self.webhook_url = webhook_url
def emit(self, record):
log_entry = self.format(record)
payload = {"text": log_entry}
try:
requests.post(self.webhook_url, json=payload)
except Exception as e:
print(f"Failed to send log to Slack: {e}")