Python 로깅: 심층 자습서

게시 됨: 2022-03-11

응용 프로그램이 더 복잡해짐에 따라 좋은 로그를 갖는 것은 디버깅할 때뿐만 아니라 응용 프로그램 문제/성능에 대한 통찰력을 제공하는 데에도 매우 유용할 수 있습니다.

Python 표준 라이브러리는 대부분의 기본 로깅 기능을 제공하는 로깅 모듈과 함께 제공됩니다. 올바르게 설정하면 로그 메시지는 실행 중인 프로세스/스레드와 같은 로그 컨텍스트뿐만 아니라 로그가 실행되는 시기와 위치에 대한 유용한 정보를 많이 가져올 수 있습니다.

장점에도 불구하고 로깅 모듈은 제대로 설정하는 데 시간이 걸리고 내 생각에 https://docs.python.org/3/library/logging.html에 있는 공식 로깅 문서가 완전하긴 하지만 설정하는 데 시간이 걸리기 때문에 종종 간과됩니다. 실제로 로깅 모범 사례를 제공하거나 일부 놀라운 로깅을 강조하지 않습니다.

이 Python 로깅 튜토리얼은 로깅 모듈에 대한 완전한 문서가 아니라 일부 로깅 개념과 주의해야 할 "문제"를 소개하는 "시작하기" 가이드를 의미합니다. 이 게시물은 모범 사례로 끝나고 고급 로깅 ​​주제에 대한 몇 가지 포인터를 포함합니다.

게시물의 모든 코드 조각은 이미 로깅 모듈을 가져왔다고 가정합니다.

 import logging

Python 로깅에 대한 개념

이 섹션에서는 로깅 모듈에서 자주 접하게 되는 몇 가지 개념에 대한 개요를 제공합니다.

Python 로깅 수준

로그 수준은 로그가 제공하는 "중요도"에 해당합니다. "오류" 로그는 "경고" 로그보다 더 긴급해야 하지만 "디버그" 로그는 응용 프로그램을 디버깅할 때만 유용해야 합니다.

Python에는 6개의 로그 수준이 있습니다. 각 수준은 로그 심각도를 나타내는 정수(NOTSET=0, DEBUG=10, INFO=20, WARN=30, ERROR=40 및 CRITICAL=50)와 연결됩니다.

Python Logging의 로그 수준

NOTSET을 제외하고 모든 수준은 다소 간단합니다(DEBUG < INFO < WARN ).

파이썬 로깅 포맷

로그 포맷터는 기본적으로 컨텍스트 정보를 추가하여 로그 메시지를 강화합니다. 로그가 전송된 시기, 위치(Python 파일, 줄 번호, 메서드 등), 스레드 및 프로세스와 같은 추가 컨텍스트(다중 스레드 응용 프로그램을 디버깅할 때 매우 유용할 수 있음)를 아는 것이 유용할 수 있습니다.

예를 들어, "hello world" 로그가 로그 포맷터를 통해 전송되는 경우:

 "%(asctime)s — %(name)s — %(levelname)s — %(funcName)s:%(lineno)d — %(message)s"

될 것이다

 2018-02-07 19:47:41,864 - abc - WARNING - <module>:1 - hello world 

Python 로깅 메시지 형식

파이썬 로깅 핸들러

로그 핸들러는 로그를 효과적으로 작성/표시하는 구성 요소입니다. 콘솔(StreamHandler를 통해), 파일에(FileHandler를 통해), 심지어 SMTPHandler를 통해 이메일을 보내서 로그를 표시합니다.

각 로그 핸들러에는 2개의 중요한 필드가 있습니다.

  • 로그에 컨텍스트 정보를 추가하는 포맷터입니다.
  • 수준이 낮은 로그를 필터링하는 로그 수준입니다. 따라서 INFO 레벨의 로그 핸들러는 DEBUG 로그를 처리하지 않습니다.

파이썬 로깅 핸들러

