Python

파이썬 로깅의 모든것

[하마] 이승현 (wowlsh93@gmail.com) 2017. 8. 7. 11:09


파이썬 로깅의 모든것 

파이썬 로깅에 대한 "모든것은" 사실 낚시구요. ㅎㅎ (유희열이 진행했던 All that music 이라는 라디오 프로그램에서 내가 좋아하는 국악이나 시부야계는 거의 나오지도 않았었다...먼소리야 이게 ;;) 그래도 블로그용으로는 꽤나 많은 정보를 담으려고 애를 썼습니다. 파이썬에서 자체 제공하는 기본 로깅 시스템에 대한 내용이구요. 버전이 올라감에 따라 내용이 달라질수 있으며, 오류도 있을 것이고 더 손쉬운 방법등이 있을 수 있음을 인지하시고 항상 다른것들도 같이 확인 하세요. 

본론으로 바로 들어가시죠. 

먼저 제가 원하는 로깅 시스템은 이렇습니다.  IoT 의 게이트웨이 안에서 활약 할 놈으로써~

  1. logging.conf 파일을 통해서 외부에서 설정 가능

  2. 레벨을 자유롭게 설정하고 추가 할 수 있어야 한다.
    (CRITICAL, ERROR,INFO, DEBUG )

  3. 파일,스트림에 동시에 출력 할 수 있어야 한다.

  4. 다양한 목적에 따라 다양한 파일에 출력 할 수 있어야 한다.

  5. 로깅 시간 출력 및 다양한 정보에 대한 추가가 가능해야 한다.

  6. 하루에 한번씩 파일을 생성 해야하며, 지난 파일은 압축하여 보관 해야한다.

  7. 하루에 한번씩 파일을 생성 해야하며, 오래된 파일을 삭제 할 수 있어야 한다.

  8. 파일 용량이 너무 커질 경우에는 자동으로 분리 시켜야 한다.

  9. 멀티프로세스를 활용해야 하는 파이썬의 특성상 멀티프로세스에서도 로그를 취득할 수 있어야 한다.

  10. 핸들러를 커스터마이징 할 수 있어야 한다. 즉 콘솔,파일 출력 뿐 만 아니라, DB 및 소켓을 통하여도 발송 할 수 있어야 한다.

  11. 사용하기 쉬워야 한다. (문서화가 잘 되 있어야 한다) , 기본 로깅 모듈을 사용해야한다.

이것을 해결 하기 위해 알아야 할 것들에 대해 공부해 보는것이 이 글의 목표 입니다. 즉 이 글을 모두 읽고 나면 모든 기능을 본인의 프로젝트에서 사용 할 수 있게 될 것이라 봅니다.

* 참고로 자바개발자에게 익숙한 log4j 같은 거대한 로깅 시스템에 비하면 제공되는 기능이 조금 부족합니다. 몇가지는 직접만들어야합니다.
* 2.x / 3.x 상관없으나, 몇몇 부분은 2.7 기준입니다. 3.x 버전의 새기능 활용 안함. 

초급 

로깅이란? 

현재 우리의 프로그램이 어떤 상태를 가지고 있는지, 외부출력을 하게 만들어서 개발자등이 눈으로 직접 확인하는 것 입니다. 이때 

print (" 난 지금 매우 좋아 ") 
print (" 30이 들어왔어")

이렇게 콘솔로 출력하는 방법이 가장 쉽고 편하긴 합니다만, 파일로의 출력 등 좀 더 다양한 기능을 사용하고 싶을 때 우리는 로깅 시스템을 따로 만들거나, 기존에 구축되어진 라이브러리를 가져와서 사용합니다. 

로깅 모듈 사용하기

파이썬에서 로깅 모듈은 기본 라이브러리에 포함되어 있기 때문에, 굳이 따로 설치 할 필요는 없습니다.

import logging

if __name__ == '__main__':
logging.error("something wrong!!") 결과: ERROR:root:somthing wrong!!

이렇게 logging 모듈을 임포트해서 그냥 사용하면 됩니다. 
위에서는 아무 전처리도 하지 않고 그저  logging 모듈 안의 error 함수를 호출 했습니다. 

로깅 레벨

위에서는 출력 할 때 error 함수를 사용했는데요. 이름에서 느껴지다 시피 먼가 큰 문제가 생겼음을 알려주기 위한 출력이었습니다. 하지만 평범한 정보를 알려 주기 위해서 사용하고 싶을 때가 있을 것입니다.

