Logging HOWTO

Logging HOWTO

프로그램이 실행되는 동안 어떤 일이 일어났는지 기록해 두는 것을 로깅(logging)이라고 해요. 단순한 경우에는 print()로 화면에 찍어도 되지만, 프로그램이 정상 동작 중에 벌어지는 이벤트를 추적하거나 오류를 조사해야 하는 상황에서는 구조화된 로깅이 훨씬 편리합니다. 파이썬 표준 라이브러리의 logging 패키지는 이런 용도를 위한 강력한 도구인데요, 이 HOWTO는 기초부터 고급 설정까지 로깅을 차근차근 안내해 줍니다.

출처: Python 공식 문서 — Logging HOWTO

기초 로깅 튜토리얼 (Basic Logging Tutorial)

로깅은 어떤 소프트웨어가 실행될 때 일어나는 이벤트를 추적하는 수단입니다. 소프트웨어 개발자는 특정 이벤트가 발생했음을 나타내기 위해 코드에 로깅 호출을 추가하죠. 이벤트는 설명적인 메시지로 기술되는데, 그 메시지는 선택적으로 가변 데이터(즉, 이벤트가 발생할 때마다 달라질 수 있는 데이터)를 담을 수 있어요. 이벤트는 또한 개발자가 부여한 중요도(importance)를 가지는데, 이를 레벨(level) 또는 *심각도(severity)*라고도 부릅니다.

로깅을 언제 써야 할까 (When to use logging)

logger = logging.getLogger(__name__)으로 logger를 만든 뒤, logger의 debug(), info(), warning(), error(), critical() 메서드를 호출하면 로깅 기능에 접근할 수 있습니다. 로깅을 언제 써야 하는지, 그리고 그때 어떤 logger 메서드를 써야 하는지 결정하려면 아래 표를 보세요. 각각의 흔한 작업에 대해 그 작업에 가장 적합한 도구를 말해 줍니다.

수행하려는 작업 그 작업에 가장 적합한 도구
명령줄 스크립트나 프로그램의 일반적 사용에서 콘솔 출력 표시 print()
프로그램의 정상 동작 중 발생하는 이벤트 보고(예: 상태 모니터링이나 장애 조사) logger의 info()(진단 목적의 매우 상세한 출력이라면 debug() 메서드)
특정 런타임 이벤트에 관한 경고 발행 문제를 피할 수 있고 클라이언트 애플리케이션이 경고를 없애도록 수정해야 한다면 라이브러리 코드에서 warnings.warn() / 클라이언트 애플리케이션이 할 수 있는 일이 없지만 그 이벤트를 기록은 해야 한다면 logger의 warning() 메서드
특정 런타임 이벤트에 관한 오류 보고 예외를 일으킨다(raise)
예외를 일으키지 않고 오류의 억제를 보고(예: 오래 실행되는 서버 프로세스의 오류 처리기) 특정 오류와 애플리케이션 도메인에 따라 logger의 error(), exception() 또는 critical() 메서드

logger 메서드들은 추적하는 이벤트의 레벨 또는 심각도에 따라 이름이 붙습니다. 표준 레벨과 그 적용 범위는 아래와 같습니다(심각도가 증가하는 순서).

레벨 사용 시점
DEBUG 상세 정보. 보통 문제를 진단할 때만 관심을 가짐.
INFO 일이 예상대로 동작하고 있음을 확인.
WARNING 예상치 못한 일이 발생했거나 가까운 미래에 어떤 문제가 있을 것임을 나타냄(예: '디스크 공간 부족'). 소프트웨어는 여전히 예상대로 동작함.
ERROR 더 심각한 문제 때문에 소프트웨어가 어떤 기능을 수행하지 못함.
CRITICAL 심각한 오류. 프로그램 자체가 계속 실행되지 못할 수도 있음을 나타냄.

기본 레벨은 WARNING입니다. 즉 logging 패키지가 다르게 구성되지 않는 한 이 심각도 이상의 이벤트만 추적된다는 뜻이에요.

추적되는 이벤트는 다양한 방식으로 처리될 수 있습니다. 추적 이벤트를 처리하는 가장 간단한 방법은 콘솔에 출력하는 것이고, 또 다른 흔한 방법은 디스크 파일에 기록하는 것입니다.

간단한 예시 (A simple example)

아주 간단한 예시는 이렇습니다.

import logging
logging.warning('Watch out!')  # will print a message to the console
logging.info('I told you so')  # will not print anything

이 줄들을 스크립트에 타이핑하고 실행하면 콘솔에 이런 출력이 찍힙니다.

WARNING:root:Watch out!

INFO 메시지는 기본 레벨이 WARNING이라 나타나지 않아요. 출력된 메시지에는 레벨 표시와 로깅 호출에서 제공한 이벤트 설명(즉 'Watch out!')이 포함됩니다. 필요하다면 실제 출력 형식을 아주 유연하게 바꿀 수도 있는데, 포매팅 옵션에 대해서는 나중에 설명할게요.

이 예시에서 우리는 logging.debug처럼 logging 모듈에서 직접 함수를 사용했지, logger를 만들어 그 위에서 함수를 호출하지는 않았어요. 이 함수들은 루트 logger에 대해 동작하지만, 아직 basicConfig()이 호출되지 않은 상태라면 이 예시처럼 그것을 대신 호출해 주므로 유용할 수 있습니다. 다만 더 큰 프로그램에서는 보통 로깅 구성을 명시적으로 제어하고 싶을 거예요. 그래서 이런 이유와 다른 이유 때문에 logger를 만들고 그 메서드를 호출하는 편이 더 낫습니다.

파일로 로깅하기 (Logging to a file)

로깅 이벤트를 파일에 기록하는 것은 아주 흔한 상황이라, 이어서 살펴볼게요. 새로 시작한 파이썬 인터프리터에서 다음을 시도하되, 위에서 설명한 세션을 계속하지는 마세요.

import logging
logger = logging.getLogger(__name__)
logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.DEBUG)
logger.debug('This message should go to the log file')
logger.info('So should this')
logger.warning('And this, too')
logger.error('And non-ASCII stuff, too, like Øresund and Malmö')

