Denis Kataev
Tinkoff.ru
Why are they needed?
import logging
logging.info(...)import logging
logging.info(...)Writing in root logger is bad practice
logger = logging.getLogger(__name__)
def some_code():
logger.info(...)logger.info('some value: %s', value)<-""
|
o<-[a]
|
o<-[a.b]
|
o<-"a.b.c"
|
o<-"a.b.d"import logging
logging.getLogger('a.b.c')
<Logger a.b.c (WARNING)>
logging.getLogger('a.b.d')
<Logger a.b.d (WARNING)>root_logger is singleton
There are events and they pop up
How to choose
| Type of message | Level |
|---|---|
| Exception with trace | .exception |
| Logical error | .error |
| Strage or rare cases | .warning |
| Normal message | .info |
| Other noise | .debug |
<--""
Level WARNING
|
o<-[a]
|
o<-[a.b]
|
o<-"a.b.c"
| Level NOTSET so inherits level WARNING
|
o<-"a.b.d"
Level NOTSET so inherits level WARNINGDefault level for all loggers except root logger
<--""
Level WARNING
|
o<-[a]
|
o [a.b]
Level NOTSET so inherits level WARNING
Propagate OFF
|
o<-"a.b.c"
Level NOTSET so inherits level WARNING
logging.getLogger('a.b').propagate = FalseCreating your own filtering logic
def kek_filter(record: LogRecord):
return 'kek' not in record.msg
class FilterSubstringText:
def __init__(self, substring: str):
self.substring = substring
def filter(self, record: LogRecord) -> bool:
return self.substring in record.msg
logger = logging.getLogger('a.b')
logger.addFilter(kek_filter)
logger.addFilter(FilterSubstringText('elapsed'))There are events and some of them survive
logger = logging.getLogger('a')
logger.addHandler(logging.StreamHandler())
<--""
Level WARNING
|
o<--"a"
Level NOTSET so inherits level WARNING
Handler Stream <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>
|
o<--[a.b]
|
o<--"a.b.c"
Level NOTSET so inherits level WARNING
Convert LogRecord instance into text!
class RequestFormatter(logging.Formatter):
def format(self, record: LogRecord) -> str:
s = super().format(record)
value = getattr(record, 'json', None)
if value:
s += f'\njson: {pprint.pformat(value)}'
return s
h = logging.StreamHandler()
h.setFormatter(RequestFormatter())
logging.getLogger('a').addHandler(h)
Кручу-верчу сообщения вывожу
logger.info("Some text %s" % "text")
logger.info(f"Some text {text}")
logger.info("Some text %s" % "text")
logger.info(f"Some text {text}")
logger.info("Some text %s", "text")
try:
some_code()
except Exception as e:
logger.error(e)
try:
some_code()
except Exception as e:
logger.error("Some bad situation %s", e)
try:
some_code()
except Exception as e:
logger.error(e)
try:
some_code()
except Exception as e:
logger.error("Some bad situation %s", e)
try:
some_code()
except Exception:
logger.exception("Some bad situation")
logger.error(f'Some error {user_id}')
# many simmilar errors in sentry
logger.error('Some error %s', user_id)
# one error in sentry with many examplesLittle nuances that mean a lot
No handlers could be found for logger X.Y.ZKing of the handlers
King of the handlers
Sad examples:
grpc
from different libraries
log_fields = {'event': 'handle_request',
'client_text': request.get('text')}
with gelf.timelog(log_fields):
some_code(...)
...
more_code()
with gelf.timelog(log_fields, **{'a': 'b'}):
some_other_code(...)
logger.info('Handle event',
extra={'client_text': request.get('text')})
some_code(...)
...
more_code()
logger.info('Some success', extra={'a': b})we can skip the context in several ways
Wrapping logger and add dict to extra each LogRecord
logger = logging.getLogger(__name__)
adapter = logging.LoggerAdapter(logger,
{'hello': 'context'}
)
adapter.info('Hello')asyncio context example
class ContextLogRecord(logging.LogRecord):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
try:
request_start = REQUEST_START.get()
except LookupError:
REQUEST_PASSED.set(0)
self.request_passed = 0
else:
now = time.monotonic()
self.request_passed = now - request_start
logging.setLogRecordFactory(ContextLogRecord)
https://github.com/afonasev/context_logging
REQUEST_START = contextvars.ContextVar('rqst_start'),
# in middleware on request start
try:
request_start = REQUEST_START.get()
except LookupError:
REQUEST_TIME_PASSED.set(0)
else:
request_now = time.monotonic()
request_passed = request_now - request_start
REQUEST_PASSED.set(request_passed)Allows not to call logger at all
def some_code(...):
some_other_code()
if logger.isEnabledFor(logging.INFO):
logger.info('all ok')
#---------
log_enable = logger.isEnabledFor(logging.INFO)
def some_code(...):
some_other_code()
if log_enable:
logger.info('all ok')Allows not to call logger at all
def some_code(...):
some_other_code()
if __debug__:
logger.info('all ok')
"filters": {
"kafka_in_clear": {
"()": "FilterRevertSubstringText",
"substring": "in the clear"
},
class LoggingSettings(BaseSettings):
level: str = logging.getLevelName(logging.INFO)
dict_config: Dict[str, Any] = {}
class Config:
env_prefix = 'LOG_'
@validator('dict_config', whole=True)
def dict_config_validator(cls, v: Dict[str, Any]):
if not v:
return None
return DictConfigurator(v)
def setup_logging(self) -> None:
if isinstance(self.dict_config, DictConfigurator):
self.dict_config.configure()
else:
logging.basicConfig(level=self.level)
LoggingSettings().setup_logging()>>> import logging_tree
>>> logging_tree.printout()
<--""
Level WARNING
>>> import concurrent.futures
>>> logging_tree.printout()
<--""
Level WARNING
|
o<--[concurrent]
|
o<--"concurrent.futures"
Level NOTSET so inherits level WARNINGfrom loguru import logger
logger.debug("That's it, beautiful"
"and simple logging!")kataev
denis.a.kataev@gmail.com