286 lines
8.7 KiB
Python
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)
|