Make lib.App logging more customizable #55

Merged
Jan Lindemann merged 6 commits from jan/feature/20260810-make-lib-app-logging-more-customizable into master 2026-08-10 20:34:59 +02:00 AGit
3 changed files with 127 additions and 56 deletions

View file

@ -9,7 +9,19 @@ from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace
from typing import Any, cast, override, TYPE_CHECKING from typing import Any, cast, override, TYPE_CHECKING
from .AsyncRunner import AsyncRunner from .AsyncRunner import AsyncRunner
from .log import DEBUG, ERR, NOTICE, log, log_m, set_log_flags, set_log_level from .log import (
DEBUG,
ERR,
NOTICE,
LogFlag,
log,
log_m,
parse_log_flags,
parse_log_level,
set_log_flags,
set_log_level
)
from .Types import LoadTypes from .Types import LoadTypes
from .util import pretty_cmd from .util import pretty_cmd
@ -43,6 +55,30 @@ class App: # export
default = None, default = None,
) )
def _default_log_flags_env(self) -> str:
return 'JW_DEFAULT_LOG_FLAGS'
def _default_log_flags(self, default: LogFlag) -> LogFlag:
return default
def _default_log_level_env(self) -> str:
return 'JW_DEFAULT_LOG_LEVEL'
def _default_log_level(self, default: int) -> int:
return default
def _default_log_file_env(self) -> str:
return 'JW_DEFAULT_LOG_FILE'
def _default_log_file(self, default: str | None) -> str | None:
return default
def _default_show_backtrace_env(self) -> str:
return 'JW_DEFAULT_SHOW_BACKTRACE'
def _default_show_backtrace(self, default: bool) -> bool:
return default
def __init__( def __init__(
self, self,
description: str = '', description: str = '',
@ -113,20 +149,25 @@ class App: # export
self.__args: Namespace | None = None self.__args: Namespace | None = None
self.__cmdline: str | None = None self.__cmdline: str | None = None
self.__default_log_flags: str = os.getenv(
'JW_DEFAULT_LOG_FLAGS', default = 'stderr,position,prio,color' self.__default_log_flags = self._default_log_flags(
LogFlag.STDERR | LogFlag.POSITION | LogFlag.PRIO | LogFlag.COLOR
) )
self.__default_log_level: str | int | None = os.getenv( if (env := os.getenv(self._default_log_flags_env(), None)) is not None:
'JW_DEFAULT_LOG_LEVEL', default = NOTICE self.__default_log_flags = parse_log_flags(env)
)
self.__default_log_file: str | None = os.getenv( self.__default_log_level = self._default_log_level(NOTICE)
'JW_DEFAULT_LOG_FILE', default = None if (env := os.getenv(self._default_log_level_env(), None)) is not None:
) self.__default_log_level = parse_log_level(env)
backtrace: str | bool = os.getenv('JW_DEFAULT_SHOW_BACKTRACE', False)
self.__back_trace = isinstance(backtrace, str) and backtrace.lower() in { self.__default_log_file = self._default_log_file(None)
'1', if (env := os.getenv(self._default_log_file_env(), None)) is not None:
'true', self.__default_log_file = env
}
self.__back_trace = self._default_show_backtrace(False)
if (env := os.getenv(self._default_show_backtrace_env())) is not None:
self.__back_trace = env.lower() in ['1', 'true']
set_log_flags(self.__default_log_flags) set_log_flags(self.__default_log_flags)
set_log_level(self.__default_log_level) set_log_level(self.__default_log_level)

View file

@ -5,9 +5,11 @@ import re
import sys import sys
import syslog import syslog
from enum import Flag, auto
from datetime import datetime from datetime import datetime
from os.path import basename from os.path import basename
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, cast, override
if TYPE_CHECKING: if TYPE_CHECKING:
import io import io
@ -81,16 +83,44 @@ CONSOLE_FONT_WHITE = '\033[37m'
CONSOLE_FONT_BLINK = '\033[5m' CONSOLE_FONT_BLINK = '\033[5m'
CONSOLE_FONT_OFF = '\033[m' CONSOLE_FONT_OFF = '\033[m'
f_position = 'position' # -- ytools compatible log flags
f_module = 'module' #syslog
f_date = 'date' #console
f_stderr = 'stderr' #color
f_stdout = 'stdout' #prio
f_prio = 'prio' #position
f_color = 'color' #ide
f_default = [ f_position, f_stderr, f_prio, f_color ] #trace_rename_thread_to_shorter
#trace_rename_thread_to_longer
#trace_inout
#skip_openlog
#id
#date
#pid
#highlight_first_error
class LogFlag(Flag):
POSITION = auto()
MODULE = auto()
DATE = auto()
STDERR = auto()
STDOUT = auto()
PRIO = auto()
COLOR = auto()
@override
def __str__(self) -> str:
def _get_name(f: LogFlag) -> str:
return f.name or ''
return ','.join(
f.name.lower()
for f in sorted(self, key = _get_name)
if f.name
)
_flags = LogFlag.POSITION | LogFlag.STDERR | LogFlag.PRIO | LogFlag.COLOR
_flags = set(f_default)
_log_prefix = '' _log_prefix = ''
_clean_log_prefix = '' _clean_log_prefix = ''
_file_name_len = 20 _file_name_len = 20
@ -181,6 +211,8 @@ def log_m(prio: int, *args: Any, **kwargs: Any) -> None: # export
if isinstance(a, list): if isinstance(a, list):
margs += '\n'.join([str(elem) for elem in a]) margs += '\n'.join([str(elem) for elem in a])
continue continue
if a == '':
continue
margs += ' ' + str(a) margs += ' ' + str(a)
if 'caller' not in kwargs: if 'caller' not in kwargs:
caller = get_caller_pos(1) caller = get_caller_pos(1)
@ -204,43 +236,44 @@ def log( # export
color_on = '' color_on = ''
color_off = '' color_off = ''
if f_date in _flags: if LogFlag.DATE & _flags:
msg += datetime.now().strftime("%b %d %H:%M:%S.%f ") msg += datetime.now().strftime("%b %d %H:%M:%S.%f") + ' '
if f_prio in _flags: if LogFlag.PRIO & _flags:
msg += _short_prio_str[prio] + ' ' msg += _short_prio_str[prio] + ' '
if f_position in _flags: if LogFlag.POSITION & _flags:
if 'caller' in kwargs: if 'caller' in kwargs:
mod, name, line = kwargs['caller'] mod, name, line = kwargs['caller']
else: else:
mod, name, line = get_caller_pos(1) mod, name, line = get_caller_pos(1)
if f_module in _flags: if LogFlag.MODULE & _flags:
msg += pad(mod, _module_name_len) msg += pad(mod, _module_name_len)
msg += pad(name, _file_name_len) + '[' + pad(str(line), 4, True) + ']' msg += pad(name, _file_name_len) + '[' + pad(str(line), 4, True) + ']'
if f_color in _flags: if LogFlag.COLOR & _flags:
color_on, color_off = console_color_chars(prio) color_on, color_off = console_color_chars(prio)
margs = '' margs = ''
if len(args): if len(args):
for a in args: margs = ' '.join(str(a) for a in args)
margs += ' ' + str(a)
if only_printable: if only_printable:
margs = _special_char_regex.sub( margs = _special_char_regex.sub(
lambda mo: _special_chars[mo.string[mo.start():mo.end()]], margs lambda mo: _special_chars[mo.string[mo.start():mo.end()]], margs
) )
margs = re.sub('[\x01-\x1f]', '.', margs) margs = re.sub('[\x01-\x1f]', '.', margs)
if msg and not msg.endswith(' '):
margs = ' ' + margs
for file in _log_file_streams: for file in _log_file_streams:
print(msg + _clean_log_prefix + margs, file = file) print(msg + _clean_log_prefix + margs, file = file)
msg += _log_prefix msg += _log_prefix
if not len(msg): if not len(msg) and not len(margs):
return return
if len(margs): if len(margs):
@ -252,10 +285,10 @@ def log( # export
elif _streams: elif _streams:
files = [s.stream for s in _streams.values()] files = [s.stream for s in _streams.values()]
else: else:
if f_stdout in _flags: if LogFlag.STDOUT & _flags:
files.append(sys.stdout) files.append(sys.stdout)
if f_stderr in _flags: if LogFlag.STDERR & _flags:
files.append(sys.stderr) files.append(sys.stderr)
if not len(files): if not len(files):
@ -307,30 +340,27 @@ def set_log_level(level: str | int | None = None) -> int:
def set_level(level: str | int | None = None) -> int: # export def set_level(level: str | int | None = None) -> int: # export
return set_log_level(level) return set_log_level(level)
def set_flags(flags: str | None) -> str: # export def parse_flags(flags: str) -> LogFlag:
global _flags ret: LogFlag = LogFlag(0)
ret = ','.join(_flags) s = set(flags.split(','))
if flags is not None: for f in LogFlag:
_flags = set(flags.split(',')) assert f.name is not None
if f.name.lower() in s:
ret |= f
return ret return ret
def set_log_flags(flags: str) -> str: parse_log_flags = parse_flags
return set_flags(flags)
#syslog def set_flags(flags: str | LogFlag | None) -> LogFlag: # export
#console global _flags
#color if isinstance(flags, str):
#prio flags = parse_flags(flags)
#position ret = _flags
#ide if flags is not None:
#trace_rename_thread_to_shorter _flags = flags
#trace_rename_thread_to_longer return ret
#trace_inout
#skip_openlog set_log_flags = set_flags
#id
#date
#pid
#highlight_first_error
def append_to_prefix(prefix: str) -> str: # export def append_to_prefix(prefix: str) -> str: # export
global _log_prefix global _log_prefix

View file

@ -11,7 +11,7 @@ jw-pkg swiss army knife
options: options:
--log-flags LOG_FLAGS --log-flags LOG_FLAGS
Log flags (default: stderr,position,prio,color) Log flags (default: color,position,prio,stderr)
--log-level LOG_LEVEL --log-level LOG_LEVEL
Log level (default: 5) Log level (default: 5)
--log-file LOG_FILE Log file (default: None) --log-file LOG_FILE Log file (default: None)