" 지금 서버가 시작됬어요."
" 서버의 대기포트는 이 번호로 시작 됬습니다"
" 어떤 함수가 호출 되었어요"
" 100이 들어왔어요"

이럴 때는 info 같은 레벨이 좋을거 같습니다. 즉 출력하는 정보의 등급을 나누어서, 구분 지으면 나중에 확인 할 때 편할 거 같습니다.보통 DEBUG, INFO, WARNING, ERROR, CRITICAL 의 5가지 등급이 기본적으로 사용됩니다.

import logging

if __name__ == '__main__':
logging.info("hello world")

응?  출력이 안되네요? 

여기서 예를 하나 들어보죠. 만약 print 로 모든 로깅을 출력한다면, 실제 프로그램이 배포 되었을 경우, 수많은 정보를 출력하게 되고  그것이 오히려 프로그램에 성능 부담이 갈 수도 있겠죠. 또한 출력을 파일로 한다면 파일이 너무 커져서 시스템을 멈출 수도 있을 거 같습니다. 따라서 설정 같은 것을 통해 로깅을 출력을 하지 말아라~ 라고 설정해서 로깅이 출력이 안되게 하면 매우 편할 거 같습니다. 

보통 DEBUG 레벨에서 굉장히 많은 정보들을 출력하게 되는데, 개발시에만 DEBUG 레벨의 정보를 보고 싶고, 실제 서비스를 할 경우는 DEBUG 레벨은 보고싶지 않을 수 있습니다.

INFO 레벨보다 심각한 것만 출력하게 하라!! 라고 로깅 시스템을 사용하면 설정 가능합니다.

파이썬 로깅의 기본 설정은 WARNING 입니다. 따라서  DEBUG < INFO < WARNING < ERROR < CRITICAL  
이 순서에서 더 높은 레벨인 ERROR 는 출력이 되지만, 하위레벨 (INFO,DEBUG) 은 출력이 안됩니다.
즉 이 레벨을 DEBUG 나 INFO 로 낮추어 설정 한후에 사용해야 합니다. 설정하는 방법은 조금 있다가 살펴보죠.


나의 로깅 모듈 사용하기 - 1 (생성)
제일 처음에  logging.error("somthing wrong") 이렇게 error 함수를 바로 사용 했었는데요.
근데 이렇게 직접 함수를 호출하기도 하지만, 자신만의 특정한 로거를 따로 만들어서 사용하는게 보통입니다.

import logging

if __name__ == '__main__':
mylogger = logging.getLogger("my")

이렇게 호출하면 "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("my") 라고 만들었었는데요.  
그냥 매개변수 없이 logging.getLogger() or logging.getLogger("") 이렇게 만들 수도 있습니다. 

이렇게 하면 루트로거 를 리턴하게 되며, 루트로거는 로깅시스템의 기본로거이고 기본레벨은 warning 을 가지고 있습니다. 우리가 젤 처음에 로거를 만들지 않고 그냥 logging.error("...") 식으로 사용했었죠? 내부에서는 루트로거를 사용하고 있답니다.

로깅 생성에 대한 특성을 정리 해 보면 아래와 같습니다. 

* logging.getLogger() 를 통해 루트 로거 얻어서 사용 할수 있습니다.(기본레벨은 warning)
* logging.error() 를 직접 호출하면 내부에서 루트로거를 이용합니다.
* Logger 인스턴스는 이름으로 식별됩니다.
* 동일 모듈 뿐만 아니라, 여러 모듈에서 동일한 인스턴스이름 logging.getLogger('someLogger')를 여러 번 호출해도 동일한 로거 객체에 대한 참조가 반환됩니다.
* 빈 문자열인 "" 라는 이름의 루트 로거가 있으며, 다른 Logger 들은 모두 루트 Logger 의 자식들입니다. 따라서 루트 로거에 기본적인 설정을 해두고 사용하면 자식들은 모두 동일한 설정을 사용하게 됩니다.(핸들러,포매터등) 
*이름은 마침표(.) 로 구분되는 문자열이며 계층 구조를 형성한다. 즉 a 라는 로거를 만들고, a.b 라는 로거를 만들면 a.b는 a 로거의 자식입니다. 
하나의 모듈에서 상위 로거를 정의 및 구성하고 별도의 모듈에서 하위 로거를 생성 할 수 있으며 하위에 대한 모든 로거 호출은 상위 노드로 전달됩니다.
* 가능하면 루트 로거를 사용하기 보다는 각 클래스나 모듈마다 별도의 로거를 만드는 것을 추천합니다.
* 로깅 메세지는 부가정보가 합쳐져서 LogRecords 로 생성되며, 이 객체를 가지고 각각의 handler 는 파일,콘솔,TCP 등으로 로 emit (출력) 합니다

