PEP 282 – 로깅 시스템
- Author:
- Vinay Sajip <vinay_sajip at red-dove.com>, Trent Mick <trentm at activestate.com>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 04-Feb-2002
- Python-Version:
- 2.3
- Post-History:
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 파이썬 표준 라이브러리에 제안하는 로깅 패키지를 설명합니다.
기본적으로 이 시스템은 사용자가 하나 이상의 로거 객체를 생성하고, 이 객체에 메서드를 호출하여 디버깅 노트, 일반 정보, 경고, 오류 등을 기록하는 방식으로 동작합니다. 서로 다른 로깅 ‘레벨’을 사용하여 중요한 메시지와 덜 중요한 메시지를 구분할 수 있습니다.
이름이 부여된 싱글턴 로거 객체의 레지스트리가 유지되므로
- 서로 다른 논리적 로깅 스트림(또는 ‘채널’)이 존재할 수 있고(예를 들어 ‘zope.zodb’ 관련 항목 하나와 ‘mywebsite’ 전용 항목 하나)
- 로거 객체 참조를 여기저기 전달할 필요가 없습니다.
이 시스템은 런타임에 설정할 수 있습니다. 이 설정 메커니즘을 통해 애플리케이션 자체는 건드리지 않은 채 로깅의 레벨과 유형을 조정할 수 있습니다.
동기
단일한 로깅 메커니즘이 표준 라이브러리에 자리 잡으면, 1) 로깅이 ‘제대로’ 이루어질 가능성이 높아지고, 2) 여러 라이브러리를 상당히 일관되게 로깅할 수 있는 더 큰 애플리케이션으로 통합할 수 있게 됩니다.
영향을 받은 것들
이 제안은 다음 로깅 패키지들을 검토한 후 작성되었습니다:
간단한 예제
이는 로깅 패키지를 사용하여 stderr에 간단한 로깅 출력을 생성하는 방법을 보여주는 매우 간단한 예제입니다.
--------- mymodule.py -------------------------------
import logging
log = logging.getLogger("MyModule")
def doIt():
log.debug("Doin' stuff...")
#do stuff...
raise TypeError, "Bogus type error for testing"
-----------------------------------------------------
--------- myapp.py ----------------------------------
import mymodule, logging
logging.basicConfig()
log = logging.getLogger("MyApp")
log.info("Starting my app")
try:
mymodule.doIt()
except Exception, e:
log.exception("There was a problem.")
log.info("Ending my app")
-----------------------------------------------------
$ python myapp.py
INFO:MyApp: Starting my app
DEBUG:MyModule: Doin' stuff...
ERROR:MyApp: There was a problem.
Traceback (most recent call last):
File "myapp.py", line 9, in ?
mymodule.doIt()
File "mymodule.py", line 7, in doIt
raise TypeError, "Bogus type error for testing"
TypeError: Bogus type error for testing
INFO:MyApp: Ending my app
위 예제는 기본 출력 형식을 보여줍니다. 출력 형식의 모든 측면은 설정 가능해야 하므로, 다음과 같이 출력 형식을 지정할 수도 있습니다:
2002-04-19 07:56:58,174 MyModule DEBUG - Doin' stuff...
or just
Doin' stuff...
제어 흐름
애플리케이션은 Logger객체에 대해 로깅 호출을 수행합니다. 로거는 계층적 네임스페이스로 조직되며, 자식 로거는 네임스페이스 상의 부모로부터 일부 로깅 속성을 상속받습니다.
로거 이름은 점(마침표)이 하위 네임스페이스를 나타내는 “점으로 구분된 이름” 네임스페이스에 속합니다. 따라서 로거 객체의 네임스페이스는 단일 트리 데이터 구조에 대응됩니다.
""는 네임스페이스의 루트입니다"Zope"는 루트의 자식 노드가 됩니다"Zope.ZODB"는"Zope"의 자식 노드가 됩니다
이 Logger 객체들은 출력을 위해 Handler 객체로 전달되는 LogRecord 객체를 생성합니다. Logger와 Handler 모두 특정 LogRecord에 관심이 있는지 결정하기 위해 로깅 레벨과 (선택적으로) Filter를 사용할 수 있습니다. LogRecord를 외부로 출력해야 할 때, Handler는 메시지를 I/O 스트림으로 보내기 전에 (선택적으로) Formatter를 사용해 지역화하고 포맷할 수 있습니다.
각 Logger는 출력 Handler의 집합을 추적합니다. 기본적으로 모든 Logger는 자신의 조상 Logger들의 모든 Handler로도 출력을 보냅니다. 그러나 Logger는 트리의 상위에 있는 Handler를 무시하도록 구성될 수도 있습니다.
이 API들은 로깅이 비활성화되었을 때 Logger API 호출이 저렴하게 이루어지도록 구성되어 있습니다. 특정 로그 레벨에 대해 로깅이 비활성화되어 있다면, Logger는 저렴한 비교 테스트를 수행하고 반환할 수 있습니다. 특정 로그 레벨에 대해 로깅이 활성화되어 있다면, Logger는 LogRecord를 Handler에 전달하기 전에도 비용을 최소화하도록 신경 씁니다. 특히 (상대적으로 비용이 많이 드는) 지역화와 포맷은 Handler가 요청할 때까지 지연됩니다.
전체 Logger 계층에도 레벨을 연결할 수 있으며, 이는 개별 Logger의 레벨보다 우선합니다. 이는 모듈 수준 함수를 통해 이루어집니다:
def disable(lvl):
"""
Do not generate any LogRecords for requests with a severity less
than 'lvl'.
"""
...
레벨
중요도가 증가하는 순서로 나열한 로깅 레벨은 다음과 같습니다:
- DEBUG
- INFO
- WARN
- ERROR
- CRITICAL
CRITICAL이라는 용어는 log4j에서 사용하는 FATAL 대신 사용됩니다. 이 레벨들은 개념적으로 동일하며, 심각한 오류 또는 매우 심각한 오류를 나타냅니다. 그러나 FATAL은 죽음을 암시하며, 이는 Python에서 발생하고 처리되지 않은 예외, 트레이스백, 종료를 의미합니다. logging 모듈은 FATAL 레벨의 로그 항목으로부터 그러한 결과를 강제하지 않으므로, FATAL 대신 CRITICAL을 사용하는 것이 타당합니다.
이들은 중요도의 단순 비교를 가능하게 하기 위한 단순한 정수 상수일 뿐입니다. 경험적으로 레벨이 너무 많으면 특정 로그 요청에 어떤 레벨을 적용해야 하는지에 대한 주관적 해석으로 이어져 혼란을 줄 수 있는 것으로 나타났습니다.
위의 레벨들이 강력히 권장되지만, 로깅 시스템이 이를 규정해서는 안 됩니다. 사용자는 자신만의 레벨은 물론 어떤 레벨에 대한 텍스트 표현도 정의할 수 있습니다. 그러나 사용자 정의 레벨은 모두 양의 정수여야 하며 심각도가 증가하는 순서로 증가해야 한다는 제약을 따라야 합니다.
사용자 정의 로깅 레벨은 두 개의 모듈 수준 함수를 통해 지원됩니다:
def getLevelName(lvl):
"""Return the text for level 'lvl'."""
...
def addLevelName(lvl, lvlName):
"""
Add the level 'lvl' with associated text 'levelName', or
set the textual representation of existing level 'lvl' to be
'lvlName'."""
...
Logger
각 Logger 객체는 관심 있는 로그 레벨(또는 임계값)을 추적하며, 해당 레벨 미만의 로그 요청은 폐기합니다.
Manager 클래스 인스턴스는 이름이 지정된 Logger 객체들의 계층적 네임스페이스를 유지합니다. 세대는 점으로 구분된 이름으로 표시됩니다: Logger “foo”는 Logger “foo.bar”와 “foo.baz”의 부모입니다.
Manager 클래스 인스턴스는 싱글턴이며 사용자에게 직접 노출되지 않고, 사용자는 다양한 모듈 수준 함수를 사용하여 이와 상호작용합니다.
일반적인 로깅 메서드는 다음과 같습니다:
class Logger:
def log(self, lvl, msg, *args, **kwargs):
"""Log 'str(msg) % args' at logging level 'lvl'."""
...
그러나 각 로깅 수준에 대해 편의 함수가 정의되어 있습니다:
class Logger:
def debug(self, msg, *args, **kwargs): ...
def info(self, msg, *args, **kwargs): ...
def warn(self, msg, *args, **kwargs): ...
def error(self, msg, *args, **kwargs): ...
def critical(self, msg, *args, **kwargs): ...
현재는 “exc_info”라는 하나의 키워드 인자만 인식됩니다. 참이면 호출자는 로깅 출력에 예외 정보가 포함되기를 원하는 것입니다. 이 메커니즘은 예외 정보가 모든 로깅 수준에서 제공되어야 할 때만 필요합니다. 더 일반적인 경우, 즉 오류가 발생했을 때만, 다시 말해 ERROR 수준에서만 예외 정보를 로그에 추가해야 하는 경우에는 또 다른 편의 메서드가 제공됩니다:
class Logger:
def exception(self, msg, *args): ...
이 메서드는 예외 처리기의 컨텍스트에서만 호출되어야 하며, 로그에 예외 정보를 포함시키고자 할 때 선호되는 방법입니다. 다른 편의 메서드들은 예를 들어 INFO 메시지의 컨텍스트에서 예외 정보를 제공하고 싶은 경우처럼 드문 상황에서만 exc_info와 함께 호출되도록 의도된 것입니다.
위에 표시된 “msg” 인자는 일반적으로 형식 문자열이지만, str(x)가 형식 문자열을 반환하는 어떤 객체 x라도 될 수 있습니다. 예를 들어, 이는 국제화/지역화된 애플리케이션을 위해 로캘별 메시지를 가져오는 객체를 표준 gettext 모듈 등을 사용하여 활용할 수 있게 해줍니다. 개략적인 예시:
class Message:
"""Represents a message"""
def __init__(self, id):
"""Initialize with the message ID"""
def __str__(self):
"""Return an appropriate localized message text"""
...
logger.info(Message("abc"), ...)
로그 메시지를 위한 데이터를 수집하고 서식화하는 것은 비용이 클 수 있으며, 어차피 로거가 그 메시지를 폐기할 것이었다면 낭비입니다. 요청이 로거에 의해 받아들여질지 확인하려면 isEnabledFor() 메서드를 사용할 수 있습니다:
class Logger:
def isEnabledFor(self, lvl):
"""
Return true if requests at level 'lvl' will NOT be
discarded.
"""
...
그러므로 이렇게 비용이 크고 낭비일 수 있는 DOM에서 XML로의 변환 대신:
...
hamletStr = hamletDom.toxml()
log.info(hamletStr)
...
다음과 같이 할 수 있습니다:
if log.isEnabledFor(logging.INFO):
hamletStr = hamletDom.toxml()
log.info(hamletStr)
새 로거가 생성되면 “수준 없음”을 나타내는 수준으로 초기화됩니다. setLevel() 메서드를 사용하여 수준을 명시적으로 설정할 수 있습니다:
class Logger:
def setLevel(self, lvl): ...
로거의 수준이 설정되어 있지 않으면 시스템은 명시적으로 설정된 수준을 찾을 때까지 계층 구조를 따라 올라가며 모든 조상을 참조합니다. 이는 로거의 “유효 수준”으로 간주되며, getEffectiveLevel() 메서드를 통해 조회할 수 있습니다:
def getEffectiveLevel(self): ...
Logger는 결코 직접 인스턴스화되지 않습니다. 대신 모듈 수준 함수가 사용됩니다:
def getLogger(name=None): ...
이름이 지정되지 않으면 루트 로거가 반환됩니다. 그렇지 않고 해당 이름의 로거가 존재하면 그것이 반환됩니다. 그렇지 않으면 새 로거가 초기화되어 반환됩니다. 여기서 “name”은 “channel name”과 동의어입니다.
사용자는 새 로거를 인스턴스화할 때 시스템이 사용할 Logger의 사용자 정의 서브클래스를 지정할 수 있습니다:
def setLoggerClass(klass): ...
전달된 클래스는 Logger의 서브클래스여야 하며, 그 __init__ 메서드는 Logger.__init__을 호출해야 합니다.
핸들러
핸들러는 주어진 LogRecord로 유용한 작업을 수행하는 책임을 집니다. 다음과 같은 핵심 핸들러가 구현될 예정입니다:
StreamHandler: 파일과 유사한 객체에 쓰기 위한 핸들러입니다.FileHandler: 단일 파일 또는 로테이션되는 파일 집합에 쓰기 위한 핸들러입니다.SocketHandler: 원격 TCP 포트에 쓰기 위한 핸들러입니다.DatagramHandler: 저비용 로깅을 위해 UDP 소켓에 쓰기 위한 핸들러입니다. Jeff Bauer는 이미 그런 시스템을 가지고 있었습니다 [5].MemoryHandler: 버퍼가 가득 차거나 특정 조건이 발생할 때까지 로그 레코드를 메모리에 버퍼링하는 핸들러입니다 [1].SMTPHandler: SMTP를 통해 이메일 주소로 전송하기 위한 핸들러입니다.SysLogHandler: UDP를 통해 Unix syslog에 쓰기 위한 핸들러입니다.NTEventLogHandler: Windows NT, 2000, XP의 이벤트 로그에 쓰기 위한 핸들러입니다.HTTPHandler: GET 또는 POST 시맨틱스를 사용하여 웹 서버에 쓰기 위한 핸들러입니다.
핸들러는 setLevel() 메서드를 사용하여 레벨을 설정할 수도 있습니다:
def setLevel(self, lvl): ...
FileHandler는 로테이션되는 로그 파일 집합을 생성하도록 설정할 수 있습니다. 이 경우 생성자에 전달된 파일 이름은 “기본” 파일 이름으로 취급됩니다. 로테이션을 위한 추가 파일 이름은 기본 파일 이름에 .1, .2 등을 붙여서 생성되며, 롤오버가 요청될 때 지정된 최댓값까지 생성됩니다. setRollover 메서드는 로그 파일의 최대 크기와 로테이션 내 백업 파일의 최대 개수를 지정하는 데 사용됩니다.
def setRollover(maxBytes, backupCount): ...
maxBytes가 0으로 지정되면 롤오버는 절대 발생하지 않으며 로그 파일은 무한정 커집니다. 0이 아닌 크기가 지정되면 그 크기를 초과하려는 시점에 롤오버가 발생합니다. 롤오버 메서드는 기본 파일 이름이 항상 가장 최근 것이 되도록 하며, .1은 그다음으로 최근 것, .2는 그다음으로 최근 것이 되는 식으로 보장합니다.
[6]과 함께 제공되는 테스트/예제 스크립트에는 XMLHandler와 SOAPHandler 등 많은 추가 핸들러가 구현되어 있습니다.
LogRecords
LogRecord는 로깅 이벤트에 대한 정보를 담는 그릇 역할을 합니다. 이것은 딕셔너리에 지나지 않지만, 메시지를 선택적 실행 인자와 병합하는 getMessage 메서드를 정의합니다.
Formatters
Formatter는 LogRecord를 문자열 표현으로 변환하는 책임을 집니다. 핸들러는 레코드를 쓰기 전에 자신의 Formatter를 호출할 수 있습니다. 다음과 같은 핵심 Formatter가 구현될 예정입니다:
Formatter: % 연산자를 사용하여 printf와 유사한 포맷팅을 제공합니다.BufferingFormatter: 헤더와 트레일러 포맷팅 지원을 포함하여 여러 메시지에 대한 포맷팅을 제공합니다.
Formatter는 핸들러에서 setFormatter()를 호출하여 핸들러와 연결됩니다:
def setFormatter(self, form): ...
포매터는 % 연산자를 사용하여 로깅 메시지의 형식을 지정합니다. 형식 문자열에는 %(name)x가 포함되어야 하며, LogRecord의 속성 딕셔너리를 사용하여 메시지별 데이터를 얻습니다. 다음 속성들이 제공됩니다:
%(name)s |
로거(로깅 채널)의 이름 |
%(levelno)s |
메시지의 숫자 로깅 레벨(DEBUG, INFO, WARN, ERROR, CRITICAL) |
%(levelname)s |
메시지의 텍스트 로깅 레벨(“DEBUG”, “INFO”, “WARN”, “ERROR”, “CRITICAL”) |
%(pathname)s |
로깅 호출이 발생한 소스 파일의 전체 경로명(가능한 경우) |
%(filename)s |
경로명 중 파일명 부분 |
%(module)s |
로깅 호출이 이루어진 모듈 |
%(lineno)d |
로깅 호출이 발생한 소스 라인 번호(가능한 경우) |
%(created)f |
LogRecord가 생성된 시각(time.time()의 반환값) |
%(asctime)s |
LogRecord가 생성된 시각을 텍스트로 표현한 값 |
%(msecs)d |
생성 시각 중 밀리초 부분 |
%(relativeCreated)d |
logging 모듈이 로드된 시각(일반적으로 애플리케이션 시작 시각)을 기준으로, LogRecord가 생성된 시각까지의 시간(밀리초 단위) |
%(thread)d |
스레드 ID(가능한 경우) |
%(message)s |
레코드가 발행되는 시점에 계산되는 record.getMessage()의 결과 |
포매터가 형식 문자열에 “(asctime)s”가 포함된 것을 확인하면, 생성 시각이 LogRecord의 asctime 속성에 형식화되어 들어갑니다. 날짜 형식화에 유연성을 부여하기 위해, Formatter는 메시지 전체에 대한 형식 문자열과 날짜/시간에 대한 별도의 형식 문자열로 초기화됩니다. 날짜/시간 형식 문자열은 time.strftime 형식이어야 합니다. 메시지 형식의 기본값은 “%(message)s”입니다. 날짜/시간의 기본 형식은 ISO8601입니다.
포매터는 시간을 초 단위에서 튜플로 변환하는 방법을 나타내기 위해 “converter”라는 클래스 속성을 사용합니다. 기본적으로 “converter”의 값은 “time.localtime”입니다. 필요한 경우, 개별 포매터 인스턴스에 다른 변환기(예: “time.gmtime”)를 설정하거나, 클래스 속성을 변경하여 모든 포매터 인스턴스에 영향을 줄 수 있습니다.
필터
레벨 기반 필터링만으로 충분하지 않을 때, Logger나 Handler는 LogRecord를 출력할지 결정하기 위해 Filter를 호출할 수 있습니다. Logger와 Handler에는 여러 개의 필터를 설치할 수 있으며, 그중 어느 하나라도 LogRecord의 출력을 거부할 수 있습니다.
class Filter:
def filter(self, record):
"""
Return a value indicating true if the record is to be
processed. Possibly modify the record, if deemed
appropriate by the filter.
"""
기본 동작에서는 Filter를 Logger 이름으로 초기화할 수 있습니다. 이렇게 하면 지정된 로거 또는 그 자식 로거들에 의해 생성된 이벤트만 통과시킵니다. 예를 들어, “A.B”로 초기화된 필터는 “A.B”, “A.B.C”, “A.B.C.D”, “A.B.D” 등의 로거가 기록한 이벤트는 통과시키지만 “A.BB”, “B.A.B” 등은 통과시키지 않습니다. 빈 문자열로 초기화하면 모든 이벤트가 Filter를 통과합니다. 이 필터 동작은 애플리케이션의 특정 한 영역에 초점을 맞추고자 할 때 유용합니다. 루트 로거에 붙은 필터를 변경하는 것만으로 초점을 바꿀 수 있습니다.
[6]에는 Filter의 다양한 예제가 제공되어 있습니다.
설정
이러한 로깅 시스템의 주된 이점은 애플리케이션의 소스 코드를 변경하지 않고도 애플리케이션으로부터 얻는 로깅 출력의 양과 종류를 제어할 수 있다는 점입니다. 따라서 설정은 로깅 API를 통해 수행할 수 있지만, 애플리케이션을 전혀 변경하지 않고도 로깅 설정을 바꿀 수 있어야 합니다. Zope와 같이 장시간 실행되는 프로그램의 경우, 프로그램이 실행되는 도중에도 로깅 설정을 변경할 수 있어야 합니다.
설정에는 다음이 포함됩니다:
- 로거나 핸들러가 관심을 가져야 할 로깅 레벨.
- 어떤 핸들러를 어떤 로거에 연결해야 하는가.
- 어떤 필터를 어떤 핸들러와 로거에 연결해야 하는가.
- 특정 핸들러와 필터에 고유한 속성을 지정하는 것.
일반적으로 각 애플리케이션은 사용자가 로깅 출력을 설정하는 방식에 대해 자체적인 요구 사항을 가집니다. 그러나 각 애플리케이션은 표준 메커니즘을 통해 필요한 설정을 로깅 시스템에 지정합니다.
가장 단순한 설정은 루트 로거에 연결되어 stderr에 기록하는 단일 핸들러로 이루어진 설정입니다. 이 설정은 logging 모듈을 임포트한 후 basicConfig() 함수를 한 번 호출하는 것으로 이루어집니다.
def basicConfig(): ...
더 정교한 설정에 대해서는, 다음과 같은 이유로 이 PEP에서 구체적인 제안을 하지 않습니다:
- 특정 제안은 규범적인 것으로 여겨질 수 있습니다.
- 파이썬 커뮤니티에서 폭넓은 실전 경험이 쌓이지 않고서는, 특정 설정 방식이 좋은 것인지 알 방법이 없습니다. 그러한 실전 경험은 로깅 모듈이 실제로 사용된 후에야, 즉 파이썬 2.3이 배포된 이후에나 얻을 수 있습니다.
- 애플리케이션 종류에 따라 서로 다른 설정 방식이 필요할 가능성이 있으므로, “하나로 모든 것을 해결”할 수는 없습니다.
참조 구현 [6]에는 개념을 입증하고 가능한 대안 하나를 제시하기 위해 구현된, 실제로 동작하는 설정 파일 형식이 있습니다. 로깅 설정 및 로그 보기, 보조 핸들러, 그리고 커뮤니티 대다수의 관심사가 아닌 기타 기능들을 위해, 파이썬 핵심 배포판에 포함되지 않는 별도의 확장 모듈이 만들어질 수도 있습니다.
스레드 안전성
로깅 시스템은 사용자가 특별한 조치를 취하지 않아도 스레드 안전 방식으로 동작해야 합니다.
모듈 수준 함수
짧은 스크립트와 소규모 애플리케이션에서 로깅 메커니즘을 사용할 수 있도록, 모듈 수준 함수 debug(), info(), warn(), error(), critical(), exception()이 제공됩니다. 이 함수들은 Logger의 이름이 대응되는 메서드와 같은 방식으로 동작합니다 - 실제로 이들은 루트 로거의 해당 메서드에 위임합니다. 이 함수들이 제공하는 추가 편의 기능은, 아직 아무 설정도 이루어지지 않았다면 basicConfig()가 자동으로 호출된다는 점입니다.
애플리케이션 종료 시, 다음 함수를 호출하여 모든 핸들러를 플러시할 수 있습니다:
def shutdown(): ...
이렇게 하면 모든 핸들러가 플러시되고 닫힙니다.
구현
참조 구현은 Vinay Sajip의 로깅 모듈 [6]입니다.
패키징
참조 구현은 단일 모듈로 구현되어 있습니다. 이는 가장 단순한 인터페이스를 제공합니다 - 사용자는 그저 “import logging”만 하면 사용 가능한 모든 기능을 사용할 수 있는 상태가 됩니다.
참고 자료
Copyright
This document has been placed in the public domain.