Python 명령줄 애플리케이션에 로그인
소개
로깅은 중요한 응용 프로그램의 필수 부분입니다. 이를 통해 귀하와 귀하의 사용자는 문제를 효과적으로 디버깅할 수 있습니다. 이 기사에서는 명령줄 Python 앱에 대한 로깅을 구성하는 좋은 방법을 소개합니다. 완성된 제품은 github gisthere 에서 보실 수 있습니다.
목표
로거의 목표는 여러 부분으로 구성됩니다.
콘솔 출력을 다음과 같이 만들 것입니다.
로그 파일은 다음과 같습니다.
DEBUG:2022-04-03 15:41:17,920:root:A debug message
INFO:2022-04-03 15:41:17,920:root:An info message
WARNING:2022-04-03 15:41:17,920:root:A warning message
ERROR:2022-04-03 15:41:17,920:root:An error message
CRITICAL:2022-04-03 15:41:17,920:root:A critical message from an exception
Traceback (most recent call last):
/home/eb/projects/py-scratch/color-log.py <module> 327: raise ValueError("A critical message from an exception")
ValueError: A critical message from an exception
여기에는 몇 가지 까다로운 일이 있습니다.
포맷터
Python 로깅 포맷터는 로그 수준에 따라 다른 포맷 문자열을 허용하지 않으므로 자체 포맷터를 구현해야 합니다.
import typing as t
class MultiFormatter(PrettyExceptionFormatter):
"""Format log messages differently for each log level"""
def __init__(self, formats: t.Dict[int, str] = None, **kwargs):
base_format = kwargs.pop("fmt", None)
super().__init__(base_format, **kwargs)
formats = formats or default_formats
self.formatters = {
level: PrettyExceptionFormatter(fmt, **kwargs)
for level, fmt in formats.items()
}
def format(self, record: logging.LogRecord):
formatter = self.formatters.get(record.levelno)
if formatter is None:
return super().format(record)
return formatter.format(record)
MultiFormatter
클래스에서는 로그 수준을 형식 문자열로 매핑한 다음 각 수준에 대해 서로 다른 포맷터를 만듭니다. .format()
에서 기록된 수준의 포맷터로 디스패치합니다.자,
PrettyExceptionFormatter
는 무엇입니까? 우리도 그것을 구현해야 합니다. 로그 레코드에 포함될 때 추적 및 예외 정보를 형식화합니다.from textwrap import indent
from pretty_traceback.formatting import exc_to_traceback_str
class PrettyExceptionFormatter(logging.Formatter):
"""Uses pretty-traceback to format exceptions"""
def __init__(self, *args, color=True, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.color = color
def formatException(self, ei):
_, exc_value, traceback = ei
return exc_to_traceback_str(exc_value, traceback, color=self.color)
def format(self, record: logging.LogRecord):
record.message = record.getMessage()
if self.usesTime():
record.asctime = self.formatTime(record, self.datefmt)
s = self.formatMessage(record)
if record.exc_info:
# Don't assign to exc_text here, since we don't want to inject color all the time
if s[-1:] != "\n":
s += "\n"
# Add indent to indicate the traceback is part of the previous message
text = indent(self.formatException(record.exc_info), " " * 4)
s += text
return s
여기서는 awesomepretty-traceback 패키지를 사용하고 있습니다.
logging.Formatter
의 기본 동작은 record.exc_text
의 출력으로 .formatException()
를 수정하는 것이므로 해당 동작을 재정의해야 합니다. 이는 ANSI 색상을 추가하고 로그 파일에서 보고 싶지 않기 때문입니다.표준
logging.Formatter
구현에서는 예외를 포맷할 때 레코드가 수정됩니다(python 3.10.2 기준).def format(self, record):
...
# exc_text is MODIFIED, which propagates to other formatters for other handlers
record.exc_text = self.formatException(record.exc_info)
...
return s
MultiFormatter
클래스는 다음과 같은 기본 문자열을 사용하여 수준별 형식 문자열을 변경하는 인수를 사용합니다.default_formats = {
logging.DEBUG: style("DEBUG", fg="cyan") + " | " + style("%(message)s", fg="cyan"),
logging.INFO: "%(message)s",
logging.WARNING: style("WARN ", fg="yellow") + " | " + style("%(message)s", fg="yellow"),
logging.ERROR: style("ERROR", fg="red") + " | " + style("%(message)s", fg="red"),
logging.CRITICAL: style("FATAL", fg="white", bg="red", bold=True) + " | " + style("%(message)s", fg="red", bold=True),
}
이렇게 하면 일반 메시지로 전달되는 정보 메시지를 제외하고 수준 이름과 메시지 사이에 수직선이 추가됩니다. 여기서
style
함수는 click.style
유틸리티의 직접 복사본입니다.컨텍스트 관리자
여기서 최종 목표는 단순히
with cli_log_config():
를 호출하고 아름다운 출력을 얻는 것입니다. 컨텍스트 관리자가 필요합니다. python docs에서 바로 로깅 컨텍스트로 시작합니다.class LoggingContext:
def __init__(
self,
logger: logging.Logger = None,
level: int = None,
handler: logging.Handler = None,
close: bool = True,
):
self.logger = logger or logging.root
self.level = level
self.handler = handler
self.close = close
def __enter__(self):
if self.level is not None:
self.old_level = self.logger.level
self.logger.setLevel(self.level)
if self.handler:
self.logger.addHandler(self.handler)
def __exit__(self, *exc_info):
if self.level is not None:
self.logger.setLevel(self.old_level)
if self.handler:
self.logger.removeHandler(self.handler)
if self.handler and self.close:
self.handler.close()
다음으로 여러 컨텍스트 관리자를 동적으로 결합하는 특수 컨텍스트 관리자를 만듭니다.
class MultiContext:
"""Can be used to dynamically combine context managers"""
def __init__(self, *contexts) -> None:
self.contexts = contexts
def __enter__(self):
return tuple(ctx.__enter__() for ctx in self.contexts)
def __exit__(self, *exc_info):
for ctx in self.contexts:
ctx.__exit__(*exc_info)
마지막으로 지금까지 수행한 모든 작업을 단일 컨텍스트 관리자로 결합할 수 있습니다.
def cli_log_config(
logger: logging.Logger = None,
verbose: int = 2,
filename: str = None,
file_verbose: int = None,
):
"""
Use a logging configuration for a CLI application.
This will prettify log messages for the console, and show more info in a log file.
Parameters
----------
logger : logging.Logger, default None
The logger to configure. If None, configures the root logger
verbose : int from 0 to 3, default 2
Sets the output verbosity.
Verbosity 0 shows critical errors
Verbosity 1 shows warnings and above
Verbosity 2 shows info and above
Verbosity 3 and above shows debug and above
filename : str, default None
The file name of the log file to log to. If None, no log file is generated.
file_verbose : int from 0 to 3, default None
Set a different verbosity for the log file. If None, is set to `verbose`.
This has no effect if `filename` is None.
Returns
-------
A context manager that will configure the logger, and reset to the previous configuration afterwards.
"""
if file_verbose is None:
file_verbose = verbose
verbosities = {
0: logging.CRITICAL,
1: logging.WARNING,
2: logging.INFO,
3: logging.DEBUG,
}
console_level = verbosities.get(verbose, logging.DEBUG)
file_level = verbosities.get(file_verbose, logging.DEBUG)
# This configuration will print pretty tracebacks with color to the console,
# and log pretty tracebacks without color to the log file.
console_handler = logging.StreamHandler()
console_handler.setFormatter(MultiFormatter())
console_handler.setLevel(console_level)
contexts = [
LoggingContext(logger=logger, level=min(console_level, file_level)),
LoggingContext(logger=logger, handler=console_handler, close=False),
]
if filename:
file_handler = logging.FileHandler(filename)
file_handler.setFormatter(
PrettyExceptionFormatter(
"%(levelname)s:%(asctime)s:%(name)s:%(message)s", color=False
)
)
file_handler.setLevel(file_level)
contexts.append(LoggingContext(logger=logger, handler=file_handler))
return MultiContext(*contexts)
이제 상세 수준, 로그 파일 및 파일에 대한 다른 상세 수준을 지정하는 옵션이 있습니다. 다음 예를 시도해 보십시오.
with cli_log_config(verbose=3, filename="test.log"):
try:
logging.debug("A debug message")
logging.info("An info message")
logging.warning("A warning message")
logging.error("An error message")
raise ValueError("A critical message from an exception")
except Exception as exc:
logging.critical(str(exc), exc_info=True)
결론
이 문서에서 우리는:
CLI 앱을 보다 사용자 친화적이고 강력하게 만드는 데 도움이 되기를 바랍니다.
Reference
이 문제에 관하여(Python 명령줄 애플리케이션에 로그인), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/eblocha/logging-in-python-command-line-applications-2gmi텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)