generated from daniil-berg/boilerplate-py
	added code and readme contents
This commit is contained in:
		
							
								
								
									
										52
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								README.md
									
									
									
									
									
								
							@@ -1,10 +1,56 @@
 | 
			
		||||
# syslogformat
 | 
			
		||||
 | 
			
		||||
Python logging.Formatter class for syslog style messages
 | 
			
		||||
Python `logging.Formatter` class for syslog style messages
 | 
			
		||||
 | 
			
		||||
## 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
 | 
			
		||||
 | 
			
		||||
@@ -12,7 +58,7 @@ Python logging.Formatter class for syslog style messages
 | 
			
		||||
 | 
			
		||||
## Dependencies
 | 
			
		||||
 | 
			
		||||
Python Version ..., OS ...
 | 
			
		||||
Python Version 3
 | 
			
		||||
 | 
			
		||||
## 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
 | 
			
		||||
		Reference in New Issue
	
	Block a user