로깅 모듈 생성 실제 - 로거 범위 

보통 main 에서 로깅 시스템 (대표설정) 을 만들어서 사용하게 됩니다. 이것을 디폴트로 자식 로거들에서 사용하게 되며, 자식별로 필요하면 특정 설정을 하게 됩니다. 또한 핸들러는 버퍼링을 하고 있기 때문에 어플리케이션이 갑자기 중지되기 전에 logging.shutdown() 을 호출해서 모두 출력해 주면 좋습니다. (fianllly 절에서 수행) 

이제 로거를 만드는 대표적인 범위를 살펴보시죠.

@ 모듈명 : 많은 함수와 클래스들을 포함하는 모듈의 전역 Logger 인스턴스로 생성 

import logging
logger = logging.getLogger(__name__)

@객체인스턴스 : __init__() 메소드 내에서 Logger 를 생성. 인스턴스 마다 고유하다. 

def __init__(self, name)
self.logger = logging.getLogger("{0}.{1}".format(self.__class__.qualname__, name))

@클래스명 : __class__.__qualname__  만으로 생성 

@함수명 :  잘 사용되지 않는 큰 함수라면 함수 내에서 로그를 생성 할 수도 있다.

def main():
    logger = logging.getLogger("main") 

파이썬에서는 하나의 파일이 하나의 모듈과 일치합니다. 따라서 파일의 제일 위쪽에서 모듈명으로 로거객체를 만들 수 있으며, 함수별/클래스별/객체별로도 만들 수 있습니다. 자신의 로깅 전략에 따라서 만들면 될 거 같습니다.

로깅 모듈 생성 실제 - 로거 부모/자식 관계 

위에서 로거는 부모/자식관계를 갖는다고 말했는데요. 즉 루트로거는 모든 로거의 부모이며,  a.b 라는 로거는 a 라는 로거의 자식입니다. ( a라는 로거가 없는데 a.b 로거를 만들 수도 있습니다. 내부적으로 a 로거는 자동으로 place holding 됩니다) 

저런 관계에 따른 이상 행동을 예측 하기 위해서는 부모/자식간의 행동을 파악할 필요가 있는데요.
다음 코드를 보시죠. 

# 루트 로거 생성 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")

루트로거를 a 모듈에서 만들고, "my" 로거를 다른 곳에서 만들었다고 칩시다.( 위에 소스에서는 보기 쉽게 같은 곳에 두었음)  "my" 로거에서 "message from mylogger" 를 출력 했는데, 결과는 어떻게 될까요? 

message from mylogger
message from mylogger
 
이렇게 2개의 메세지가 출력되는데 상위로 전파되었기 때문입니다.
이것을 막으려면, 간단하게는 루트로거에서 핸들러를 추가해 주지 않아도 되며, 혹은
mylogger.propagate = 0
propagate 를 0 으로 세팅하면 상위로 전파하지 않습니다.

로깅 모듈 생성 실제 - 파일을 통한 설정 

외부 파일을 통해서, 로깅레벨, 핸들러 (파일로 할것인지, 콘솔로 할 것인지 등) 와 포매터등을 설정 할 수도 있습니다. 이렇게 되면 굳이 소스를 건드리지 않고도 간편히 설정가능하겠지요? 
파일 포맷은 JSON, INI, YAML 등 다양하게 할 수 있습니다만 여기서는 JSON을 살펴보겠습니다.

logging.json 파일은 아래와 같습니다.

{
"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"]
}
}

포매터를 작성했으며, 핸들러를 두개 만들었습니다. root 로거에 대한 설정도 했네요.
파일핸들러를 통해서 info.log 파일에 DEBUG 레벨 이상일 경우 출력합니다. 저것을 실제 서비스시 INFO로 바꾸면 디버깅 출력은 disable 되겠죠. 

