dylanglenister
Updating file path information
d0aabb7
# src/utils/logger.py
import inspect
import logging
import sys
# A more informative default format, including the logger's name and a custom tag.
_DEFAULT_FORMAT = "%(asctime)s — %(levelname)s \t[%(name)s%(tag)s] \t%(message)s"
_DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
class TaggedFormatter(logging.Formatter):
"""
A custom formatter that adds a default value for 'tag' if it is not present
in the LogRecord. This prevents errors if a log is generated without the
TaggedAdapter, making the logging system more robust.
"""
def __init__(
self,
fmt=_DEFAULT_FORMAT,
datefmt=_DEFAULT_DATE_FORMAT,
**kwargs
):
super().__init__(fmt, datefmt, **kwargs)
def format(self, record: logging.LogRecord) -> str:
"""
Adds a default 'tag' to the record before formatting.
"""
if not hasattr(record, 'tag'):
record.tag = ""
return super().format(record)
def setup_logging(
level: int = logging.INFO,
stream=sys.stdout
) -> None:
"""
Configures the root logger for the application.
This function should be called once at application startup. It establishes a
StreamHandler with the custom TaggedFormatter, ensuring all subsequent logs
are handled and formatted correctly.
Args:
level: The minimum logging level to output (e.g., logging.INFO).
stream: The stream where logs will be written (e.g., sys.stdout).
"""
root_logger = logging.getLogger()
# Avoid adding duplicate handlers if this function is called multiple times.
if root_logger.handlers:
return
handler = logging.StreamHandler(stream=stream)
formatter = TaggedFormatter()
handler.setFormatter(formatter)
root_logger.addHandler(handler)
root_logger.setLevel(level)
root_logger.info("Logger set up")
def logger(
tag: str | None = None,
*,
name: str | None = None
) -> logging.LoggerAdapter:
"""
Returns a logger that injects a tag into the LogRecord's context.
The custom TaggedFormatter must be active for the tag to appear in the output.
If 'name' is not provided, it defaults to the name of the calling module.
Example:
```
# In a file named 'my_app/database.py'
setup_logging()
logger = logger(tag="DATABASE") # Name will be 'my_app.database'
logger.info("Connection established.")
# Expected Output:
# 2025-09-07 22:15:30 — INFO [my_app.database:DATABASE] Connection established.
```
Args:
tag: The tag to inject into the 'extra' context of log records.
name: The name for the logger. Defaults to the calling module's name.
Returns:
A LoggerAdapter that enriches log records with the specified tag.
"""
logger_name = name
if logger_name is None:
# inspect.stack()[1] gets the frame of the caller.
# inspect.getmodule() gets the module from that frame.
# .__name__ gets the module's name.
frame = inspect.stack()[1]
module = inspect.getmodule(frame[0])
if module:
logger_name = module.__name__
else:
# Fallback if the module can't be determined
logger_name = "unknown_module"
base_logger = logging.getLogger(logger_name)
# A LoggerAdapter is the standard way to pass contextual information to loggers.
# It wraps the logger and adds the dictionary provided to the 'extra' attribute of each LogRecord.
return logging.LoggerAdapter(
base_logger,
{"tag": f":{tag}" if tag is not None else ""}
)