버전 3.9에서 변경: encoding 인자가 추가되었습니다. 더 이른 파이썬 버전에서, 또는 지정하지 않으면 사용되는 인코딩은 open()이 사용하는 기본값입니다. 위 예시에는 보이지 않지만, 이제 errors 인자도 전달할 수 있는데, 이는 인코딩 오류가 어떻게 처리되는지를 결정합니다. 사용 가능한 값과 기본값은 open() 문서를 보세요.

이제 파일을 열어 무엇이 있는지 보면 로그 메시지를 찾을 수 있을 거예요.

DEBUG:__main__:This message should go to the log file
INFO:__main__:So should this
WARNING:__main__:And this, too
ERROR:__main__:And non-ASCII stuff, too, like Øresund and Malmö

이 예시는 추적의 임계값 역할을 하는 로깅 레벨을 어떻게 설정하는지도 보여줍니다. 여기서는 임계값을 DEBUG로 설정했기 때문에 모든 메시지가 출력되었어요.

--log=INFO 같은 명령줄 옵션으로 로깅 레벨을 설정하고 싶다면:

--log=INFO

그리고 --log에 전달된 매개변수 값을 어떤 변수 loglevel에 갖고 있다면, 이렇게 쓸 수 있습니다.

getattr(logging, loglevel.upper())

그러면 basicConfig()level 인자로 전달할 값을 얻을 수 있어요. 사용자 입력 값을 오류 검사하고 싶을 수 있는데, 아마 다음 예시처럼 하면 됩니다.

# assuming loglevel is bound to the string value obtained from the
# command line argument. Convert to upper case to allow the user to
# specify --log=DEBUG or --log=debug
numeric_level = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_level, int):
    raise ValueError('Invalid log level: %s' % loglevel)
logging.basicConfig(level=numeric_level, ...)

basicConfig() 호출은 debug(), info() 같은 logger 메서드에 대한 어떤 호출보다 먼저 와야 합니다. 그렇지 않으면 그 로깅 이벤트가 원하는 방식으로 처리되지 않을 수 있어요.

위 스크립트를 여러 번 실행하면 연속 실행의 메시지들이 파일 example.log에 추가됩니다. 각 실행이 이전 실행의 메시지를 기억하지 않고 새로 시작하게 하려면 filemode 인자를 지정하면 되는데, 위 예시의 호출을 이렇게 바꾸면 됩니다.

logging.basicConfig(filename='example.log', filemode='w', level=logging.DEBUG)

출력은 이전과 같지만 로그 파일에 더 이상 추가되지 않으므로, 이전 실행의 메시지들은 사라집니다.

가변 데이터 로깅하기 (Logging variable data)

가변 데이터를 기록하려면 이벤트 설명 메시지에 포맷 문자열을 쓰고 가변 데이터를 인자로 덧붙이세요. 예를 들어:

import logging
logging.warning('%s before you %s', 'Look', 'leap!')

이렇게 표시됩니다.

WARNING:root:Look before you leap!

보시다시피 가변 데이터를 이벤트 설명 메시지에 병합하는 것은 옛날 %-스타일 문자열 포매팅을 사용합니다. 이는 하위 호환성 때문인데, logging 패키지는 str.format()string.Template 같은 더 새로운 포매팅 옵션보다 먼저 나왔기 때문이에요. 이런 더 새로운 포매팅 옵션 역시 지원되지만, 그것을 탐구하는 것은 이 튜토리얼의 범위 밖입니다. 자세한 내용은 "애플리케이션 전체에서 특정 포매팅 스타일 사용하기"를 참고하세요.

표시되는 메시지의 형식 바꾸기 (Changing the format of displayed messages)

메시지를 표시하는 데 사용되는 형식을 바꾸려면 사용하려는 형식을 지정해야 합니다.

import logging
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG)
logging.debug('This message should appear on the console')
logging.info('So should this')
logging.warning('And this, too')

이 코드는 이렇게 출력합니다.

DEBUG:This message should appear on the console
INFO:So should this
WARNING:And this, too

이전 예시에 나타났던 'root'가 사라진 것을 볼 수 있어요. 포맷 문자열에 나타날 수 있는 전체 목록은 LogRecord 속성 문서를 참고할 수 있지만, 간단한 사용에서는 그냥 levelname(심각도), message(가변 데이터를 포함한 이벤트 설명), 그리고 아마 이벤트가 발생한 시각을 표시하는 것만 있으면 됩니다. 이것은 다음 절에서 설명할게요.

메시지에 날짜/시간 표시하기 (Displaying the date/time in messages)

이벤트의 날짜와 시간을 표시하려면 포맷 문자열에 '%(asctime)s'를 넣으면 됩니다.

import logging
logging.basicConfig(format='%(asctime)s %(message)s')
logging.warning('is when this event was logged.')

이 코드는 이렇게 출력합니다.

2010-12-12 11:41:42,612 is when this event was logged.

날짜/시간 표시의 기본 형식(위에 나온)은 ISO8601 또는 RFC 3339와 비슷합니다. 날짜/시간 형식을 더 제어해야 한다면 다음 예시처럼 basicConfigdatefmt 인자를 제공하세요.