표준 라이브러리는 일반적인 사용 사례에 충분해야 하는 소수의 핸들러를 제공합니다. https://docs.python.org/3/library/logging.handlers.html#module-logging.handlers. 가장 일반적인 것은 StreamHandler 및 FileHandler입니다.

 console_handler = logging.StreamHandler() file_handler = logging.FileHandler("filename")

파이썬 로거

로거는 아마도 코드에서 가장 자주 직접 사용되며 가장 복잡한 것입니다. 새 로거는 다음을 통해 얻을 수 있습니다.

 toto_logger = logging.getLogger("toto")

로거에는 세 가지 주요 필드가 있습니다.

  • 전파: 로그를 로거의 상위 항목으로 전파해야 하는지 여부를 결정합니다. 기본적으로 값은 True입니다.
  • A 수준: 로그 처리기 수준과 마찬가지로 로거 수준은 "덜 중요한" 로그를 필터링하는 데 사용됩니다. 로그 핸들러와 달리 레벨은 "하위" 로거에서만 확인됩니다. 로그가 부모에게 전파되면 수준이 확인되지 않습니다. 이것은 다소 직관적이지 않은 동작입니다.
  • 핸들러: 로그가 로거에 도착할 때 로그가 전송될 핸들러 목록입니다. 이를 통해 유연한 로그 처리가 가능합니다. 예를 들어 모든 DEBUG 로그를 기록하는 파일 로그 처리기와 CRITICAL 로그에만 사용되는 이메일 로그 처리기를 가질 수 있습니다. 이와 관련하여 로거-처리기 관계는 게시자-소비자 관계와 유사합니다. 로거 수준 검사를 통과하면 로그가 모든 처리기에 브로드캐스트됩니다.

Python 로깅의 프로세스

로거는 이름이 고유 합니다. 즉, "toto"라는 이름의 로거가 생성된 경우 결과적으로 logging.getLogger("toto") 호출이 동일한 객체를 반환합니다.

 assert id(logging.getLogger("toto")) == id(logging.getLogger("toto"))

짐작하셨겠지만 로거에는 계층 구조가 있습니다. 계층 구조의 맨 위에는 logging.root를 통해 액세스할 수 있는 루트 로거가 있습니다. 이 로거는 logging.debug() 와 같은 메소드가 사용될 때 호출됩니다. 기본적으로 루트 로그 수준은 WARN이므로 더 낮은 수준의 모든 로그(예: logging.info("info") 를 통해)는 무시됩니다. 루트 로거의 또 다른 특징은 WARN보다 높은 수준의 로그가 처음 기록될 때 기본 처리기가 생성된다는 것입니다. logging.debug() 와 같은 메소드를 통해 루트 로거를 직접 또는 간접적으로 사용하는 것은 일반적으로 권장되지 않습니다.

기본적으로 새 로거가 생성되면 부모가 루트 로거로 설정됩니다.

 lab = logging.getLogger("ab") assert lab.parent == logging.root # lab's parent is indeed the root logger

그러나 로거는 "점 표기법"을 사용합니다. 즉, 이름이 "ab"인 로거는 로거 "a"의 자식이 됩니다. 그러나 이것은 로거 "a"가 생성된 경우에만 해당되며, 그렇지 않으면 "ab" 부모가 여전히 루트입니다.

 la = logging.getLogger("a") assert lab.parent == la # lab's parent is now la instead of root

로거가 레벨 검사에 따라 로그를 통과할지 여부를 결정할 때(예: 로그 레벨이 로거 레벨보다 낮으면 로그는 무시됨) 실제 레벨 대신 "유효 레벨"을 사용합니다. 유효 레벨은 레벨이 NOTSET이 아닌 경우, 즉 DEBUG에서 CRITICAL까지의 모든 값이 아닌 경우 로거 레벨과 동일합니다. 그러나 로거 수준이 NOTSET이면 유효 수준은 NOTSET 수준이 아닌 첫 번째 상위 수준이 됩니다.

