# logger_custom_formatter.py
import logging
from logging import LogRecord
class CustomFormatter(logging.Formatter):
= "\x1b[38;20m"
grey = "\033[92m"
green = "\x1b[33;20m"
yellow = "\x1b[31;20m"
red = "\x1b[31;1m"
bold_red = "\x1b[0m"
reset = "%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
my_format
= {
dict_formats + my_format + reset,
logging.DEBUG: grey + my_format + reset,
logging.INFO: green + my_format + reset,
logging.WARNING: yellow + my_format + reset,
logging.ERROR: red + my_format + reset,
logging.CRITICAL: bold_red
}
def format(self, record: LogRecord) -> str:
= self.dict_formats.get(record.levelno)
log_format = logging.Formatter(log_format)
formatter return formatter.format(record)
def get_logger(filename: str) -> logging.Logger:
"""logging factory"""
= filename.split(".")[-1]
filename if filename in logging.root.manager.loggerDict:
# avoid logger duplicates
return logging.getLogger(filename)
= logging.getLogger(filename)
logger
logger.setLevel(logging.INFO)= logging.StreamHandler()
ch
ch.setLevel(logging.DEBUG)
ch.setFormatter(CustomFormatter())
logger.addHandler(ch)return logger
This is another blog post aiming at the best approximation of coding best practices I can produce, and it goes beyond the topic of geospatial data science to which this blog is devoted.
Inspired by this stack-overflow question stack-overflow question, this blog post delineates how to customise and visualise log messages, and to prompt them into your jupyter notebook cells.
Problem
To see what is happening under the hood, adding print
messages in the sourced code, or adding a breakpoint for line-by-line debugging all valid options. At least until the print
messages are of general use and the code is intended for production.
When approaching production, print messages are no longer a viable option, but the logging messages taking their place would not be prompted to the jupyter notebook by default. One of my early solutions to this problem was to wrap the python logging
library in a custom PrintAndLog
class that would prints and log every message.
If this sounded like a terrible idea, it is because it was a terrible idea. Beside the poor design, and the different goals of logging and of printing, it would cause duplicate logs when the code runs on platforms that also logs the printout. Moreover the printed messages would not be associated to any hierarchy level (i.e. DEBUG
, INFO
, WARNING
, ERROR
or CRITICAL
).
Proposed solution
Following this stackoverflow message and this gist, the proposed solution is a logger factory that wraps logging.Formatter
to customise colors and formats, as well as to reduce logger instances duplication.
Now, to have the custom formatter, you can create a logger within the jupyter notebook with:
!export MallocStackLoggingNoCompact=1 # more about this line in the troubleshooting
= get_logger(__name__) logger
"logging some information") logger.info(
2025-07-15 18:53:48,773 - __main__ - INFO - logging some information (2803960019.py:1)
"logging an error") logger.error(
2025-07-15 18:53:48,778 - __main__ - ERROR - logging an error (2599693193.py:1)
Without the custom get_logger
, logging can be achieved directly with the standard log, still taking care to passing __name__
in the getLogger
function, instead of __file__
.
import logging
= logging.getLogger(__name__)
logger "warning test") logger.warning(
Tough this solution would result in a message with identical text formatting as writing print("warning test")
in the jupyter notebook.
You can check that the solution works with loggers, creating an external module where to import the logger factory:
# external_module.py
import numpy as np
from logger_custom_formatter import get_logger
= get_logger(__file__)
logger
def special_division(a: float, b: float) -> float:
try:
return a / b
except ZeroDivisionError:
if a == 0:
"Zero numerator and denominator found. Returning zero")
logger.warning(return 0
if a > 0:
"Zero denominator found. Returning + infinity")
logger.warning(return np.inf
else:
"Zero denominator found. Returning - infinity")
logger.warning(return -1 * np.inf
and then calling it from the jupyter notebook check that is correctly raised:
from external_module import special_division
7, 0) special_division(
2025-07-15 18:53:48,837 - py - WARNING - Zero denominator found. Returning + infinity (external_module.py:15)
inf
Troubleshooting
If on MAC Sonoma
To allow logs to be shown to jupyter notebooks, you may need to set MallocStackLoggingNoCompact=1
. This can be achieved directly within the jupyter notebook with the !
command to call the shell.
!export MallocStackLoggingNoCompact=1
If using boto3
If you are using boto3, you can avoid very long and repetitive messages about found credentials
with:
import boto3
="botocore.credentials", level=logging.ERROR) boto3.set_stream_logger(name