import logging
logging.basicConfig(format='%(asctime)s %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
logging.warning('is when this event was logged.')

이 코드는 이렇게 표시합니다.

12/12/2010 11:46:36 AM is when this event was logged.

datefmt 인자의 형식은 time.strftime()이 지원하는 것과 같습니다.

다음 단계 (Next Steps)

이것으로 기초 튜토리얼이 끝입니다. 로깅을 시작해서 동작하게 만드는 데는 이것으로 충분해요. logging 패키지가 제공하는 것은 훨씬 많지만, 그것을 최대한 활용하려면 다음 절들을 읽는 데 시간을 조금 더 투자해야 합니다. 준비가 된다면 좋아하는 음료를 하나 들고 계속 읽어 보세요.

로깅 요구가 단순하다면 위 예시들을 사용해 자신의 스크립트에 로깅을 통합하고, 문제가 생기거나 이해되지 않는 부분이 있다면 파이썬 토론 포럼의 Help 카테고리에 질문을 올리면 얼마 지나지 않아 도움을 받을 수 있을 거예요.

아직 여기 있나요? 다음 몇 개 절을 계속 읽을 수 있는데, 거기서는 위의 기초보다 약간 더 고급/심층적인 튜토리얼을 제공합니다. 그 후에는 Logging Cookbook을 살펴볼 수 있어요.

고급 로깅 튜토리얼 (Advanced Logging Tutorial)

logging 라이브러리는 모듈식 접근을 취하며 몇 가지 범주의 컴포넌트를 제공합니다. logger, handler, filter, formatter가 그것입니다.

  • Logger는 애플리케이션 코드가 직접 사용하는 인터페이스를 노출합니다.
  • Handler는 (logger가 만든) 로그 레코드를 적절한 목적지로 보냅니다.
  • Filter는 어떤 로그 레코드를 출력할지 결정하는 더 세밀한 기능을 제공합니다.
  • Formatter는 최종 출력에서 로그 레코드의 배치를 지정합니다.

로그 이벤트 정보는 LogRecord 인스턴스에서 logger, handler, filter, formatter 사이를 전달됩니다.

로깅은 Logger 클래스(이하 loggers)의 인스턴스에 메서드를 호출함으로써 수행됩니다. 각 인스턴스는 이름을 가지며, 개념적으로 점(마침표)을 구분자로 사용하는 네임스페이스 계층으로 배열됩니다. 예를 들어 'scan'이라는 logger는 'scan.text', 'scan.html', 'scan.pdf'라는 logger들의 부모입니다. logger 이름은 무엇이든 될 수 있고, 로그 메시지가 발생한 애플리케이션의 영역을 나타냅니다.

logger 이름을 지을 때 좋은 관례는, 로깅을 사용하는 각 모듈에서 모듈 레벨 logger를 다음과 같이 이름 지어 사용하는 것입니다.

logger = logging.getLogger(__name__)

즉 logger 이름이 패키지/모듈 계층을 추적하게 되어, logger 이름만으로도 이벤트가 어디서 기록됐는지 직관적으로 명확해집니다.

logger 계층의 뿌리를 루트 logger(root logger)라고 합니다. 이것은 debug(), info(), warning(), error(), critical() 함수들이 사용하는 logger인데, 이 함수들은 그저 루트 logger의 같은 이름의 메서드를 호출합니다. 함수와 메서드는 같은 시그니처를 가져요. 루트 logger의 이름은 로그 출력에서 'root'로 인쇄됩니다.

물론 로그 메시지를 다른 목적지로 기록하는 것도 가능합니다. 패키지는 파일, HTTP GET/POST 위치, SMTP를 통한 이메일, 일반 소켓, 큐, 또는 syslog나 Windows NT 이벤트 로그 같은 OS 특유의 로깅 메커니즘에 로그 메시지를 쓰는 것을 지원합니다. 목적지는 handler 클래스들이 담당하죠. 내장 handler 클래스 중 어느 것도 충족하지 못하는 특별한 요구가 있다면 자신만의 로그 목적지 클래스를 만들 수 있어요.

기본적으로 어떤 로깅 메시지에도 목적지가 설정되어 있지 않습니다. 튜토리얼 예시처럼 basicConfig()을 사용해 (콘솔이나 파일 같은) 목적지를 지정할 수 있어요. debug(), info(), warning(), error(), critical() 함수를 호출하면 목적지가 설정되어 있는지 확인하고, 설정되어 있지 않으면 실제 메시지 출력을 루트 logger에 위임하기 전에 콘솔(sys.stderr) 목적지와 표시 메시지의 기본 형식을 설정합니다.

basicConfig()이 메시지에 설정하는 기본 형식은:

severity:logger name:message

format 키워드 인자로 포맷 문자열을 basicConfig()에 전달하면 이것을 바꿀 수 있습니다. 포맷 문자열을 구성하는 방법에 대한 모든 옵션은 Formatter 객체(FORMATTER OBJECTS)를 보세요.

로깅 흐름 (Logging Flow)

logger와 handler에서 로그 이벤트 정보가 흐르는 방식은 다음 순서로 이해할 수 있습니다.

  1. 사용자 코드에서 로깅 호출(예: logger.info(...))이 발생해 LogRecord를 만든다.
  2. logger가 호출의 레벨 처리를 활성화했는지 확인한다. 활성화되지 않았으면 이벤트는 버려진다.
  3. logger에 붙은 필터가 레코드를 거부하는지 확인한다. 거부하면 멈춘다.
  4. 레코드를 현재 logger의 handler들에 전달한다.
  5. 현재 logger에 대해 propagate가 참인지 확인한다. 참이고 부모 logger가 있으면 현재 logger를 부모로 설정하고 2~5를 반복한다.
  6. (계층에 handler가 하나도 없으면) lastResort handler를 사용한다.
  7. 각 handler에서: handler가 이 레코드의 레벨에 대해 활성화되어 있는지, handler에 붙은 필터가 레코드를 거부하지 않는지 확인한 뒤, emit()(포매팅 포함)으로 출력한다.

Logger

Logger 객체는 세 가지 임무를 가집니다. 첫째, 애플리케이션 코드가 런타임에 로그 메시지를 기록할 수 있도록 여러 메서드를 노출합니다. 둘째, logger 객체는 심각도(기본 필터링 기능)나 filter 객체에 기반해 어떤 로그 메시지에 동작할지 결정합니다. 셋째, logger 객체는 관련 로그 메시지를 관심 있는 모든 로그 handler에 전달합니다.

logger 객체의 가장 널리 사용되는 메서드는 구성(configuration)과 메시지 전송의 두 범주로 나뉩니다.

가장 흔한 구성 메서드들은:

  • Logger.setLevel()은 logger가 처리할 가장 낮은 심각도의 로그 메시지를 지정합니다. 여기서 debug는 가장 낮은 내장 심각도이고 critical은 가장 높은 내장 심각도입니다. 예를 들어 심각도 레벨이 INFO면 logger는 INFO, WARNING, ERROR, CRITICAL 메시지만 처리하고 DEBUG 메시지는 무시합니다.
  • Logger.addHandler()Logger.removeHandler()는 logger 객체에서 handler 객체를 추가하고 제거합니다. Handler는 "Handlers" 절에서 더 자세히 다룹니다.
  • Logger.addFilter()Logger.removeFilter()는 logger 객체에서 filter 객체를 추가하고 제거합니다. Filter는 "Filter Objects" 절에서 더 자세히 다룹니다.

만드는 모든 logger에 대해 항상 이 메서드들을 호출할 필요는 없습니다. 이 절의 마지막 두 문단을 보세요.

logger 객체가 구성되면 다음 메서드들이 로그 메시지를 만듭니다.

  • Logger.debug(), Logger.info(), Logger.warning(), Logger.error(), Logger.critical()은 모두 각자의 메서드 이름에 대응하는 메시지와 레벨을 가진 로그 레코드를 만듭니다. 메시지는 실제로는 포맷 문자열이며, %s, %d, %f 등의 표준 문자열 치환 구문을 담을 수 있어요. 나머지 인자들은 메시지의 치환 필드에 대응하는 객체들의 리스트입니다. **kwargs에 관해서는, 로깅 메서드는 exc_info라는 키워드만 신경 쓰고 이것을 사용해 예외 정보를 로깅할지 결정합니다.
  • Logger.exception()Logger.error()와 비슷한 로그 메시지를 만듭니다. 차이는 Logger.exception()이 스택 추적을 함께 덤프한다는 점입니다. 이 메서드는 예외 처리기에서만 호출하세요.
  • Logger.log()는 로그 레벨을 명시적 인자로 받습니다. 이것은 위에 나열한 레벨 편의 메서드를 사용하는 것보다 로그 메시지에 대해 조금 더 장황하지만, 커스텀 로그 레벨로 로깅하는 방법입니다.

getLogger()는 지정된 이름의 logger 인스턴스에 대한 참조를 돌려주고, 이름이 제공되지 않으면 root를 돌려줍니다. 이름은 점으로 구분된 계층 구조입니다. 같은 이름으로 getLogger()를 여러 번 호출하면 같은 logger 객체에 대한 참조를 돌려줍니다. 계층 목록에서 더 아래에 있는 logger는 더 위에 있는 logger의 자식입니다. 예를 들어 foo라는 이름의 logger가 주어졌을 때, foo.bar, foo.bar.baz, foo.bam이라는 이름의 logger는 모두 foo의 후손입니다.

logger는 *유효 레벨(effective level)*이라는 개념을 가집니다. logger에 레벨이 명시적으로 설정되지 않으면 부모의 레벨이 유효 레벨로 대신 사용됩니다. 부모에 명시적 레벨이 설정되어 있지 않으면 부모를 살펴보는 식으로, 명시적으로 설정된 레벨을 찾을 때까지 모든 조상을 검색합니다. 루트 logger는 항상 명시적 레벨(WARNING 기본)을 가집니다. 이벤트를 처리할지 결정할 때 logger의 유효 레벨이 이벤트를 logger의 handler에 전달할지 결정하는 데 사용됩니다.

자식 logger는 조상 logger에 연결된 handler까지 메시지를 전파(propagate)합니다. 이 때문에 애플리케이션이 사용하는 모든 logger에 대해 handler를 정의하고 구성할 필요가 없습니다. 최상위 logger에 대한 handler를 구성하고 필요에 따라 자식 logger를 만드는 것으로 충분해요.(다만 logger의 propagate 속성을 False로 설정하면 전파를 끌 수 있습니다.)

Handler

Handler 객체는 (로그 메시지의 심각도에 기반해) 적절한 로그 메시지를 handler의 지정된 목적지로 보내는 책임이 있습니다. Logger 객체는 addHandler() 메서드로 자신에게 handler 객체를 0개 이상 추가할 수 있어요. 예시 시나리오로, 애플리케이션이 모든 로그 메시지를 로그 파일로, error 이상의 모든 메시지를 stdout으로, 모든 critical 메시지를 이메일 주소로 보내고 싶을 수 있습니다. 이 시나리오는 세 개의 개별 handler가 필요한데, 각 handler는 특정 심각도의 메시지를 특정 위치로 보내는 책임을 집니다.

표준 라이브러리는 꽤 많은 handler 타입을 포함하고 있습니다(Useful Handlers 참조). 튜토리얼의 예시에서는 주로 StreamHandlerFileHandler를 사용합니다.

애플리케이션 개발자가 신경 써야 할 handler 메서드는 아주 적습니다. 내장 handler 객체(즉, 커스텀 handler를 만들지 않는)를 사용하는 애플리케이션 개발자에게 관련 있는 것처럼 보이는 유일한 handler 메서드는 다음 구성 메서드들입니다.

  • setLevel() 메서드는 logger 객체에서처럼, 적절한 목적지로 보내질 가장 낮은 심각도를 지정합니다. setLevel() 메서드가 두 개인 이유는 무엇일까요? logger에 설정된 레벨은 logger가 자신의 handler로 넘길 심각도의 메시지가 무엇인지 결정합니다. 각 handler에 설정된 레벨은 그 handler가 어느 메시지를 보낼 것인지 결정해요.
  • setFormatter()는 이 handler가 사용할 Formatter 객체를 선택합니다.
  • addFilter()removeFilter()는 handler에서 filter 객체를 각각 구성하고 구성 해제합니다.

애플리케이션 코드는 Handler의 인스턴스를 직접 인스턴스화하고 사용해서는 안 됩니다. 대신 Handler 클래스는 모든 handler가 가져야 할 인터페이스를 정의하고 자식 클래스가 사용(또는 덮어쓸)할 수 있는 몇 가지 기본 동작을 확립하는 기반 클래스입니다.

Formatter

Formatter 객체는 로그 메시지의 최종 순서, 구조, 내용을 구성합니다. 기반 logging.Handler 클래스와 달리 애플리케이션 코드는 formatter 클래스를 인스턴스화할 수 있으며, 애플리케이션에 특별한 동작이 필요하다면 formatter를 서브클래싱할 수도 있습니다. 생성자는 메시지 포맷 문자열, 날짜 포맷 문자열, 스타일 표시자라는 세 가지 선택적 인자를 받아요.

logging.Formatter.__init__(*fmt=None*, *datefmt=None*, *style='%'*)

메시지 포맷 문자열이 없으면 기본은 원시 메시지를 사용하는 것입니다. 날짜 포맷 문자열이 없으면 기본 날짜 형식은:

%Y-%m-%d %H:%M:%S

인데 끝에 밀리초가 붙습니다. style'%', '{', '$' 중 하나입니다. 이 중 하나가 지정되지 않으면 '%'가 사용됩니다.

style'%'이면 메시지 포맷 문자열은 %(<dictionary key>)s 스타일 문자열 치환을 사용합니다. 가능한 키는 LogRecord 속성에 문서화되어 있어요. style'{'이면 메시지 포맷 문자열이 (키워드 인자를 사용하는) str.format()과 호환된다고 가정하고, style'$'이면 메시지 포맷 문자열은 string.Template.substitute()가 기대하는 것을 따라야 합니다.

버전 3.2에서 변경: style 매개변수가 추가되었습니다.

다음 메시지 포맷 문자열은 사람이 읽을 수 있는 형식의 시간, 메시지의 심각도, 메시지의 내용을 그 순서대로 기록합니다.

'%(asctime)s - %(levelname)s - %(message)s'

Formatter는 사용자 구성 가능한 함수를 사용해 레코드의 생성 시간을 튜플로 변환합니다. 기본적으로 time.localtime()이 사용됩니다. 특정 formatter 인스턴스에 대해 이것을 바꾸려면 그 인스턴스의 converter 속성을 time.localtime()이나 time.gmtime()과 같은 시그니처의 함수로 설정하세요. 모든 formatter에 대해 바꾸려면(예를 들어 모든 로깅 시간을 GMT로 표시하고 싶다면) Formatter 클래스의 converter 속성을 (time.gmtime으로, GMT 표시를 위해) 설정하면 됩니다.

로깅 구성하기 (Configuring Logging)

프로그래머는 세 가지 방식으로 로깅을 구성할 수 있습니다.

  • 위에 나열한 구성 메서드를 호출하는 파이썬 코드를 사용해 logger, handler, formatter를 명시적으로 만드는 것.
  • 로깅 구성 파일을 만들고 fileConfig() 함수로 읽는 것.
  • 구성 정보의 딕셔너리를 만들어 dictConfig() 함수에 전달하는 것.

마지막 두 옵션에 대한 참조 문서는 Configuration functions를 보세요. 다음 예시는 파이썬 코드로 아주 간단한 logger, 콘솔 handler, 간단한 formatter를 구성합니다.

import logging

# create logger
logger = logging.getLogger('simple_example')
logger.setLevel(logging.DEBUG)

# create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

# create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# add formatter to ch
ch.setFormatter(formatter)

# add ch to logger
logger.addHandler(ch)

# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

이 모듈을 명령줄에서 실행하면 다음 출력이 생깁니다.

$ python simple_logging_module.py
2005-03-19 15:10:26,618 - simple_example - DEBUG - debug message
2005-03-19 15:10:26,620 - simple_example - INFO - info message
2005-03-19 15:10:26,695 - simple_example - WARNING - warn message
2005-03-19 15:10:26,697 - simple_example - ERROR - error message
2005-03-19 15:10:26,773 - simple_example - CRITICAL - critical message

다음 파이썬 모듈은 위 예시의 것과 거의 동일한 logger, handler, formatter를 만드는데, 유일한 차이는 객체의 이름뿐입니다.

import logging
import logging.config

logging.config.fileConfig('logging.conf')

# create logger
logger = logging.getLogger('simpleExample')

# 'application' code
logger.debug('debug message')
logger.info('info message')
logger.warning('warn message')
logger.error('error message')
logger.critical('critical message')

다음은 logging.conf 파일입니다.

[loggers]
keys=root,simpleExample

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s

출력은 config 파일에 기반하지 않은 예시의 것과 거의 동일합니다.

$ python simple_logging_config.py
2005-03-19 15:38:55,977 - simpleExample - DEBUG - debug message
2005-03-19 15:38:55,979 - simpleExample - INFO - info message
2005-03-19 15:38:56,054 - simpleExample - WARNING - warn message
2005-03-19 15:38:56,055 - simpleExample - ERROR - error message
2005-03-19 15:38:56,130 - simpleExample - CRITICAL - critical message

config 파일 방식이 파이썬 코드 방식에 비해 몇 가지 이점, 주로 구성과 코드의 분리, 그리고 비프로그래머도 로깅 속성을 쉽게 수정할 수 있다는 점을 가진다는 것을 알 수 있어요.

경고

fileConfig() 함수는 기본 매개변수 disable_existing_loggers를 받는데, 하위 호환성 이유로 기본값이 True입니다. 이것이 당신이 원하는 것일 수도 아닐 수도 있는데, fileConfig() 호출 전에 존재했던 루트가 아닌 logger들이 구성에 (그들 또는 조상이) 명시적으로 이름이 언급되지 않으면 비활성화되게 만들기 때문입니다. 참조 문서에서 자세한 내용을 확인하고, 원한다면 이 매개변수에 False를 지정하세요.

dictConfig()에 전달되는 딕셔너리도 키 disable_existing_loggers로 불리언 값을 지정할 수 있는데, 딕셔너리에서 명시적으로 지정되지 않으면 기본적으로 True로 해석됩니다. 이것은 위에서 설명한 logger-비활성화 동작을 초래하는데, 당신이 원하는 것이 아닐 수 있어요. 그렇다면 키를 False 값으로 명시적으로 제공하세요.

config 파일에서 참조되는 클래스 이름은 logging 모듈에 상대적이거나, 일반 import 메커니즘으로 해석될 수 있는 절대 값이어야 합니다. 따라서 WatchedFileHandler(logging 모듈에 상대적) 또는 mypackage.mymodule.MyHandler(mypackage가 파이썬 import 경로에 있는, 패키지 mypackage와 모듈 mymodule에 정의된 클래스) 중 어느 것이든 쓸 수 있어요.

파이썬 3.2에서 로깅을 구성하는 새 수단이 도입되었는데, 구성 정보를 담는 데 딕셔너리를 사용합니다. 이것은 위에 설명한 config-file 기반 접근의 기능의 상위 집합을 제공하며, 새 애플리케이션과 배포에 권장되는 구성 방법입니다. 구성 정보를 담는 데 파이썬 딕셔너리가 사용되고, 그 딕셔너리를 다양한 수단으로 채울 수 있으므로 구성을 위한 더 많은 옵션이 있습니다. 예를 들어 JSON 형식의 구성 파일, 또는 YAML 처리 기능에 접근할 수 있다면 YAML 형식의 파일을 사용해 구성 딕셔너리를 채울 수 있어요. 물론 파이썬 코드에서 딕셔너리를 직접 구성하거나, 소켓을 통해 피클된 형태로 받거나, 애플리케이션에 맞는 어떤 방식을 사용해도 됩니다.

다음은 위와 같은 구성을 새 딕셔너리 기반 접근을 위해 YAML 형식으로 나타낸 예시입니다.

version: 1
formatters:
  simple:
    format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
handlers:
  console:
    class: logging.StreamHandler
    level: DEBUG
    formatter: simple
    stream: ext://sys.stdout
loggers:
  simpleExample:
    level: DEBUG
    handlers: [console]
    propagate: no
root:
  level: DEBUG
  handlers: [console]

딕셔너리를 사용한 로깅에 대한 더 많은 정보는 Configuration functions를 보세요.

구성이 제공되지 않으면 어떻게 되나 (What happens if no configuration is provided)

로깅 구성이 제공되지 않으면, 로깅 이벤트를 출력해야 하는데 그 이벤트를 출력할 handler를 찾을 수 없는 상황이 생길 수 있습니다.

이벤트는 lastResort에 저장된 '마지막 수단의 handler(handler of last resort)'를 사용해 출력됩니다. 이 내부 handler는 어떤 logger와도 연관되지 않으며, 이벤트 설명 메시지를 현재 sys.stderr의 값에 쓰는 StreamHandler처럼 동작합니다(따라서 적용 중인 어떤 리다이렉션도 존중합니다). 메시지에 대해 아무 포매팅도 수행되지 않으며, 그냥 맨 이벤트 설명 메시지만 출력됩니다. handler의 레벨은 WARNING으로 설정되어, 이 심각도 이상의 모든 이벤트가 출력됩니다.

버전 3.2에서 변경: 3.2 이전 파이썬 버전의 동작은 다음과 같습니다.

  • raiseExceptionsFalse(프로덕션 모드)이면 이벤트는 조용히 버려집니다.
  • raiseExceptionsTrue(개발 모드)이면 'No handlers could be found for logger X.Y.Z' 메시지가 한 번 출력됩니다.

3.2 이전 동작을 얻으려면 lastResortNone으로 설정할 수 있습니다.

라이브러리를 위한 로깅 구성 (Configuring Logging for a Library)

로깅을 사용하는 라이브러리를 개발할 때는 라이브러리가 로깅을 어떻게 사용하는지(예: 사용하는 logger의 이름)를 문서화하는 데 주의를 기울여야 합니다. 또한 그 로깅 구성에도 일부 고려가 필요합니다. 사용 애플리케이션이 로깅을 사용하지 않고 라이브러리 코드가 로깅 호출을 만들면, (이전 절에서 설명한 대로) WARNING 이상 심각도의 이벤트가 sys.stderr에 출력됩니다. 이것이 최상의 기본 동작으로 간주됩니다.

어떤 이유로 로깅 구성이 없는 상태에서 이런 메시지가 출력되기를 원하지 않는다면, 라이브러리의 최상위 logger에 아무것도 하지 않는(do-nothing) handler를 붙일 수 있습니다. 이렇게 하면 라이브러리의 이벤트에 대해 handler가 항상 발견되므로(그저 아무 출력도 만들지 않을 뿐) 메시지가 출력되는 것이 방지됩니다. 라이브러리 사용자가 애플리케이션용 로깅을 구성한다면, 아마 그 구성이 handler 몇 개를 추가할 것이고, 레벨이 적절히 구성되어 있으면 라이브러리 코드에서 만든 로깅 호출이 평소처럼 그 handler들로 출력을 보낼 거예요.

아무것도 하지 않는 handler가 logging 패키지에 포함되어 있습니다. 바로 NullHandler(파이썬 3.1부터)입니다. 이 handler의 인스턴스를 라이브러리가 사용하는 로깅 네임스페이스의 최상위 logger에 추가할 수 있습니다(로깅 구성이 없는 상태에서 라이브러리의 로깅 이벤트가 sys.stderr로 출력되는 것을 막고 싶다면). 라이브러리 foo의 모든 로깅이 'foo.x', 'foo.x.y' 등과 일치하는 이름의 logger로 행해진다면, 다음 코드:

import logging
logging.getLogger('foo').addHandler(logging.NullHandler())

가 원하는 효과를 가져야 합니다. 조직이 여러 라이브러리를 만든다면 지정되는 logger 이름은 그냥 'foo'가 아니라 'orgname.foo'가 될 수 있습니다.

참고

라이브러리에서 루트 logger로 로깅하지 않을 것을 강력히 권장합니다. 대신 라이브러리의 최상위 패키지나 모듈의 __name__ 같은, 고유하고 쉽게 식별되는 이름을 가진 logger를 사용하세요. 루트 logger로 로깅하면 애플리케이션 개발자가 원하는 대로 라이브러리의 로깅 상세도나 handler를 구성하기 어렵거나 불가능해집니다.

참고

NullHandler 외의 어떤 handler도 라이브러리의 logger에 추가하지 않을 것을 강력히 권장합니다. handler의 구성은 라이브러리를 사용하는 애플리케이션 개발자의 권한이기 때문이에요. 애플리케이션 개발자는 자신의 대상 고객과 자신의 애플리케이션에 가장 적합한 handler가 무엇인지 알고 있습니다. 당신이 '뒤에서' handler를 추가하면 그들이 단위 테스트를 수행하고 자신의 요구에 맞는 로그를 제공하는 능력을 방해할 수 있습니다.

로깅 레벨 (Logging Levels)

로깅 레벨의 숫자 값은 다음 표에 나와 있습니다. 이것은 주로 자신만의 레벨을 정의하고 싶고, 그 레벨이 사전 정의된 레벨에 상대적인 특정 값을 가져야 할 때 관심이 있습니다. 같은 숫자 값의 레벨을 정의하면 사전 정의된 값을 덮어쓰고, 사전 정의된 이름은 사라집니다.

레벨 숫자 값
CRITICAL 50
ERROR 40
WARNING 30
INFO 20
DEBUG 10
NOTSET 0

레벨은 logger에 연관될 수도 있는데, 개발자가 설정하거나 저장된 로깅 구성을 불러옴으로써 설정됩니다. logger에 로깅 메서드를 호출하면 logger는 자신의 레벨과 메서드 호출에 연관된 레벨을 비교합니다. logger의 레벨이 메서드 호출의 것보다 높으면 실제로 로그 메시지가 생성되지 않습니다. 이것이 로깅 출력의 상세도를 제어하는 기본 메커니즘입니다.

로그 메시지는 LogRecord 클래스의 인스턴스로 인코딩됩니다. logger가 실제로 이벤트를 기록하기로 결정하면 로그 메시지에서 LogRecord 인스턴스가 만들어집니다.

로그 메시지는 handler를 통해 전달 메커니즘을 거치는데, handler는 Handler 클래스의 서브클래스의 인스턴스입니다. Handler는 로그 메시지(LogRecord 형태)가 그 메시지의 대상 고객(최종 사용자, 지원 데스크 직원, 시스템 관리자, 개발자 같은)에게 유용한 특정 위치(또는 위치 집합)에 도달하도록 보장하는 책임을 집니다. Handler에는 특정 목적지에 대한 LogRecord 인스턴스가 전달됩니다. 각 logger는 (LoggeraddHandler() 메서드를 통해) 0개, 1개 또는 그 이상의 handler와 연관될 수 있어요. logger에 직접 연관된 어떤 handler에 더해, logger의 모든 조상에 연관된 모든 handler가 메시지를 전달하도록 호출됩니다(logger의 propagate 플래그가 거짓 값으로 설정되어 조상 handler로의 전달이 멈추는 지점까지).

logger처럼 handler도 레벨을 연관시킬 수 있습니다. handler의 레벨은 logger의 레벨과 같은 방식으로 필터 역할을 합니다. handler가 실제로 이벤트를 전달하기로 결정하면 emit() 메서드가 메시지를 그 목적지로 보내는 데 사용됩니다. 대부분의 사용자 정의 Handler 서브클래스는 이 emit()을 덮어써야 합니다.

커스텀 레벨 (Custom Levels)

자신만의 레벨을 정의하는 것은 가능하지만, 기존 레벨들이 실용적인 경험에 기반해 선택되었으므로 보통 필요하지는 않아야 합니다. 다만 커스텀 레벨이 필요하다고 확신한다면, 이때 큰 주의를 기울여야 하고, 라이브러리를 개발 중이라면 커스텀 레벨을 정의하는 것은 아주 나쁜 생각일 수 있습니다. 여러 라이브러리 저자가 모두 자신의 커스텀 레벨을 정의하면, 함께 사용되는 여러 라이브러리의 로깅 출력을 사용 개발자가 제어하거나 해석하기 어려워질 수 있는데, 주어진 숫자 값이 라이브러리마다 다른 의미를 가질 수 있기 때문입니다.

유용한 Handler (Useful Handlers)

기반 Handler 클래스에 더해 많은 유용한 서브클래스가 제공됩니다.

  • StreamHandler 인스턴스는 스트림(파일처럼 생긴 객체)으로 메시지를 보냅니다.
  • FileHandler 인스턴스는 디스크 파일로 메시지를 보냅니다.
  • BaseRotatingHandler는 어떤 지점에서 로그 파일을 회전(rotate)시키는 handler들의 기반 클래스입니다. 직접 인스턴스화하도록 의도되지 않았어요. 대신 RotatingFileHandlerTimedRotatingFileHandler를 사용하세요.
  • RotatingFileHandler 인스턴스는 최대 로그 파일 크기와 로그 파일 회전을 지원하며 디스크 파일로 메시지를 보냅니다.
  • TimedRotatingFileHandler 인스턴스는 일정한 시간 간격으로 로그 파일을 회전시키며 디스크 파일로 메시지를 보냅니다.
  • SocketHandler 인스턴스는 TCP/IP 소켓으로 메시지를 보냅니다. 3.4부터 Unix 도메인 소켓도 지원됩니다.
  • DatagramHandler 인스턴스는 UDP 소켓으로 메시지를 보냅니다. 3.4부터 Unix 도메인 소켓도 지원됩니다.
  • SMTPHandler 인스턴스는 지정된 이메일 주소로 메시지를 보냅니다.
  • SysLogHandler 인스턴스는 아마 원격 머신의 Unix syslog 데몬으로 메시지를 보냅니다.
  • NTEventLogHandler 인스턴스는 Windows NT/2000/XP 이벤트 로그로 메시지를 보냅니다.
  • MemoryHandler 인스턴스는 메모리의 버퍼로 메시지를 보내며, 특정 기준이 충족될 때마다 비워집니다(flush).
  • HTTPHandler 인스턴스는 GET 또는 POST 의미론을 사용해 HTTP 서버로 메시지를 보냅니다.
  • WatchedFileHandler 인스턴스는 로깅하고 있는 파일을 지켜봅니다. 파일이 바뀌면 파일 이름을 사용해 닫고 다시 엽니다. 이 handler는 Unix 계열 시스템에서만 유용합니다. Windows는 사용되는 기반 메커니즘을 지원하지 않아요.
  • QueueHandler 인스턴스는 queue 또는 multiprocessing 모듈에 구현된 것 같은 큐로 메시지를 보냅니다.
  • NullHandler 인스턴스는 오류 메시지로 아무것도 하지 않습니다. 로깅을 사용하고 싶지만, 라이브러리 사용자가 로깅을 구성하지 않았을 때 표시될 수 있는 'No handlers could be found for logger XXX' 메시지를 피하고 싶은 라이브러리 개발자가 사용합니다. 자세한 내용은 "라이브러리를 위한 로깅 구성"을 보세요.

버전 3.1에서 추가: NullHandler 클래스.

버전 3.2에서 추가: QueueHandler 클래스.

NullHandler, StreamHandler, FileHandler 클래스는 핵심 로깅 패키지에 정의되어 있습니다. 다른 handler들은 하위 모듈 logging.handlers에 정의되어 있어요.(구성 기능을 위한 또 다른 하위 모듈 logging.config도 있습니다.)

로그 메시지는 Formatter 클래스의 인스턴스를 통해 표시용으로 형식화됩니다. 이들은 % 연산자와 딕셔너리와 함께 사용하기에 적합한 포맷 문자열로 초기화됩니다.

여러 메시지를 일괄(batch)로 형식화하려면 BufferingFormatter의 인스턴스를 사용할 수 있습니다. (배치의 각 메시지에 적용되는) 포맷 문자열에 더해, 헤더와 트레일러 포맷 문자열을 위한 장치가 있습니다.

logger 레벨 및/또는 handler 레벨에 기반한 필터링으로 충분하지 않을 때, Filter의 인스턴스를 LoggerHandler 인스턴스 둘 다에 (그들의 addFilter() 메서드를 통해) 추가할 수 있습니다. 메시지를 더 처리할지 결정하기 전에 logger와 handler 둘 다 모든 필터에 허가를 묻습니다. 어떤 필터가 거짓 값을 돌려주면 메시지는 더 처리되지 않습니다.

기본 Filter 기능은 특정 logger 이름에 의한 필터링을 허용합니다. 이 기능을 사용하면, 이름이 지정된 logger와 그 자식에게 보내진 메시지는 필터를 통과하고 다른 모든 것들은 버려집니다.

로깅 중 발생하는 예외 (Exceptions raised during logging)

logging 패키지는 프로덕션에서 로깅하는 동안 발생하는 예외를 삼키도록(swallow) 설계되었습니다. 이것은 로깅 이벤트를 처리하는 동안 발생하는 오류(로깅 오구성, 네트워크 또는 그와 유사한 오류 같은)가 로깅을 사용하는 애플리케이션을 조기에 종료시키지 않게 하기 위해서입니다.

SystemExitKeyboardInterrupt 예외는 절대 삼켜지지 않습니다. Handler 서브클래스의 emit() 메서드 중에 발생하는 다른 예외들은 그 handleError() 메서드로 전달됩니다.

HandlerhandleError()의 기본 구현은 모듈 레벨 변수 raiseExceptions가 설정되어 있는지 확인합니다. 설정되어 있으면 sys.stderr에 스택 추적이 출력됩니다. 설정되어 있지 않으면 예외는 삼켜집니다.

참고

raiseExceptions의 기본값은 True입니다. 개발 중에는 발생하는 어떤 예외도 통보받고 싶을 것이기 때문이죠. 프로덕션 사용에서는 raiseExceptionsFalse로 설정할 것을 권합니다.

메시지로 임의의 객체 사용하기 (Using arbitrary objects as messages)

앞의 절과 예시들에서 로깅 이벤트 시 전달되는 메시지가 문자열이라고 가정했습니다. 하지만 이것이 유일한 가능성은 아니에요. 임의의 객체를 메시지로 전달할 수 있고, 로깅 시스템이 그것을 문자열 표현으로 변환해야 할 때 그 __str__() 메서드가 호출됩니다. 실제로 원한다면 문자열 표현 계산을 아예 피할 수도 있는데, 예를 들어 SocketHandler는 이벤트를 피클해서 와이어로 보내는 방식으로 이벤트를 내보냅니다.

최적화 (Optimization)

메시지 인자의 형식화는 피할 수 없을 때까지 지연됩니다. 다만 로깅 메서드에 전달되는 인자를 계산하는 것도 비용이 들 수 있고, logger가 그냥 이벤트를 버릴 것이라면 그것을 피하고 싶을 수 있어요. 무엇을 할지 결정하려면 isEnabledFor() 메서드를 호출할 수 있는데, 이는 레벨 인자를 받아 그 레벨의 호출에 대해 Logger가 이벤트를 만들 것이라면 참을 돌려줍니다. 다음과 같은 코드를 쓸 수 있어요.

if logger.isEnabledFor(logging.DEBUG):
    logger.debug('Message with %s, %s', expensive_func1(),
                 expensive_func2())

이렇게 하면 logger의 임계값이 DEBUG 위로 설정되어 있을 때 expensive_func1expensive_func2에 대한 호출이 절대 이루어지지 않습니다.

참고

어떤 경우에는 isEnabledFor() 자체가 원하는 것보다 더 비쌀 수 있습니다(예를 들어 명시적 레벨이 logger 계층의 높은 곳에만 설정된 깊게 중첩된 logger의 경우). 그런 경우(또는 단단한 루프에서 메서드를 호출하는 것을 피하고 싶다면) isEnabledFor() 호출의 결과를 지역이나 인스턴스 변수에 캐시하고, 매번 메서드를 호출하는 대신 그것을 사용하세요. 그런 캐시된 값은 애플리케이션이 실행되는 동안 로깅 구성이 동적으로 바뀔 때(그렇게 흔하지는 않음)만 다시 계산하면 됩니다.

수집되는 로깅 정보에 대해 더 정밀한 제어가 필요한 특정 애플리케이션을 위해 할 수 있는 다른 최적화도 있습니다. 로깅 중 원하지 않는 처리를 피하기 위해 할 수 있는 것들의 목록입니다.

원하지 않는 수집 수집을 피하는 방법
호출이 이루어진 위치에 대한 정보. logging._srcfileNone으로 설정. 이는 sys._getframe() 호출을 피하는데, PyPy 같은 환경에서 코드 속도를 높이는 데 도움이 될 수 있습니다(첫째, sys._getframe()을 사용하는 코드는 가속할 수 없기 때문).
스레딩 정보. logging.logThreadsFalse로 설정.
현재 프로세스 ID(os.getpid()) logging.logProcessesFalse로 설정.
multiprocessing으로 여러 프로세스를 관리할 때의 현재 프로세스 이름. logging.logMultiprocessingFalse로 설정.
asyncio 사용 시 현재 asyncio.Task 이름. logging.logAsyncioTasksFalse로 설정.

또한 핵심 로깅 모듈은 기본 handler만 포함한다는 점도 기억하세요. logging.handlerslogging.config를 import하지 않으면 그것들은 메모리를 차지하지 않아요.

그 밖의 자료 (Other resources)

  • logging 모듈 — logging 모듈의 API 참조.
  • logging.config 모듈 — logging 모듈의 구성 API.
  • logging.handlers 모듈 — logging 모듈에 포함된 유용한 handler들.
  • Logging Cookbook — 여러 실용적인 로깅 예시 레시피.

더 알아보기 (Learn more)