[MISC] Rework logger to enable pythonic custom logging configuration to be provided (#4273)
This commit is contained in:
parent
826b82a260
commit
b8afa8b95a
178
examples/logging_configuration.md
Normal file
178
examples/logging_configuration.md
Normal file
@ -0,0 +1,178 @@
|
||||
# Logging Configuration
|
||||
|
||||
vLLM leverages Python's `logging.config.dictConfig` functionality to enable
|
||||
robust and flexible configuration of the various loggers used by vLLM.
|
||||
|
||||
vLLM offers two environment variables that can be used to accommodate a range
|
||||
of logging configurations that range from simple-and-inflexible to
|
||||
more-complex-and-more-flexible.
|
||||
|
||||
- No vLLM logging (simple and inflexible)
|
||||
- Set `VLLM_CONFIGURE_LOGGING=0` (leaving `VLLM_LOGGING_CONFIG_PATH` unset)
|
||||
- vLLM's default logging configuration (simple and inflexible)
|
||||
- Leave `VLLM_CONFIGURE_LOGGING` unset or set `VLLM_CONFIGURE_LOGGING=1`
|
||||
- Fine-grained custom logging configuration (more complex, more flexible)
|
||||
- Leave `VLLM_CONFIGURE_LOGGING` unset or set `VLLM_CONFIGURE_LOGGING=1` and
|
||||
set `VLLM_LOGGING_CONFIG_PATH=<path-to-logging-config.json>`
|
||||
|
||||
|
||||
## Logging Configuration Environment Variables
|
||||
|
||||
### `VLLM_CONFIGURE_LOGGING`
|
||||
|
||||
`VLLM_CONFIGURE_LOGGING` controls whether or not vLLM takes any action to
|
||||
configure the loggers used by vLLM. This functionality is enabled by default,
|
||||
but can be disabled by setting `VLLM_CONFIGURE_LOGGING=0` when running vLLM.
|
||||
|
||||
If `VLLM_CONFIGURE_LOGGING` is enabled and no value is given for
|
||||
`VLLM_LOGGING_CONFIG_PATH`, vLLM will use built-in default configuration to
|
||||
configure the root vLLM logger. By default, no other vLLM loggers are
|
||||
configured and, as such, all vLLM loggers defer to the root vLLM logger to make
|
||||
all logging decisions.
|
||||
|
||||
If `VLLM_CONFIGURE_LOGGING` is disabled and a value is given for
|
||||
`VLLM_LOGGING_CONFIG_PATH`, an error will occur while starting vLLM.
|
||||
|
||||
### `VLLM_LOGGING_CONFIG_PATH`
|
||||
|
||||
`VLLM_LOGGING_CONFIG_PATH` allows users to specify a path to a JSON file of
|
||||
alternative, custom logging configuration that will be used instead of vLLM's
|
||||
built-in default logging configuration. The logging configuration should be
|
||||
provided in JSON format following the schema specified by Python's [logging
|
||||
configuration dictionary
|
||||
schema](https://docs.python.org/3/library/logging.config.html#dictionary-schema-details).
|
||||
|
||||
If `VLLM_LOGGING_CONFIG_PATH` is specified, but `VLLM_CONFIGURE_LOGGING` is
|
||||
disabled, an error will occur while starting vLLM.
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Customize vLLM root logger
|
||||
|
||||
For this example, we will customize the vLLM root logger to use
|
||||
[`python-json-logger`](https://github.com/madzak/python-json-logger) to log to
|
||||
STDOUT of the console in JSON format with a log level of `INFO`.
|
||||
|
||||
To begin, first, create an appropriate JSON logging configuration file:
|
||||
|
||||
**/path/to/logging_config.json:**
|
||||
|
||||
```json
|
||||
{
|
||||
"formatters": {
|
||||
"json": {
|
||||
"class": "pythonjsonlogger.jsonlogger.JsonFormatter"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class" : "logging.StreamHandler",
|
||||
"formatter": "json",
|
||||
"level": "INFO",
|
||||
"stream": "ext://sys.stdout"
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"vllm": {
|
||||
"handlers": ["console"],
|
||||
"level": "INFO",
|
||||
"propagate": false
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
Next, install the `python-json-logger` package if it's not already installed:
|
||||
|
||||
```bash
|
||||
pip install python-json-logger
|
||||
```
|
||||
|
||||
Finally, run vLLM with the `VLLM_LOGGING_CONFIG_PATH` environment variable set
|
||||
to the path of the custom logging configuration JSON file:
|
||||
|
||||
```bash
|
||||
VLLM_LOGGING_CONFIG_PATH=/path/to/logging_config.json \
|
||||
python3 -m vllm.entrypoints.openai.api_server \
|
||||
--max-model-len 2048 \
|
||||
--model mistralai/Mistral-7B-v0.1
|
||||
```
|
||||
|
||||
|
||||
### Example 2: Silence a particular vLLM logger
|
||||
|
||||
To silence a particular vLLM logger, it is necessary to provide custom logging
|
||||
configuration for the target logger that configures the logger so that it won't
|
||||
propagate its log messages to the root vLLM logger.
|
||||
|
||||
When custom configuration is provided for any logger, it is also necessary to
|
||||
provide configuration for the root vLLM logger since any custom logger
|
||||
configuration overrides the built-in default logging configuration used by vLLM.
|
||||
|
||||
First, create an appropriate JSON logging configuration file that includes
|
||||
configuration for the root vLLM logger and for the logger you wish to silence:
|
||||
|
||||
**/path/to/logging_config.json:**
|
||||
|
||||
```json
|
||||
{
|
||||
"formatters": {
|
||||
"vllm": {
|
||||
"class": "vllm.logging.NewLineFormatter",
|
||||
"datefmt": "%m-%d %H:%M:%S",
|
||||
"format": "%(levelname)s %(asctime)s %(filename)s:%(lineno)d] %(message)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"vllm": {
|
||||
"class" : "logging.StreamHandler",
|
||||
"formatter": "vllm",
|
||||
"level": "INFO",
|
||||
"stream": "ext://sys.stdout"
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"vllm": {
|
||||
"handlers": ["vllm"],
|
||||
"level": "DEBUG",
|
||||
"propagage": false
|
||||
},
|
||||
"vllm.example_noisy_logger": {
|
||||
"propagate": false
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
Finally, run vLLM with the `VLLM_LOGGING_CONFIG_PATH` environment variable set
|
||||
to the path of the custom logging configuration JSON file:
|
||||
|
||||
```bash
|
||||
VLLM_LOGGING_CONFIG_PATH=/path/to/logging_config.json \
|
||||
python3 -m vllm.entrypoints.openai.api_server \
|
||||
--max-model-len 2048 \
|
||||
--model mistralai/Mistral-7B-v0.1
|
||||
```
|
||||
|
||||
|
||||
### Example 3: Disable vLLM default logging configuration
|
||||
|
||||
To disable vLLM's default logging configuration and silence all vLLM loggers,
|
||||
simple set `VLLM_CONFIGURE_LOGGING=0` when running vLLM. This will prevent vLLM
|
||||
for configuring the root vLLM logger, which in turn, silences all other vLLM
|
||||
loggers.
|
||||
|
||||
```bash
|
||||
VLLM_CONFIGURE_LOGGING=0 \
|
||||
python3 -m vllm.entrypoints.openai.api_server \
|
||||
--max-model-len 2048 \
|
||||
--model mistralai/Mistral-7B-v0.1
|
||||
```
|
||||
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [`logging.config` Dictionary Schema Details](https://docs.python.org/3/library/logging.config.html#dictionary-schema-details)
|
@ -1,8 +1,19 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from json.decoder import JSONDecodeError
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
from uuid import uuid4
|
||||
|
||||
from vllm.logger import enable_trace_function_call
|
||||
import pytest
|
||||
|
||||
from vllm.logger import (_DATE_FORMAT, _FORMAT, _configure_vllm_root_logger,
|
||||
enable_trace_function_call, init_logger)
|
||||
from vllm.logging import NewLineFormatter
|
||||
|
||||
|
||||
def f1(x):
|
||||
@ -25,3 +36,179 @@ def test_trace_function_call():
|
||||
assert "f2" in content
|
||||
sys.settrace(None)
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def test_default_vllm_root_logger_configuration():
|
||||
"""This test presumes that VLLM_CONFIGURE_LOGGING (default: True) and
|
||||
VLLM_LOGGING_CONFIG_PATH (default: None) are not configured and default
|
||||
behavior is activated."""
|
||||
logger = logging.getLogger("vllm")
|
||||
assert logger.level == logging.DEBUG
|
||||
assert not logger.propagate
|
||||
|
||||
handler = logger.handlers[0]
|
||||
assert handler.stream == sys.stdout
|
||||
assert handler.level == logging.INFO
|
||||
|
||||
formatter = handler.formatter
|
||||
assert formatter is not None
|
||||
assert isinstance(formatter, NewLineFormatter)
|
||||
assert formatter._fmt == _FORMAT
|
||||
assert formatter.datefmt == _DATE_FORMAT
|
||||
|
||||
|
||||
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1)
|
||||
@patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", None)
|
||||
def test_descendent_loggers_depend_on_and_propagate_logs_to_root_logger():
|
||||
"""This test presumes that VLLM_CONFIGURE_LOGGING (default: True) and
|
||||
VLLM_LOGGING_CONFIG_PATH (default: None) are not configured and default
|
||||
behavior is activated."""
|
||||
root_logger = logging.getLogger("vllm")
|
||||
root_handler = root_logger.handlers[0]
|
||||
|
||||
unique_name = f"vllm.{uuid4()}"
|
||||
logger = init_logger(unique_name)
|
||||
assert logger.name == unique_name
|
||||
assert logger.level == logging.NOTSET
|
||||
assert not logger.handlers
|
||||
assert logger.propagate
|
||||
|
||||
message = "Hello, world!"
|
||||
with patch.object(root_handler, "emit") as root_handle_mock:
|
||||
logger.info(message)
|
||||
|
||||
root_handle_mock.assert_called_once()
|
||||
_, call_args, _ = root_handle_mock.mock_calls[0]
|
||||
log_record = call_args[0]
|
||||
assert unique_name == log_record.name
|
||||
assert message == log_record.msg
|
||||
assert message == log_record.msg
|
||||
assert log_record.levelno == logging.INFO
|
||||
|
||||
|
||||
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 0)
|
||||
@patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH", None)
|
||||
def test_logger_configuring_can_be_disabled():
|
||||
"""This test calls _configure_vllm_root_logger again to test custom logging
|
||||
config behavior, however mocks are used to ensure no changes in behavior or
|
||||
configuration occur."""
|
||||
|
||||
with patch("logging.config.dictConfig") as dict_config_mock:
|
||||
_configure_vllm_root_logger()
|
||||
dict_config_mock.assert_not_called()
|
||||
|
||||
|
||||
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1)
|
||||
@patch(
|
||||
"vllm.logger.VLLM_LOGGING_CONFIG_PATH",
|
||||
"/if/there/is/a/file/here/then/you/did/this/to/yourself.json",
|
||||
)
|
||||
def test_an_error_is_raised_when_custom_logging_config_file_does_not_exist():
|
||||
"""This test calls _configure_vllm_root_logger again to test custom logging
|
||||
config behavior, however it fails before any change in behavior or
|
||||
configuration occurs."""
|
||||
with pytest.raises(RuntimeError) as ex_info:
|
||||
_configure_vllm_root_logger()
|
||||
assert ex_info.type == RuntimeError
|
||||
assert "File does not exist" in str(ex_info)
|
||||
|
||||
|
||||
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1)
|
||||
def test_an_error_is_raised_when_custom_logging_config_is_invalid_json():
|
||||
"""This test calls _configure_vllm_root_logger again to test custom logging
|
||||
config behavior, however it fails before any change in behavior or
|
||||
configuration occurs."""
|
||||
with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file:
|
||||
logging_config_file.write("---\nloggers: []\nversion: 1")
|
||||
logging_config_file.flush()
|
||||
with patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH",
|
||||
logging_config_file.name):
|
||||
with pytest.raises(JSONDecodeError) as ex_info:
|
||||
_configure_vllm_root_logger()
|
||||
assert ex_info.type == JSONDecodeError
|
||||
assert "Expecting value" in str(ex_info)
|
||||
|
||||
|
||||
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1)
|
||||
@pytest.mark.parametrize("unexpected_config", (
|
||||
"Invalid string",
|
||||
[{
|
||||
"version": 1,
|
||||
"loggers": []
|
||||
}],
|
||||
0,
|
||||
))
|
||||
def test_an_error_is_raised_when_custom_logging_config_is_unexpected_json(
|
||||
unexpected_config: Any):
|
||||
"""This test calls _configure_vllm_root_logger again to test custom logging
|
||||
config behavior, however it fails before any change in behavior or
|
||||
configuration occurs."""
|
||||
with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file:
|
||||
logging_config_file.write(json.dumps(unexpected_config))
|
||||
logging_config_file.flush()
|
||||
with patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH",
|
||||
logging_config_file.name):
|
||||
with pytest.raises(ValueError) as ex_info:
|
||||
_configure_vllm_root_logger()
|
||||
assert ex_info.type == ValueError
|
||||
assert "Invalid logging config. Expected Dict, got" in str(ex_info)
|
||||
|
||||
|
||||
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 1)
|
||||
def test_custom_logging_config_is_parsed_and_used_when_provided():
|
||||
"""This test calls _configure_vllm_root_logger again to test custom logging
|
||||
config behavior, however mocks are used to ensure no changes in behavior or
|
||||
configuration occur."""
|
||||
valid_logging_config = {
|
||||
"loggers": {
|
||||
"vllm.test_logger.logger": {
|
||||
"handlers": [],
|
||||
"propagate": False,
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file:
|
||||
logging_config_file.write(json.dumps(valid_logging_config))
|
||||
logging_config_file.flush()
|
||||
with patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH",
|
||||
logging_config_file.name), patch(
|
||||
"logging.config.dictConfig") as dict_config_mock:
|
||||
_configure_vllm_root_logger()
|
||||
assert dict_config_mock.called_with(valid_logging_config)
|
||||
|
||||
|
||||
@patch("vllm.logger.VLLM_CONFIGURE_LOGGING", 0)
|
||||
def test_custom_logging_config_causes_an_error_if_configure_logging_is_off():
|
||||
"""This test calls _configure_vllm_root_logger again to test custom logging
|
||||
config behavior, however mocks are used to ensure no changes in behavior or
|
||||
configuration occur."""
|
||||
valid_logging_config = {
|
||||
"loggers": {
|
||||
"vllm.test_logger.logger": {
|
||||
"handlers": [],
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
with NamedTemporaryFile(encoding="utf-8", mode="w") as logging_config_file:
|
||||
logging_config_file.write(json.dumps(valid_logging_config))
|
||||
logging_config_file.flush()
|
||||
with patch("vllm.logger.VLLM_LOGGING_CONFIG_PATH",
|
||||
logging_config_file.name):
|
||||
with pytest.raises(RuntimeError) as ex_info:
|
||||
_configure_vllm_root_logger()
|
||||
assert ex_info.type is RuntimeError
|
||||
expected_message_snippet = (
|
||||
"VLLM_CONFIGURE_LOGGING evaluated to false, but "
|
||||
"VLLM_LOGGING_CONFIG_PATH was given.")
|
||||
assert expected_message_snippet in str(ex_info)
|
||||
|
||||
# Remember! The root logger is assumed to have been configured as
|
||||
# though VLLM_CONFIGURE_LOGGING=1 and VLLM_LOGGING_CONFIG_PATH=None.
|
||||
root_logger = logging.getLogger("vllm")
|
||||
other_logger_name = f"vllm.test_logger.{uuid4()}"
|
||||
other_logger = init_logger(other_logger_name)
|
||||
assert other_logger.handlers != root_logger.handlers
|
||||
assert other_logger.level != root_logger.level
|
||||
assert other_logger.propagate
|
||||
|
122
vllm/logger.py
122
vllm/logger.py
@ -1,73 +1,91 @@
|
||||
# Adapted from
|
||||
# https://github.com/skypilot-org/skypilot/blob/86dc0f6283a335e4aa37b3c10716f90999f48ab6/sky/sky_logging.py
|
||||
"""Logging configuration for vLLM."""
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from functools import partial
|
||||
from typing import Optional
|
||||
from logging import Logger
|
||||
from logging.config import dictConfig
|
||||
from os import path
|
||||
from typing import Dict, Optional
|
||||
|
||||
VLLM_CONFIGURE_LOGGING = int(os.getenv("VLLM_CONFIGURE_LOGGING", "1"))
|
||||
VLLM_LOGGING_CONFIG_PATH = os.getenv("VLLM_LOGGING_CONFIG_PATH")
|
||||
|
||||
_FORMAT = "%(levelname)s %(asctime)s %(filename)s:%(lineno)d] %(message)s"
|
||||
_DATE_FORMAT = "%m-%d %H:%M:%S"
|
||||
|
||||
|
||||
class NewLineFormatter(logging.Formatter):
|
||||
"""Adds logging prefix to newlines to align multi-line messages."""
|
||||
|
||||
def __init__(self, fmt, datefmt=None):
|
||||
logging.Formatter.__init__(self, fmt, datefmt)
|
||||
|
||||
def format(self, record):
|
||||
msg = logging.Formatter.format(self, record)
|
||||
if record.message != "":
|
||||
parts = msg.split(record.message)
|
||||
msg = msg.replace("\n", "\r\n" + parts[0])
|
||||
return msg
|
||||
DEFAULT_LOGGING_CONFIG = {
|
||||
"formatters": {
|
||||
"vllm": {
|
||||
"class": "vllm.logging.NewLineFormatter",
|
||||
"datefmt": _DATE_FORMAT,
|
||||
"format": _FORMAT,
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"vllm": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "vllm",
|
||||
"level": "INFO",
|
||||
"stream": "ext://sys.stdout",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"vllm": {
|
||||
"handlers": ["vllm"],
|
||||
"level": "DEBUG",
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
"version": 1,
|
||||
}
|
||||
|
||||
|
||||
_root_logger = logging.getLogger("vllm")
|
||||
_default_handler: Optional[logging.Handler] = None
|
||||
def _configure_vllm_root_logger() -> None:
|
||||
logging_config: Optional[Dict] = None
|
||||
|
||||
|
||||
def _setup_logger():
|
||||
_root_logger.setLevel(logging.DEBUG)
|
||||
global _default_handler
|
||||
if _default_handler is None:
|
||||
_default_handler = logging.StreamHandler(sys.stdout)
|
||||
_default_handler.flush = sys.stdout.flush # type: ignore
|
||||
_default_handler.setLevel(logging.INFO)
|
||||
_root_logger.addHandler(_default_handler)
|
||||
fmt = NewLineFormatter(_FORMAT, datefmt=_DATE_FORMAT)
|
||||
_default_handler.setFormatter(fmt)
|
||||
# Setting this will avoid the message
|
||||
# being propagated to the parent logger.
|
||||
_root_logger.propagate = False
|
||||
|
||||
|
||||
# The logger is initialized when the module is imported.
|
||||
# This is thread-safe as the module is only imported once,
|
||||
# guaranteed by the Python GIL.
|
||||
if VLLM_CONFIGURE_LOGGING:
|
||||
_setup_logger()
|
||||
|
||||
|
||||
def init_logger(name: str):
|
||||
# Use the same settings as above for root logger
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(os.getenv("LOG_LEVEL", "DEBUG"))
|
||||
if not VLLM_CONFIGURE_LOGGING and VLLM_LOGGING_CONFIG_PATH:
|
||||
raise RuntimeError(
|
||||
"VLLM_CONFIGURE_LOGGING evaluated to false, but "
|
||||
"VLLM_LOGGING_CONFIG_PATH was given. VLLM_LOGGING_CONFIG_PATH "
|
||||
"implies VLLM_CONFIGURE_LOGGING. Please enable "
|
||||
"VLLM_CONFIGURE_LOGGING or unset VLLM_LOGGING_CONFIG_PATH.")
|
||||
|
||||
if VLLM_CONFIGURE_LOGGING:
|
||||
if _default_handler is None:
|
||||
raise ValueError(
|
||||
"_default_handler is not set up. This should never happen!"
|
||||
" Please open an issue on Github.")
|
||||
logger.addHandler(_default_handler)
|
||||
logger.propagate = False
|
||||
return logger
|
||||
logging_config = DEFAULT_LOGGING_CONFIG
|
||||
|
||||
if VLLM_LOGGING_CONFIG_PATH:
|
||||
if not path.exists(VLLM_LOGGING_CONFIG_PATH):
|
||||
raise RuntimeError(
|
||||
"Could not load logging config. File does not exist: %s",
|
||||
VLLM_LOGGING_CONFIG_PATH)
|
||||
with open(VLLM_LOGGING_CONFIG_PATH, encoding="utf-8",
|
||||
mode="r") as file:
|
||||
custom_config = json.loads(file.read())
|
||||
|
||||
if not isinstance(custom_config, dict):
|
||||
raise ValueError("Invalid logging config. Expected Dict, got %s.",
|
||||
type(custom_config).__name__)
|
||||
logging_config = custom_config
|
||||
|
||||
if logging_config:
|
||||
dictConfig(logging_config)
|
||||
|
||||
|
||||
def init_logger(name: str) -> Logger:
|
||||
"""The main purpose of this function is to ensure that loggers are
|
||||
retrieved in such a way that we can be sure the root vllm logger has
|
||||
already been configured."""
|
||||
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
# The root logger is initialized when the module is imported.
|
||||
# This is thread-safe as the module is only imported once,
|
||||
# guaranteed by the Python GIL.
|
||||
_configure_vllm_root_logger()
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
5
vllm/logging/__init__.py
Normal file
5
vllm/logging/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from vllm.logging.formatter import NewLineFormatter
|
||||
|
||||
__all__ = [
|
||||
"NewLineFormatter",
|
||||
]
|
15
vllm/logging/formatter.py
Normal file
15
vllm/logging/formatter.py
Normal file
@ -0,0 +1,15 @@
|
||||
import logging
|
||||
|
||||
|
||||
class NewLineFormatter(logging.Formatter):
|
||||
"""Adds logging prefix to newlines to align multi-line messages."""
|
||||
|
||||
def __init__(self, fmt, datefmt=None, style="%"):
|
||||
logging.Formatter.__init__(self, fmt, datefmt, style)
|
||||
|
||||
def format(self, record):
|
||||
msg = logging.Formatter.format(self, record)
|
||||
if record.message != "":
|
||||
parts = msg.split(record.message)
|
||||
msg = msg.replace("\n", "\r\n" + parts[0])
|
||||
return msg
|
Loading…
x
Reference in New Issue
Block a user