generated from daniil-berg/boilerplate-py
added code and readme contents
This commit is contained in:
parent
fc29d44522
commit
45a0e123fd
52
README.md
52
README.md
@ -1,10 +1,56 @@
|
|||||||
# syslogformat
|
# syslogformat
|
||||||
|
|
||||||
Python logging.Formatter class for syslog style messages
|
Python `logging.Formatter` class for syslog style messages
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
...
|
Example with in-code setup:
|
||||||
|
```python
|
||||||
|
# example.py
|
||||||
|
import logging
|
||||||
|
from syslogformat import SyslogFormatter
|
||||||
|
|
||||||
|
log = logging.getLogger()
|
||||||
|
hdl = logging.StreamHandler()
|
||||||
|
fmt = SyslogFormatter()
|
||||||
|
hdl.setFormatter(fmt)
|
||||||
|
hdl.setLevel(logging.NOTSET)
|
||||||
|
log.addHandler(hdl)
|
||||||
|
log.setLevel(logging.NOTSET)
|
||||||
|
|
||||||
|
log.debug("foo")
|
||||||
|
log.info("bar")
|
||||||
|
log.warning("baz")
|
||||||
|
try:
|
||||||
|
raise ValueError("this is bad")
|
||||||
|
except ValueError as e:
|
||||||
|
log.exception("oh no")
|
||||||
|
```
|
||||||
|
|
||||||
|
This is what the output should be like:
|
||||||
|
```
|
||||||
|
<7>DEBUG | foo
|
||||||
|
<6>INFO | bar
|
||||||
|
<4>WARNING | baz | example.<module>.14
|
||||||
|
<3>ERROR | oh no | example.<module>.18 | 'Traceback (most recent call last):\n File "/path/to/example.py", line 16, in <module>\n raise ValueError("this is bad")\nValueError: this is bad'
|
||||||
|
```
|
||||||
|
|
||||||
|
Same configuration with YAML file to be loaded into `logging.config.dictConfig`:
|
||||||
|
```yaml
|
||||||
|
version: 1
|
||||||
|
formatters:
|
||||||
|
custom:
|
||||||
|
(): syslogformat.SyslogFormatter
|
||||||
|
handlers:
|
||||||
|
console:
|
||||||
|
class: logging.StreamHandler
|
||||||
|
level: NOTSET
|
||||||
|
formatter: custom
|
||||||
|
stream: ext://sys.stdout
|
||||||
|
root:
|
||||||
|
level: NOTSET
|
||||||
|
handlers: [console]
|
||||||
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -12,7 +58,7 @@ Python logging.Formatter class for syslog style messages
|
|||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
Python Version ..., OS ...
|
Python Version 3
|
||||||
|
|
||||||
## Building from source
|
## Building from source
|
||||||
|
|
||||||
|
1
src/syslogformat/__init__.py
Normal file
1
src/syslogformat/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from syslogformat.formatter import SyslogFormatter
|
65
src/syslogformat/formatter.py
Normal file
65
src/syslogformat/formatter.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import logging as _logging
|
||||||
|
|
||||||
|
from syslogformat.helpers import check_level, py_to_sys_lvl
|
||||||
|
|
||||||
|
|
||||||
|
class SyslogFormatter(_logging.Formatter):
|
||||||
|
"""
|
||||||
|
logging.Formatter subclass for doing three things:
|
||||||
|
1) Format exception log messages into one-liners,
|
||||||
|
2) prepend a syslog oriented log level number to be recognized by systemd, and
|
||||||
|
3) append more details ([module].[function].[line]) to every message, when specified level is exceeded.
|
||||||
|
"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.detail_threshold = check_level(kwargs.pop('detail_threshold', _logging.WARNING))
|
||||||
|
self.prepend_lvl_name = check_level(kwargs.pop('prepend_lvl_name', True))
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def formatException(self, exc_info) -> str:
|
||||||
|
"""Format an exception so that it prints on a single line."""
|
||||||
|
return repr(super().formatException(exc_info))
|
||||||
|
|
||||||
|
def format(self, record: _logging.LogRecord) -> str:
|
||||||
|
"""
|
||||||
|
Format to be compatible with syslog levels and replace the newlines.
|
||||||
|
The entire message format is hard-coded here, so no format needs to be specified in the usual config.
|
||||||
|
"""
|
||||||
|
record.message = record.getMessage()
|
||||||
|
|
||||||
|
# Prepend syslog level depending on record level
|
||||||
|
s = f"<{py_to_sys_lvl(record.levelno)}>"
|
||||||
|
if self.prepend_lvl_name:
|
||||||
|
s += f"{record.levelname:<8} | "
|
||||||
|
s += self.formatMessage(record)
|
||||||
|
|
||||||
|
# If record level exceeds the threshold, append additional details
|
||||||
|
if record.levelno >= self.detail_threshold:
|
||||||
|
s += f" | {record.module}.{record.funcName}.{record.lineno}"
|
||||||
|
|
||||||
|
if record.exc_info and not record.exc_text:
|
||||||
|
record.exc_text = self.formatException(record.exc_info)
|
||||||
|
if record.exc_text:
|
||||||
|
s += ' | ' + record.exc_text
|
||||||
|
if record.stack_info:
|
||||||
|
s += self.formatStack(record.stack_info)
|
||||||
|
|
||||||
|
# Reformat exception line
|
||||||
|
return s.replace("\n", "")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
log = _logging.getLogger()
|
||||||
|
hdl = _logging.StreamHandler()
|
||||||
|
fmt = SyslogFormatter()
|
||||||
|
hdl.setFormatter(fmt)
|
||||||
|
log.addHandler(hdl)
|
||||||
|
log.setLevel(_logging.NOTSET)
|
||||||
|
|
||||||
|
log.debug("foo")
|
||||||
|
log.info("bar")
|
||||||
|
log.warning("baz")
|
||||||
|
try:
|
||||||
|
raise ValueError("this is bad")
|
||||||
|
except ValueError as e:
|
||||||
|
log.exception("oh no")
|
54
src/syslogformat/helpers.py
Normal file
54
src/syslogformat/helpers.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import logging as _logging
|
||||||
|
from syslog import LOG_ALERT, LOG_CRIT, LOG_ERR, LOG_WARNING, LOG_INFO, LOG_DEBUG
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
LevelT = Union[str, int]
|
||||||
|
|
||||||
|
|
||||||
|
def check_level(level: LevelT) -> int:
|
||||||
|
"""
|
||||||
|
Custom implementation of the logging module's _checkLevel(...) function.
|
||||||
|
Returns the numeric representation of a log level.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
level: Either a string such as 'DEBUG' or 'WARNING' or an integer.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
If an integer is passed, it is returned unchanged;
|
||||||
|
if a string is passed, the corresponding numeric log level is returned.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TypeError if something other than a string or an integer is passed.
|
||||||
|
ValueError if the string has no corresponding level in the logging module.
|
||||||
|
"""
|
||||||
|
if isinstance(level, int):
|
||||||
|
return level
|
||||||
|
if str(level) != level:
|
||||||
|
raise TypeError(f"Level not an integer or a valid string: {level}")
|
||||||
|
output = getattr(_logging, level)
|
||||||
|
if output is None:
|
||||||
|
raise ValueError(f"Unknown level: {level}")
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def py_to_sys_lvl(level_num: int) -> int:
|
||||||
|
"""
|
||||||
|
Maps a (numeric) log level as defined in Python stdlib logging module to the syslog module's log levels.
|
||||||
|
The output number corresponds to a syslog `PRI` without the enclosing angle brackets.
|
||||||
|
Even though there are more levels available to syslog, the `EMERG` (num. 0) and `NOTICE` (num. 5) levels are
|
||||||
|
omitted here, i.e. it goes straight from `INFO` (num. 6) to `WARNING` (num. 4) because there is no equivalent
|
||||||
|
in the Python logging module to `NOTICE`, and `EMERG` is unnecessary because no Python script should be able to
|
||||||
|
cause such severe problems.
|
||||||
|
"""
|
||||||
|
if level_num <= _logging.DEBUG:
|
||||||
|
return LOG_DEBUG
|
||||||
|
if level_num <= _logging.INFO:
|
||||||
|
return LOG_INFO
|
||||||
|
if level_num <= _logging.WARNING:
|
||||||
|
return LOG_WARNING
|
||||||
|
if level_num <= _logging.ERROR:
|
||||||
|
return LOG_ERR
|
||||||
|
if level_num <= _logging.CRITICAL:
|
||||||
|
return LOG_CRIT
|
||||||
|
return LOG_ALERT
|
Loading…
x
Reference in New Issue
Block a user