defl/defl/_logger_.py
2025-04-28 14:44:03 -04:00

286 lines
8.7 KiB
Python

#!/usr/bin/env python
# use sydlib logger backend
# https://github.com/mCodingLLC/VideosSampleCode
# https://github.com/mCodingLLC/VideosSampleCode/tree/master/videos/135_modern_logging
# https://www.youtube.com/watch?v=9L77QExPmI0
# TODO use default python logging with rich handler
# TODO disable log from files based on regex
# TODO if debug and thread then add thread id to msg
# TODO create color object for each output
# TODO supress file config option
# TODO contex manager to add prefix to log line (i.e. analyzeDispatch to differentiate services)
import inspect
import io
import re
import sys, shlex
from dataclasses import dataclass, field
from typing import Any, Callable
from ._string_ import cottonCandyStr, dictToStr
from ._ansii_ import cl
from ._obj_ import dictToObj
from ._jq_ import jdumps
from ._shell_ import shlexQuoteList
from ._typing_ import *
from ._rich_ import *
from ._regexPattern_ import RegexPattern
@dataclass(slots=True, kw_only=True, frozen=False)
class LogCast:
@staticmethod
def cast_json(
item: Any,
c: bool = False, # color
k: bool = False, # compact
s: bool = False, # sort key
S: bool = False, # sort value
t: bool = False, # include type
) -> str:
assert not (s and S)
if s:
item = dict(sorted(item.items(), key=itemgetter(0)))
if S:
item = dict(sorted(item.items(), key=itemgetter(1)))
return jdumps(item, colorize=c, compact=k, incType=t)
@staticmethod
def cast_rich(item: Any) -> str:
return richRenderNoWrap(item)
@staticmethod
def cast_dict(item: dict) -> str:
return dictToStr(item, k=cl.green, v=cl.blu, join=' ', joinItem='=')
@staticmethod
def cast_shell(item: Any, c: bool = True) -> str:
if isinstance(item, str):
item = shlex.quote(item)
elif isinstance(item, Iterable):
if c:
item = cottonCandyStr(item, shellQuote=True)
else:
item = shlexQuoteList(item)
else:
raise ValueError
return item
ShortMap: ClassVar[dict] = {
'j': cast_json,
's': cast_shell,
'r': cast_rich,
'd': cast_dict,
'x': N,
}
def _strToTgt(tgt: str):
if tgt in ['out', 'o', 1]:
return sys.stdout
elif tgt in ['err', 'e', 2]:
return sys.stderr
else:
raise ValueError(tgt)
def _logWriter(
*message,
tgt: io.IOBase,
t: io.IOBase = N,
lineInfo: bool,
color: str,
start: str = cl.startOfLine + cl.clearLine,
end: str = '\n',
frameBackBefore: int = 0,
frameBackAfter: int = 0,
contextMessage: str = N,
rich: bool = F,
r: bool = F,
):
rich = rich or r
if t:
tgt = t
if not hasattr(tgt, 'write'):
tgt = _strToTgt(tgt)
callLocation = ''
frame = N
if lineInfo:
frame = inspect.currentframe().f_back.f_back
# TODO use stackLocalValue
lif = '__logIgnoreFrame__'
for i in range(frameBackBefore):
frame = frame.f_back
while frame.f_code.co_filename.startswith('<') or (lif in frame.f_locals and frame.f_locals[lif]):
frame = frame.f_back
for i in range(frameBackAfter):
frame = frame.f_back
filename = re.sub(r'.*\/', '', frame.f_code.co_filename)
frameStr = f'{filename}:{frame.f_code.co_name}:{frame.f_lineno}'
callLocation = f'{cl.ul}{color}[{frameStr}]{cl.r} '
message = list(message)
for i, msg in enumerate(message):
if callable(msg) and hasattr(msg, '__name__') and msg.__name__ == '<lambda>':
frame = inspect.currentframe().f_back.f_back if frame is N else frame
param = inspect.signature(msg).parameters
assert len(param) == 1
firstParamName = list(param.keys())[0]
firstParamName[0]
values = {k: True for k in firstParamName[1:]}
otd: AnonBase = dictToObj(frame.f_locals)
res = msg(otd)
castType = LogCast.ShortMap[firstParamName[0]]
if castType:
res = castType(res, **values)
message[i] = str(res).strip('\n')
elif isinstance(msg, Exception):
message[i] = msg.__class__.__name__
if rich:
message[i] = richRenderNoWrap(message[i], highlight=F)
elif i == 0 and color and len(message) > 1:
message[0] = f'{color}{message[0]}{cl.r}'
if contextMessage:
message.insert(0, f'{cl.cyan}[{contextMessage}]{cl.r}')
message = ' '.join([str(x) for x in message])
if callLocation:
message = [f'{start}{callLocation}{x}{cl.r}' for x in message.split('\n')]
# TODO if len(message) > 1 # add overline and underline to box in
message = '\n'.join(message)
message = start + message + end
message = RegexPattern.ansii.sub('', message) if not color else message
tgt.write(message)
tgt.flush()
# with console.capture() as capture:
# console.print("[bold red]Hello[/] World")
# str_output = capture.get()
@dataclass(slots=T, kw_only=T, frozen=F)
class _Log:
name: str
color: UT | str = field(default=U, repr=F)
lineInfo: UT | bool = U
enabled: UT | bool = U
tgt: UT | io.IOBase = U
def write(_, *args, **kargs) -> Callable:
kargs = {'color': _.color, 'lineInfo': _.lineInfo, 'tgt': _.tgt} | kargs
assert U not in kargs.values(), str(_)
if _.enabled:
return _logWriter(*args, **kargs)
def toDict(_):
a = {k: getattr(_, k) for k in _.__slots__}
a['tgt'] = _.tgt.name
a['color'] = repr(_.color)
return a
@dataclass(slots=T, kw_only=T, frozen=F)
class LogValMap(dict):
error: Any = U
warning: Any = U
info: Any = U
debug: Any = U
@classmethod
def From(cls, val: Any, typ: type) -> Self:
if isinstance(val, cls):
return val
elif isinstance(val, Mapping):
assert all(inst(x, typ | UT) for x in val.values())
return cls(**val)
else:
assert inst(val, typ | UT)
return cls(error=val, warning=val, info=val, debug=val)
def __getitem__(_, k):
return getattr(_, k)
@dataclass(slots=T, kw_only=T, frozen=F)
class Logger:
error: Callable = _logWriter
warning: Callable = _logWriter
info: Callable = _logWriter
# TODO log tags to enable/disable with env var
debug: Callable = _logWriter
_meta: dict = field(default_factory=dict, kw_only=T, repr=T)
@property
def w(_) -> Callable:
return _.warning
@property
def i(_) -> Callable:
return _.info
@property
def d(_) -> Callable:
return _.debug
@property
def e(_) -> Callable:
return _.error
def setLevel(
_,
lvl: str,
colorMap: LogValMap[bool] = U,
lineInfoMap: LogValMap[bool] = U,
tgtMap: LogValMap[io.IOBase] = U,
):
levels = {k[0]: _Log(name=k) for k in ['error', 'warning', 'info', 'debug']}
enabled = list(levels.values())
if lvl:
lvl = levels[lvl[0]]
enabled = enabled[: enabled.index(lvl) + 1]
mapp = {
'lineInfo': LogValMap.From(lineInfoMap, typ=bool),
'tgt': LogValMap.From(tgtMap, typ=io.IOBase),
'color': LogValMap.From(colorMap, typ=str),
}
for attr, valMap in mapp.items():
for __, log in levels.items():
setattr(log, attr, valMap[log.name])
defaultColor = {'i': cl.mag, 'e': cl.red, 'w': cl.yel, 'd': cl.cyn}
for k, log in levels.items():
log.enabled = T if (lvl is None or log in enabled) else F
if log.tgt is U:
if k == 'i' and levels['d'].enabled is U:
log.tgt = sys.stdout
else:
log.tgt = sys.stderr
if log.color is U:
# sys.stderr.write(f'{log.tgt} {log.tgt.isatty()} ==========\n')
log.color = defaultColor.get(k) if log.tgt.isatty() else ''
setattr(_, log.name, log.write)
_._meta[k] = log
for k, log in levels.items():
if log.lineInfo is U:
log.lineInfo = levels['d'].enabled is T
return _
def toDict(_):
return {k: v.toDict() for k, v in _._meta.items()}
log = Logger()
# log.setLevel(lvl=None, colorMap='', lineInfoMap=T, tgtMap=sys.stderr)
log.setLevel(lvl=None, tgtMap=sys.stderr)
def depricateWarning(*args, frameBack=1):
import inspect
funcName = inspect.currentframe().f_back.f_code.co_name
log.warning('Deprication warning', funcName, *args, lineInfo=True, frameBackBefore=1 + frameBack)