https://hamait.tistory.com/880
파이썬 로깅의 모든것
[하마] 이승현 (wowlsh93@gmail.com) 2017.08.07 11:09파이썬 로깅의 모든것
파이썬 로깅에 대한 "모든것은" 사실 낚시구요. ㅎㅎ (유희열이 진행했던 All that music 이라는 라디오 프로그램에서 내가 좋아하는 국악이나 시부야계는 거의 나오지도 않았었다...먼소리야 이게 ;;) 그래도 블로그용으로는 꽤나 많은 정보를 담으려고 애를 썼습니다. 파이썬에서 자체 제공하는 기본 로깅 시스템에 대한 내용이구요. 버전이 올라감에 따라 내용이 달라질수 있으며, 오류도 있을 것이고 더 손쉬운 방법등이 있을 수 있음을 인지하시고 항상 다른것들도 같이 확인 하세요.
본론으로 바로 들어가시죠.
먼저 제가 원하는 로깅 시스템은 이렇습니다. IoT 의 게이트웨이 안에서 활약 할 놈으로써~
logging.conf 파일을 통해서 외부에서 설정 가능
레벨을 자유롭게 설정하고 추가 할 수 있어야 한다.
(CRITICAL, ERROR,INFO, DEBUG )파일,스트림에 동시에 출력 할 수 있어야 한다.
다양한 목적에 따라 다양한 파일에 출력 할 수 있어야 한다.
로깅 시간 출력 및 다양한 정보에 대한 추가가 가능해야 한다.
하루에 한번씩 파일을 생성 해야하며, 지난 파일은 압축하여 보관 해야한다.
하루에 한번씩 파일을 생성 해야하며, 오래된 파일을 삭제 할 수 있어야 한다.
파일 용량이 너무 커질 경우에는 자동으로 분리 시켜야 한다.
멀티프로세스를 활용해야 하는 파이썬의 특성상 멀티프로세스에서도 로그를 취득할 수 있어야 한다.
핸들러를 커스터마이징 할 수 있어야 한다. 즉 콘솔,파일 출력 뿐 만 아니라, DB 및 소켓을 통하여도 발송 할 수 있어야 한다.
사용하기 쉬워야 한다. (문서화가 잘 되 있어야 한다) , 기본 로깅 모듈을 사용해야한다.
이것을 해결 하기 위해 알아야 할 것들에 대해 공부해 보는것이 이 글의 목표 입니다. 즉 이 글을 모두 읽고 나면 모든 기능을 본인의 프로젝트에서 사용 할 수 있게 될 것이라 봅니다.
* 참고로 자바개발자에게 익숙한 log4j 같은 거대한 로깅 시스템에 비하면 제공되는 기능이 조금 부족합니다. 몇가지는 직접만들어야합니다.
* 2.x / 3.x 상관없으나, 몇몇 부분은 2.7 기준입니다. 3.x 버전의 새기능 활용 안함.
초급
print (" 30이 들어왔어")
import logging
if __name__ == '__main__':
logging.error("something wrong!!") 결과: ERROR:root:somthing wrong!!
" 서버의 대기포트는 이 번호로 시작 됬습니다"
" 어떤 함수가 호출 되었어요"
" 100이 들어왔어요"
import logging
if __name__ == '__main__':
logging.info("hello world")
import logging
if __name__ == '__main__':
mylogger = logging.getLogger("my")
나의 로깅 모듈 사용하기 - 2 (레벨 설정)
import logging
if __name__ == '__main__':
mylogger = logging.getLogger("my")
mylogger.setLevel(logging.INFO)
setLevel 메소드를 통해서 INFO 레벨 이상은 출력 하도록 설정 하였습니다. (DEBUG 출력 안함)
나의 로깅 모듈 사용하기 - 3 (핸들러 설정)
import logging
if __name__ == '__main__':
mylogger = logging.getLogger("my")
mylogger.setLevel(logging.INFO)
stream_hander = logging.StreamHandler()
mylogger.addHandler(stream_hander)
mylogger.info("server start!!!") 결과: server start!!!
핸들러란 내가 로깅한 정보가 출력되는 위치 설정하는 것을 말합니다. 위에서는 콘솔을 통해 출력하도록 설정 했지만, 파일,DB,소켓,큐등을 통해 출력 하도록 설정 할 수도 있습니다.
나의 로깅 모듈 사용하기 - 4 (파일 핸들러 설정)
파일로도 동시에 출력하게 해봅시다. 매우 간단합니다.
#!/usr/bin/python
import logging
if __name__ == '__main__':
mylogger = logging.getLogger("my")
mylogger.setLevel(logging.INFO)
stream_hander = logging.StreamHandler()
mylogger.addHandler(stream_hander)
file_handler = logging.FileHandler('my.log')
mylogger.addHandler(file_handler)
mylogger.info("server start!!!")
logging.FileHandler 클래스를 통해 객체를 만들어서 나의 로거에 추가해주면 됩니다. 현재 디렉토리에 파일(my.log)이 만들어 질 것 입니다.
노트: 파일은 a 모드로 열리는게 디폴트입니다. a 모드란 같은 이름의 파일이 이미 있다면, 파일 끝에 추가하여 쓰고, 없다면 쓰기용으로 파일 만들라는 뜻입니다.
나의 로깅 모듈 사용하기 - 5 (출력 포매팅 설정)
server start!!! 라는 메세지 말고도, 이 메세지가 언제 쓰여졌는지, 어떤 모듈에서 쓰여졌는지 등 기타 정보를 같이 출력하고 싶을 것입니다. 이때 포매팅이란 것을 하게 되는데요.
import logging
if __name__ == '__main__':
mylogger = logging.getLogger("my")
mylogger.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
stream_hander = logging.StreamHandler()
stream_hander.setFormatter(formatter)
mylogger.addHandler(stream_hander)
file_handler = logging.FileHandler('my.log')
mylogger.addHandler(file_handler)
mylogger.info("server start!!!") 결과: 2017-08-07 12:00:29,141 - my - INFO - server start!!!
포매팅에 추가한 정보를 모든 로그 데이터에 추가해서 출력하게 됩니다.
asctime 시간
name 로거이름
levelname 로깅레벨
message 메세지
이것 말고도 모듈이름, 파일이름, 출력라인넘버등 다앙하게 많으니, 더 많은 정보는 메뉴얼을 참고 하시구요. 위에서는 포매터를 콘솔출력에 한정해서 적용했기에 파일은 기존 그대로 출력 될 것입니다. 파일도 포매팅되게 하고 싶으면 역시 포매터를 파일 핸들러에도 추가하세요.
중급
로깅 모듈 생성에 관한 고찰
그냥 매개변수 없이 logging.getLogger() or logging.getLogger("") 이렇게 만들 수도 있습니다.
* logging.error() 를 직접 호출하면 내부에서 루트로거를 이용합니다.
로깅 모듈 생성 실제 - 로거 범위
보통 main 에서 로깅 시스템 (대표설정) 을 만들어서 사용하게 됩니다. 이것을 디폴트로 자식 로거들에서 사용하게 되며, 자식별로 필요하면 특정 설정을 하게 됩니다. 또한 핸들러는 버퍼링을 하고 있기 때문에 어플리케이션이 갑자기 중지되기 전에 logging.shutdown() 을 호출해서 모두 출력해 주면 좋습니다. (fianllly 절에서 수행)
이제 로거를 만드는 대표적인 범위를 살펴보시죠.
# 루트 로거 생성 rootlogger = logging.getLogger()
rootlogger.setLevel(logging.INFO)
stream_hander = logging.StreamHandler()
rootlogger.addHandler(stream_hander)
# "my" 로거 생성
mylogger = logging.getLogger("my")
mylogger.setLevel(logging.INFO)
stream_hander2 = logging.StreamHandler()
mylogger.addHandler(stream_hander2)
# "my" 로거 생성
mylogger.info("message from mylogger")
이것을 막으려면, 간단하게는 루트로거에서 핸들러를 추가해 주지 않아도 되며, 혹은
mylogger.propagate = 0
{
"version": 1,
"formatters": {
"simple": {
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"info_file_handler": {
"class": "logging.FileHandler",
"level": "DEBUG",
"formatter": "simple",
"filename": "info.log"
}
},
"root": {
"level": "DEBUG",
"handlers": ["console", "info_file_handler"]
}
}
: dev 환경과 pro환경에서 다르게 작동하도록 config를 나누었다.
{
"dev":{
'version':1,
'formatters':{
'error':{
'format':ERROR_FORMAT
},
'else':{
'format':ELSE_FORMAT
}
},
'handlers':{
'console':{
'class':'logging.StreamHandler',
'formatter':'else',
'level':'DEBUG'
},
'file_else': {
'class': 'logging.handlers.RotatingFileHandler',
'level': 'DEBUG',
'formatter': 'else',
'filename': './log/app.log',
'mode': 'a',
'maxBytes': 10485760,
'backupCount': 5,
},
'file_error': {
'class': 'logging.handlers.RotatingFileHandler',
'level': 'ERROR',
'formatter': 'error',
'filename': './log/error.log',
'mode': 'a',
'maxBytes': 10485760,
'backupCount': 5,
}
},
'root':{
'handlers':('console','file_else','file_error'),
'level':'DEBUG'
}
},
"pro":{
'version':1,
'formatters':{
'error':{
'format':ERROR_FORMAT
},
'else':{
'format':ELSE_FORMAT
}
},
'handlers':{
'console':{
'class':'logging.StreamHandler',
'formatter':'else',
'level':'DEBUG'
},
'file_else': {
'class': 'logging.handlers.RotatingFileHandler',
'level': 'INFO',
'formatter': 'else',
'filename': './log/app.log',
'mode': 'a',
'maxBytes': 10485760,
'backupCount': 5,
},
'file_error': {
'class': 'logging.handlers.RotatingFileHandler',
'level': 'ERROR',
'formatter': 'error',
'filename': './log/error.log',
'mode': 'a',
'maxBytes': 10485760,
'backupCount': 5,
}
},
'root':{
'handlers':('console','file_else','file_error'),
'level':'DEBUG'
}
},
포매터를 작성했으며, 핸들러를 두개 만들었습니다. root 로거에 대한 설정도 했네요.
파일핸들러를 통해서 info.log 파일에 DEBUG 레벨 이상일 경우 출력합니다. 저것을 실제 서비스시 INFO로 바꾸면 디버깅 출력은 disable 되겠죠.
import logging
import logging.config
import json
import os
if __name__ == '__main__':
with open('logging.json', 'rt') as f:
config = json.load(f)
logging.config.dictConfig(config)
logger = logging.getLogger()
logger.info("test!!!")
"loggers": {
"my_module": {
"level": "ERROR",
"handlers": ["console"],
"propagate": "no"
}
},
class LoggerAdapter(logging.LoggerAdapter):
def __init__(self, prefix, logger):
super(LoggerAdapter, self).__init__(logger, {})
self.prefix = prefix
def process(self, msg, kwargs):
return '[%s] %s' % (self.prefix, msg), kwargs
if __name__ == '__main__':
with open('logging.json', 'rt') as f:
config = json.load(f)
logging.config.dictConfig(config)
logger = logging.getLogger("")
logger = LoggerAdapter("SAS", logger)
logger.info("test!!!") 결과: 2017-08-07 12:54:55,911 - root - INFO - [SAS] test!!!!
CRITICAL = 50
FATAL = CRITICAL
ERROR = 40
WARNING = 30
WARN = WARNING
INFO = 20
DEBUG = 10
NOTSET = 0
logging.addLevelName(15, "DATA")
logging.DATA = 15
logger.log(logging.DATA, "message")
import logging
import threading
import time
def worker(arg):
while not arg['stop']:
logging.debug('Hi from myfunc')
time.sleep(0.5)
def main():
logging.basicConfig(level=logging.DEBUG, format='%(relativeCreated)6d %(threadName)s %(message)s')
info = {'stop': False}
thread = threading.Thread(target=worker, args=(info,))
thread.start()
while True:
try:
logging.debug('Hello from main')
time.sleep(0.75)
except KeyboardInterrupt:
info['stop'] = True
break
thread.join()
if __name__ == '__main__':
main()
import Queue
import threading
import traceback
import logging
import sys
class QueueListener(threading.Thread):
def __init__(self, queue, stream_h):
threading.Thread.__init__(self)
self.queue = queue
self.daemon = True
self.logger = logging.getLogger("main")
self.logger.addHandler(self.handler)
def run(self):
while True:
try:
record = self.queue.get()
self.logger.callHandlers(record)
except (KeyboardInterrupt, SystemExit):
raise
except EOFError:
break
except:
traceback.print_exc(file=sys.stderr)
class QueueHandler(logging.Handler):
def __init__(self, queue):
logging.Handler.__init__(self)
self.queue = queue
def emit(self, record):
self.queue.put(record)
if __name__ == '__main__': # 리스터 부분
logging_q = Queue.Queue(-1)
stream_h = logging.StreamHandler()
log_queue_reader = QueueListener(logging_q,stream_h)
log_queue_reader.start()
# 핸들러 부분
handler = QueueHandler(logging_q)
root = logging.getLogger()
root.addHandler(handler)
# 사용
root.error("queue handler test!!")
큐 생성)
큐에 데이터를 담아줄 QueueHandler 를 만듭니다. 핸들러는 기본적으로 emit 메소드를 가지고 있어서 이 메소드를 통해서 log 정보를 (LogRecord 객체) 콘솔로 쏴주거나, 파일로 쏘게 됩니다만, 여기서는 큐에 데이터를 넣습니다.
실제 처리를 담당할 핸들러를 위에서는 스트림 핸들러를 만들어서 매개변수로 넣어주었습니다.
위에서는 쓰레드를 사용했지만, 멀티프로세싱을 통해서 할 수도 있으며, 이 방식은 파이썬에서 문제가 되는 멀티프로세싱 로깅을 해결 할 수 있는 실마리를 제공해줍니다. 이 부분은 고급에서 배우게 될 것입니다.
이전 로그파일을 압축하려고 할 때 우리는 , 압축 할 파일을 잠시 close 하고 압축을 하게 되는데, 윈도우즈에서는 압축이 안되는 현상 및 기존 로그 파일을 날짜가 붙은 새로운 이름으로 변경 하러 할 때도 안되는 현상이 있다.이것은 다른 프로세스에서 그 로그파일을 열고 있기 때문인데 우분투에서는 잘되었다. 아마 파일 디스크립터는 다르기 때문으로 추정한다. 아무튼 운영체제별 이런 차이들이 있어서 좀 골치 아프다. 마지막으로 멀티프로세스 환경의 로깅에서는 각각 따로 쓰면 파일에 써지지 않기 때문에 멀티프로세스용 큐를 이용하여 하나로 모아서 파일에 쓰게 되는 것을 보게 될 것이다.
윈도우)
- 2개의 프로세스에서 하나의 파일에 쓰기를 할 경우 윈도우는 예측 불가능이고 (하나의 프로세스 것만 써질 수도 있고, 부분 누락이 되며 순서대로 써질 수도 있고)
- 파일 삭제는 불가능
- 압축파일에 파일 옮겨 쓰는것은 예측 불가하다.
- 다만 하나의 프로세스에서 파일을 close 하면 ,그 이후로 다른프로세스의 데이터는 파일에 써지며, close 한 프로세스에서 쓴 데이터의 일부는 압축도 된다.
리눅스)
- 2개의 프로세스에서 하나의 파일에 쓰기를 할 경우 리눅스는 하나의 프로세스 것만 써진다.
- 파일 삭제는 가능하다.
- 압축파일로 옮겨 쓰는 것도 가능하다. 다만 파일을 close 해주고 해야한다. 물론 메인프로세스에서 쓴 내용만 압축된다.
고급
* 용량 넘어선 로깅파일 나누기
https://github.com/Clivern/pylogging
그래도 먼가 많은 기능들이 다 있으며, 표준처럼 활용하는 멋진 로깅라이브러리가 없는건 아쉽네요.
출처: https://hamait.tistory.com/880 [HAMA 블로그]
'C Lang > Python Program Diary' 카테고리의 다른 글
RESTful API Designing guidelines — The best practices (0) | 2019.09.09 |
---|---|
What is Serializable? 직렬화란 무엇인가? (0) | 2019.09.02 |
named parameter로 파이썬 구동하기(ex] python app.py --env=dev) (0) | 2019.08.27 |
nginx - gunicorn - supervisor로 파이썬 디플로이하기 (0) | 2019.08.16 |
python에서 dict형은 순서를 보장하는가? (0) | 2019.08.13 |