Cada registros, los loggers y los handlers tiene asociado un nivel de severidad,
Existe un método que emite registros con su respectivo nivel logger.debug(), logger.info()...
Y los logger tienen un nivel efectivo necesario.
import logging
root = logging.getLogger()
root
<RootLogger root (WARNING)>
El logger por defecto "root" tiene nivel mínimo WARNING
root.info("Hola PyConAr") # info < warning => ignorado
root.warning("Dije hola PyConAr!")
Dije hola PyConAr!
Pero también los Handler tienen nivel
otro_logger = logging.getLogger("ejemplo")
otro_logger.setLevel(logging.INFO) # nivel del logger
sh = logging.StreamHandler()
sh.setLevel(logging.WARNING) # nivel del handler
otro_logger.addHandler(sh)
# alcanza el nivel del logger pero no el del handler
otro_logger.info("Te la debo, no estoy en tema") # el registro se crea, pero no se maneja en el handler
otro_logger.warning("veniamos bien, pero pasaron cosas")
veniamos bien, pero pasaron cosas
Recordemos que un logger puede tener varios handlers
fh = logging.FileHandler('charla.log')
fh.setLevel(logging.INFO)
# le agregamos este nuevo Handler al logger que ya teniamos
otro_logger.addHandler(fh)
# esto no va a stdout pero sí al archivo
otro_logger.info("deberían tener angustia de tomar la decisión, querido Rey, de separarse de España")
!cat charla.log
deberían tener angustia de tomar la decisión, querido Rey, de separarse de España
getLogger
podemos darle un nombre La convención es usar la jerarquía completa del módulo
logger = logging.getLogger(__name__)
# reusamos la config del namespace "ejemplo" que tenia `otro_logger`
logger = logging.getLogger('ejemplo.hijo')
logger.info("Hay lugares donde falta el agua, y lugares donde sobra")
!cat charla.log
deberían tener angustia de tomar la decisión, querido Rey, de separarse de España Hay lugares donde falta el agua, y lugares donde sobra
propagate=True
pasa el registro a "loggers padres".
Transformar o descartar registros
class ShortMsgFilter(logging.Filter):
def filter(self, record):
msg = record.msg
if isinstance(msg, str):
return len(msg) > 10
return False
logger.addFilter(ShortMsgFilter())
logger.warning("hola")
logger.warning("buscaremos otros jueces que nos representen")
buscaremos otros jueces que nos representen
import logging.config
DEFAULT_LOGGING = {
'version': 1,
'disable_existing_loggers': True,
'loggers': {
'ejemplo': {
'level': 'DEBUG',
'handlers': ["console_handler"]
},
},
"handlers": {
'console_handler': {
'level': 'DEBUG',
'formatter': 'info',
'class': 'logging.StreamHandler',
},
},
"formatters": {
"info": {
'format': "%(asctime)s [%(levelname)s] %(message)s"
},
}
}
logging.config.dictConfig(DEFAULT_LOGGING)
logger = logging.getLogger("ejemplo")
logger.debug("No logramos lograr ese logro")
2020-10-26 02:02:34,519 [DEBUG] No logramos lograr ese logro
Structlog
es a logging
lo que requests
a httplib
It makes logging in Python less painful and more powerful by adding structure to your log entries.
import structlog
# drop-in replacement!
logger = structlog.get_logger()
logger.info("El oceáno es muy grande y el submarino es muy pequeño")
2020-10-26 11:19.00 El oceáno es muy grande y el submarino es muy pequeño
(Ejemplos reales del código de mi trabajo, permiso)
logger.info(
"Reserving inventory for (%s, %d, %d, %d, %d, %d)",
sku,
warehouse_id,
account_id,
priority,
available_inventory,
required_quantity,
)
logger.warning(
"LineItem points to an non-existent SKU\n"
"LineItem (id, sku, account_id): (%d, %s, %s)\n"
"Order (id, order_number): (%d, %s)",
li.id,
li.sku,
li.account_id,
li.order.id,
li.order.order_number,
)
logger.info(
"kit_allocations: %r",
[
{
"sku": li.get("sku"),
"quantity": li.get("quantity"),
"quantity_pending_fulfillment": li.get("quantity_pending_fulfillment"),
"quantity_pending_allocation": li.get("quantity_pending_allocation"),
}
for li in kit_allocations
],
)
La manera de structlog
es la manera pythonica
: keyword arguments!
logger.error("Iniciamos conversaciones", interlocutor="FMI", tipo_acuerdo="stand-by", monto=52e9, moneda="usd")
2020-10-26 11:19.09 Iniciamos conversaciones interlocutor=FMI moneda=usd monto=52000000000.0 tipo_acuerdo=stand-by
structlog.configure
En structlog
los formatters
y filters
se unifican en processors,
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer()
]
)
logger = structlog.get_logger()
logger.warning("todas las tormentas juntas", tipo_conferencia="grabada", lira_turca=25.3)
{"tipo_conferencia": "grabada", "lira_turca": 25.3, "event": "todas las tormentas juntas", "level": "warning", "timestamp": "2020-10-26T04:10:34.829984Z"}
JSONRenderer
usa json.dumps
by default (configurable) pero es inteligente
class Periodo:
def __repr__(self):
return "50 años"
p = Periodo()
import json
json.dumps(p)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-14-b4c9add0615f> in <module> 1 import json ----> 2 json.dumps(p) /usr/lib/python3.7/json/__init__.py in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw) 229 cls is None and indent is None and separators is None and 230 default is None and not sort_keys and not kw): --> 231 return _default_encoder.encode(obj) 232 if cls is None: 233 cls = JSONEncoder /usr/lib/python3.7/json/encoder.py in encode(self, o) 197 # exceptions aren't as detailed. The list call should be roughly 198 # equivalent to the PySequence_Fast that ''.join() would do. --> 199 chunks = self.iterencode(o, _one_shot=True) 200 if not isinstance(chunks, (list, tuple)): 201 chunks = list(chunks) /usr/lib/python3.7/json/encoder.py in iterencode(self, o, _one_shot) 255 self.key_separator, self.item_separator, self.sort_keys, 256 self.skipkeys, _one_shot) --> 257 return _iterencode(o, 0) 258 259 def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, /usr/lib/python3.7/json/encoder.py in default(self, o) 177 178 """ --> 179 raise TypeError(f'Object of type {o.__class__.__name__} ' 180 f'is not JSON serializable') 181 TypeError: Object of type Periodo is not JSON serializable
logger.info("el mejor equipo", periodo=p)
{"periodo": "50 a\u00f1os", "event": "el mejor equipo", "level": "info", "timestamp": "2020-10-26T04:20:58.496938Z"}
class PeriodoReal(C):
def __structlog__(self):
return "hasta el 11 de agosto"
logger.info("gobierno economico", hasta=PeriodoReal())
{"hasta": "hasta el 11 de agosto", "event": "gobierno economico", "level": "info", "timestamp": "2020-10-26T04:22:28.040188Z"}
structlog
tiene procesadores "amigables" con el desarrollo
import structlog
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M.%S"),
structlog.dev.ConsoleRenderer()
]
)
logger = structlog.get_logger()
logger.info("es todo mentira", tema="autopistas")
2020-10-26 14:40.10 [info ] es todo mentira tema=autopistas
**Ejemplo de Cloudwatch
%%writefile a_module.py
from structlog import get_logger
logger = get_logger()
def some_function():
logger.error("escuchas", tipo="ilegales", respuesta="cuentapropistas")
Overwriting a_module.py
import sys
import logging
import structlog
from a_module import some_function
logging.basicConfig(
format="%(message)s", stream=sys.stdout, level=logging.INFO
)
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.KeyValueRenderer(
key_order=["timestamp", "event"]
),
],
context_class=structlog.threadlocal.wrap_dict(dict),
logger_factory=structlog.stdlib.LoggerFactory(),
)
def entrevista():
log = logger.new(periodista="amigo") # set a new context. any futher event from the logger will get x for free
log.warning("inicio entrevista")
log = log.bind(sobre=4) # update the context
some_function()
log = log.unbind("periodista")
log.warning("final entrevista")
entrevista()
timestamp='2020-10-26T04:58:01.123994Z' event='inicio entrevista' periodista='amigo' timestamp='2020-10-26T04:58:01.125263Z' event='escuchas' periodista='amigo' sobre=4 tipo='ilegales' respuesta='cuentapropistas' timestamp='2020-10-26T04:58:01.126142Z' event='final entrevista' sobre=4
A veces loggeamos mucho pero no siempre efectivamente.
Es barato pero no gratis.
Además de timestamp, agregar info de trazabilidad: request_id, thread_id, user_id, etc.
Structlog es fácil, poderoso y pythónico: úsenlo!
Configuración
stdlib.LoggerFactory