루트 로거의 레벨이 DEBUG 이기 때문에 모든 레벨을 다 출력하게 됩니다만, 자식로거에서 레벨을 높히게 되면 자식이 우선순위를 갖게 됩니다.

소스에서는 아래와 같이 logging.json 을 가져다가 사용합니다.


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!!!")

파일을 읽어서 logging.config.dictConfig 를 통해서 설정을 세팅하고, 루트로거를 가져다가 사용하게 됩니다.
만약 설정 파일에 아래와 같은 특정 로거를 세팅해 주게되면 

"loggers": {
"my_module": {
"level": "ERROR",
"handlers": ["console"],
"propagate": "no"
}
},

logging.getLogger("my_module") 식으로 가져와서 사용하면 저 속성을 갖게 됩니다.

로깅 모듈 생성 실제 - 부가정보 출력하기
로깅 호출에 전달 된 매개 변수 외에도 컨텍스트 정보를 포함하도록 로깅 출력을 원하는 경우가 있습니다.
이때 LoggerAdapters 를 사용 할 수 있는데요.

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!!!!

logging.LoggerAdapter 를 상속받아서 만든 어댑터를 통해서 SAS 라는 문구를 메세지 앞에 고정 시킬 수 있게 됩니다. process 메소드에 prefix 를 추가한것을 확인하세요. 

로깅 모듈 생성 실제 - 커스텀 로그레벨 설정하기 
저같이 센서로 부터 데이터를 받는 IoT 서비스에서 로깅을 출력 할 때 DATA 만을 확인해 보고 싶을 경우가 있습니다. 이 경우 DEBUG 보다 한단계 아래에 로깅 설정을 할 수 있게 되는데요 
CRITICAL = 50
FATAL = CRITICAL
ERROR = 40
WARNING = 30
WARN = WARNING
INFO = 20
DEBUG = 10
NOTSET = 0
위처럼 각 레벨에는 숫자가 10단위로 적용되어 있습니다. 예상 할 수 있다시피, 내부에서는

if level > 세팅된 레벨:
   출력 

식으로 되어 있어서 세팅된 레벨보다 출력레벨이 클 경우에 한 해서만 출력하고 있습니다.

if ERROR  > INFO :   세팅이 INFO 면 ERROR 는 출력
if DEBUG > INFO :    세팅이 INFO 면 DEBUG 는 출력하지 않음 

따라서 DATA = 15 이렇게 설정해 두면, DEBUG 정보는 출력하지 않고, DATA 레벨만  출력하게 됩니다.

사용예는 아래와 같습니다. 


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()
실행하면 스크립트는 다음과 같이 출력 합니다.

  1 MainThread Hello from main
     1 Thread-1 Hi from myfunc
   503 Thread-1 Hi from myfunc
   751 MainThread Hello from main
  1003 Thread-1 Hi from myfunc
  1503 MainThread Hello from main
  1503 Thread-1 Hi from myfunc
  2004 Thread-1 Hi from myfunc


블럭되는 핸들러 유연하게 다루기 
파이썬 로깅 시스템에서 핸들러를 사용 하여 출력을 하게 되는데, 중간에 버퍼링을 하고 있지 않기 때문에, 쓰기에서 오랜 지연시간이 걸리는 행동을 하게 된다면,  전체 시스템은 느려지게 마련입니다. 주로 이메일 전송의 경우 이런 행동이 자주 나타나곤 하는데, 이 경우 중간 큐를 사용하여 버퍼링을 해 줄 필요가 있습니다. 
 
이때 QueueHandler 와 QueueListener 를 사용하면 됩니다. 문제는 파이썬 2.7에서는 이것을 지원하지 않기 때문에 직접 비스무리하게 만들어야 합니다. 


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!!")

어떻게 활용하는지 설명드리면 

큐 생성)
먼저 버퍼로 사용할 큐를 -1 을 매개변수로 만들고, 

데이터 입력)
큐에 데이터를 담아줄 QueueHandler 를 만듭니다. 핸들러는 기본적으로 emit 메소드를 가지고 있어서 이 메소드를 통해서 log 정보를 (LogRecord 객체) 콘솔로 쏴주거나, 파일로 쏘게 됩니다만, 여기서는 큐에 데이터를 넣습니다.

