Pythonコマンドラインアプリケーションのログ出力
導入
ログは、重大なアプリケーションの必要な部分です.それはあなたとあなたのユーザーが効果的に問題をデバッグすることができます.この記事では、コマンドラインPythonアプリケーションのログを設定する良い方法を紹介します.完成品はgithub gistとして見ることができますhere .
ゴール
我々のロガーの目標は複数の部品を持つでしょう.
ログファイルは次のようになります.
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
我々は素晴らしいを使用しているpretty-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
The 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),
}
これは、プレーンメッセージとして渡される情報メッセージを除いて、レベル名とメッセージの間に垂直線を加えます.The 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)
陰謀
本稿では、
Reference
この問題について(Pythonコマンドラインアプリケーションのログ出力), 我々は、より多くの情報をここで見つけました https://dev.to/eblocha/logging-in-python-command-line-applications-2gmiテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol