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 앱을 보다 사용자 친화적이고 강력하게 만드는 데 도움이 되기를 바랍니다.

    좋은 웹페이지 즐겨찾기