데이터 처리 ) 
큐에서 데이터 (LogRecord 객체) 를 가져와서, 핸들러에게 넘겨주는 역할을 하는 QueueListener 를 만듭니다.
실제 처리를 담당할 핸들러를 위에서는 스트림 핸들러를 만들어서 매개변수로 넣어주었습니다.

그 후에는 그냥 로거 사용하듯이 사용하면 됩니다. 그러면 중간에 큐를 거쳐서 출력하게 되겠지요. 
위에서는 쓰레드를 사용했지만, 멀티프로세싱을 통해서 할 수도 있으며, 이 방식은 파이썬에서 문제가 되는 멀티프로세싱 로깅을 해결 할 수 있는 실마리를 제공해줍니다. 
이 부분은 고급에서 배우게 될 것입니다.

노트: 
고급을 진행하기 전에 멀티프로세스에 관해서 알아보자. 아시다시피 파이썬에서는 멀티쓰레드가 I/O 를 제외하고 동시계산하는 곳에서는 그닥 효율적이지 않기 때문에, 멀티프로세스를 사용하게 된다. 이때 로깅도 프로세스 각각에서 사용하게 되는데, 이때 동일한 파일이 두 프로세스에 의해 열려져 있기 도 한다.이런 상황에서 여러 문제가 발생하고 해결 해야 하는 문제가 있다.
이전 로그파일을 압축하려고 할 때 우리는 , 압축 할 파일을 잠시 close 하고  압축을 하게 되는데, 윈도우즈에서는 압축이 안되는 현상 및 기존 로그 파일을 날짜가 붙은 새로운 이름으로 변경 하러 할 때도 안되는 현상이 있다.이것은 다른 프로세스에서 그 로그파일을 열고 있기 때문인데 우분투에서는 잘되었다. 아마 파일 디스크립터는 다르기 때문으로 추정한다.  아무튼 운영체제별 이런 차이들이 있어서 좀 골치 아프다. 마지막으로 멀티프로세스 환경의 로깅에서는 
각각 따로 쓰면 파일에 써지지 않기 때문에  멀티프로세스용 큐를 이용하여 하나로 모아서 파일에 쓰게 되는 것을 보게 될 것이다


윈도우) 
- 2개의 프로세스에서 하나의 파일에 쓰기를 할 경우 윈도우는 예측 불가능이고 (하나의 프로세스 것만 써질 수도 있고, 부분 누락이 되며 순서대로 써질 수도 있고)
- 파일 삭제는 불가능
- 압축파일에 파일 옮겨 쓰는것은 예측 불가하다.
- 다만 하나의 프로세스에서 파일을 close 하면 ,그 이후로 다른프로세스의 데이터는 파일에 써지며, close 한 프로세스에서 쓴 데이터의 일부는 압축도 된다.   

리눅스) 
- 2개의 프로세스에서 하나의 파일에 쓰기를 할 경우 리눅스는 하나의 프로세스 것만 써진다.  
- 파일 삭제는 가능하다.
- 압축파일로 옮겨 쓰는 것도 가능하다. 다만 파일을 close 해주고 해야한다. 물론 메인프로세스에서 쓴 내용만 압축된다.


고급

고급은 아래와 같은 주제를 다룹니다.  조금 숨 좀 돌리고 작성 하도록 할께요~다음 기회에 봐요 ~:-) 

* 추가 컨텍스트 정보 출력 하기 / 필터 사용하기 
* 소켓핸들러 작성 
* 멀티프로세싱을 위한 로깅 작성
* 용량 넘어선 로깅파일 나누기 
* 어제로깅파일을 압축하기
* 한달전 로깅파일을 삭제하기  


참고로 기본 로깅보다 조금 더 구색을 갖춘 pylogging 이란 것도 있습니다.
https://github.com/Clivern/pylogging

그래도 먼가 많은 기능들이 다 있으며, 표준처럼 활용하는 멋진 로깅라이브러리가 없는건 아쉽네요.


레퍼런스) 

https://docs.python.org/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes 
https://fangpenlin.com/posts/2012/08/26/good-logging-practice-in-python/
https://stackoverflow.com/questions/8467978/python-want-logging-with-log-rotation-and-compression
https://www.amazon.com/Mastering-Object-oriented-Python-Steven-Lott/dp/1783280972/ref=sr_1_2?ie=UTF8&qid=1501725014&sr=8-2&keywords=mastering+python