기본적으로 새 로거에는 NOTSET 수준이 있으며 루트 로거에는 WARN 수준이 있으므로 로거의 유효 수준은 WARN이 됩니다. 따라서 새 로거에 일부 처리기가 연결되어 있어도 로그 수준이 WARN을 초과하지 않는 한 이러한 처리기는 호출되지 않습니다.

 toto_logger = logging.getLogger("toto") assert toto_logger.level == logging.NOTSET # new logger has NOTSET level assert toto_logger.getEffectiveLevel() == logging.WARN # and its effective level is the root logger level, ie WARN # attach a console handler to toto_logger console_handler = logging.StreamHandler() toto_logger.addHandler(console_handler) toto_logger.debug("debug") # nothing is displayed as the log level DEBUG is smaller than toto effective level toto_logger.setLevel(logging.DEBUG) toto_logger.debug("debug message") # now you should see "debug message" on screen

기본적으로 로거 수준은 로그 통과를 결정하는 데 사용됩니다. 로그 수준이 로거 수준보다 낮으면 로그가 무시됩니다.

Python 로깅 모범 사례

로깅 모듈은 실제로 매우 편리하지만 최고의 Python 개발자에게도 오랜 시간 골치 아픈 문제를 일으킬 수 있는 몇 가지 단점이 있습니다. 내 생각에 이 모듈을 사용하기 위한 모범 사례는 다음과 같습니다.

  • 루트 로거를 구성하지만 코드에서 사용하지 마십시오. 예를 들어 실제로 장면 뒤에서 루트 로거를 호출하는 logging.info() 와 같은 함수를 호출하지 마십시오. 사용하는 라이브러리에서 오류 메시지를 포착하려면 예를 들어 디버깅을 더 쉽게 하기 위해 파일에 쓰도록 루트 로거를 구성해야 합니다. 기본적으로 루트 로거는 stderr 에만 출력하므로 로그가 쉽게 손실될 수 있습니다.
  • 로깅을 사용하려면 logging.getLogger(logger name) 을 사용하여 새 로거를 생성해야 합니다. 나는 일반적으로 __name__ 을 로거 이름으로 사용하지만 일관성이 있는 한 무엇이든 사용할 수 있습니다. 핸들러를 더 추가하려면 일반적으로 로거를 반환하는 메서드가 있습니다(요점은 https://gist.github.com/nguyenkims/e92df0f8bd49973f0c94bddf36ed7fd0에서 찾을 수 있음).
 import logging import sys from logging.handlers import TimedRotatingFileHandler FORMATTER = logging.Formatter("%(asctime)s — %(name)s — %(levelname)s — %(message)s") LOG_FILE = "my_app.log" def get_console_handler(): console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(FORMATTER) return console_handler def get_file_handler(): file_handler = TimedRotatingFileHandler(LOG_FILE, when='midnight') file_handler.setFormatter(FORMATTER) return file_handler def get_logger(logger_name): logger = logging.getLogger(logger_name) logger.setLevel(logging.DEBUG) # better to have too much log than not enough logger.addHandler(get_console_handler()) logger.addHandler(get_file_handler()) # with this pattern, it's rarely necessary to propagate the error up to parent logger.propagate = False return logger

새 로거를 만들고 사용할 수 있는 후:

 my_logger = get_logger("my module name") my_logger.debug("a debug message")
  • FileHandler 대신 예제에서 사용된 TimedRotatingFileHandler와 같은 RotatingFileHandler 클래스를 사용하십시오. 파일 크기 제한에 도달하거나 매일 수행할 때 자동으로 파일을 회전하기 때문입니다.
  • Sentry, Airbrake, Raygun 등과 같은 도구를 사용하여 오류 로그를 자동으로 잡아냅니다. 이는 로그가 매우 장황하고 오류 로그가 쉽게 손실될 수 있는 웹 앱의 컨텍스트에서 특히 유용합니다. 이러한 도구를 사용하는 또 다른 이점은 오류의 변수 값에 대한 세부 정보를 얻을 수 있으므로 오류를 트리거하는 URL, 우려하는 사용자 등을 알 수 있다는 것입니다.

더 많은 모범 사례에 관심이 있다면 동료 Toptaler Martin Chikilian의 The 10 Most Common Mistakes That Python 개발자가 만드는 The 10 Most Common Mistakes That Python 개발자를 읽으십시오.