logging - Context-based logging¶
Changed in version 2.0.0.
Overview¶
This module provides context-based logging system built on top of standard logging module.
It also solves the common logging management problem when various modules use hard-coded
separate loggers, and provides several types of message wrappers that allow lazy message
interpolation using f-string, brace (str.format) or dollar (string.Template) formats.
The context-based logging:
Adds context information into
LogRecord, that could be used in logging entry formats.Allows assignment of loggers to specific contexts.
Basics¶
Normally, when you want to use a logger, you call the logging.getLogger function and
pass it a logger name (or not to get the root logger). The common (and recommended) practice
is to use getLogger(__name__) which often leads to logging configuration problems as complex
applications with many modules (including used libraries) may create complex logger hierarchy.
Our logging module solves the problem by replacing the logger name with agent identification.
The agent is typically an unit of code that works in specific execution contexts. For
example a code that process client request in web application (where request is the context),
or executes SQL command (the context could be a database connection). In most cases, the
agent is an instance of some class.
So, from user’s perspective, the context logging is used similarly to normal Python logging -
but you pass the agent identification instead logger name to get_logger function. If agent
identification is a string, it’s used as is. If it’s an object, it uses value of its _agent_name_
attribute if defined, otherwise it uses name in “MODULE_NAME.CLASS_QUALNAME” format. If
_agent_name_ value is not a string, it’s converted to string.
The typical usage pattern inside a class is therefore:
logger = get_logger(self)
or for direct use:
get_logger(self).debug("message")
The get_logger also has an optional topic string parameter that could be used to
differentiate between various logging “streams”. For example the trace module
uses context logging with topic “trace”, so it’s possible to configure the logging system
to handle “trace” output in specific way.
The underlying machinery behind get_logger function maps the agent and its context to
particular Logger, and returns a ContextLoggerAdapter that you can use as normal
logger. This adapter is responsible to add context information into LogRecord.
Context information¶
The conext information added by ContextLoggerAdapter into LogRecord consists
from next items:
- agent:
String representation of the agent identification described above.
- context:
Agent context that could be defined via
log_contextattribute onagentinstance, or by assigning its value directly toextra['context']on adapter returned byget_logger()function.- domain:
A name assigned to a group of agents (more about that later).
- topic:
Name of a logging stream.
They could be used in Formatter templates. If you want to use logging that combines
normal and context logging, it’s necessary to assign ContextFilter to your logging.Handler
to add (empty) context information into LogRecords that are produced by normal loggers.
LoggingManager¶
The firebird.base.logging module defines a global LoggingManager instance logging_manager
that manages several mappings and implements the get_logger method.
Some methods are also provided as global functions for conveniense: get_logger,
set_agent_mapping, set_domain_mapping and get_agent_name.
Mappings and Logger names¶
The Logger wrapped by the get_logger function is determined by applying the
values of several parameters to the logger name format. These parameters are:
Domain: String used to group output from agents.
Topic: String identification of particular logging stream.
The logger name format is a list that can contain any number of string values and at most
one occurrence of DOMAIN or TOPIC enum values. Empty strings are removed.
The final Logger name is constructed by joining elements of this list with
dots, and with sentinels replaced with domain and topic names.
For example, if values are defined as:
logger_fmt = ['app', DOMAIN, TOPIC]
domain = 'database'
topic = 'trace'
the Logger name will be: “app.database.trace”
The logger name format is defined in LoggingManager.logger_fmt property, and it’s an
empty list by default, which means that get_logger function always maps to root logger.
The domain is determined from agent passed to get_logger. You can use set_domain_mapping
to assign agent identifications to particular domain. The agents that are not assigned to domain
belong to default domain specifid in LoggingManager.default_domain, which is None by default.
It’s also possible to change agent identification used for logger name mapping porposes to
different value with set_agent_mapping function.
Lazy Formatting Messages¶
This module also provides message wrapper classes (FStrMessage, BraceMessage,
DollarMessage) that defer string formatting until the log record is actually
processed by a handler. This avoids the performance cost of formatting messages
that might be filtered out due to log levels.
Basic Setup Example¶
import logging
from firebird.base.logging import (
get_logger, LogLevel, ContextFilter, logging_manager,
DOMAIN, TOPIC # For logger_fmt
)
# 1. Configure standard logging (handlers, formatters)
log_format = "[%(levelname)-8s] %(asctime)s %(name)s (%(agent)s) - %(message)s"
formatter = logging.Formatter(log_format)
handler = logging.StreamHandler()
handler.setFormatter(formatter)
# 2. Add ContextFilter to handler(s) to ensure context fields exist
handler.addFilter(ContextFilter())
# 3. Get the root logger or specific standard loggers and add the handler
root_logger = logging.getLogger()
root_logger.addHandler(handler)
root_logger.setLevel(LogLevel.DEBUG) # Use LogLevel enum or logging constants
# 4. (Optional) Configure logging_manager mappings
logging_manager.logger_fmt = ['app', DOMAIN, TOPIC] # Logger name format
logging_manager.default_domain = 'web' # Default domain if not mapped
logging_manager.set_domain_mapping('db', ['myapp.database.Connection']) # Map agent to domain
# 5. Use in your code
class RequestHandler:
_agent_name_ = 'myapp.web.RequestHandler' # Optional explicit agent name
log_context = None # Can be set per request, e.g., client IP
def handle(self, request_id):
self.log_context = f"ReqID:{request_id}"
logger = get_logger(self, topic='requests') # Get context logger
logger.info("Handling request...")
# ... processing ...
logger.debug("Request handled successfully.")
class DbConnection:
_agent_name_ = 'myapp.database.Connection'
log_context = None # e.g., DB user
def query(self, sql):
self.log_context = "user:admin"
logger = get_logger(self) # Use default topic (None)
logger.debug("Executing query: %s", sql) # Standard formatting works too
# ... execute ...
# --- Execution ---
handler_instance = RequestHandler()
db_conn = DbConnection()
handler_instance.handle("12345")
db_conn.query("SELECT * FROM T")
# --- Example Output ---
# [INFO ] 2023-10-27... app.web.requests (myapp.web.RequestHandler) - Handling request...
# [DEBUG ] 2023-10-27... app.web.requests (myapp.web.RequestHandler) - Request handled successfully.
# [DEBUG ] 2023-10-27... app.db (myapp.database.Connection) - Executing query: SELECT * FROM T
Enums & Flags¶
- class firebird.base.logging.FormatElement(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)[source]¶
Bases:
EnumSentinels used within
LoggingManager.logger_fmtlist.- DOMAIN = 1¶
- TOPIC = 2¶
- class firebird.base.logging.LogLevel(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)[source]¶
Bases:
IntEnumMirrors standard
logginglevels for convenience and type hinting.Provides symbolic names (e.g.,
LogLevel.DEBUG) corresponding to the integer values used by the standardloggingmodule (logging.DEBUG).- CRITICAL = 50¶
- DEBUG = 10¶
- ERROR = 40¶
- FATAL = 50¶
- INFO = 20¶
- NOTSET = 0¶
- WARN = 30¶
- WARNING = 30¶
Constants¶
- firebird.base.logging.DOMAIN: FormatElement = FormatElement.DOMAIN¶
Sentinel representing the domain element in
LoggingManager.logger_fmt.
- firebird.base.logging.TOPIC: FormatElement = FormatElement.TOPIC¶
Sentinel representing the topic element in
LoggingManager.logger_fmt.
Functions¶
- firebird.base.logging.get_logger(agent: Any, topic: str | None = None) ContextLoggerAdapter¶
Shortcut to global
LoggingManager.get_loggerfunction.- Parameters:
- Return type:
- firebird.base.logging.set_domain_mapping(domain: str, agents: Iterable[str] | str | None, *, replace: bool = False) None¶
Shortcut to global
LoggingManager.set_domain_mappingfunction.
Adapters and Filters¶
- class firebird.base.logging.ContextLoggerAdapter(logger, domain: str | None, topic: str | None, agent: Any, agent_name: str)[source]¶
Bases:
LoggerAdapterLogger adapter injecting context (
domain,topic,agent,context) info.Wraps a standard
logging.Logger. When a logging method (e.g.,info,debug) is called, it adds the context information into theextradictionary, making it available as attributes on the resultinglogging.LogRecord.- Parameters:
logger – The standard
logging.Loggerinstance to wrap.domain (str | None) – Context Domain name (or None).
topic (str | None) – Context Topic name (or None).
agent (Any) – The original agent object or string passed to
get_logger.agent_name (str) – The resolved string name for the agent.
- process(msg: Any, kwargs: dict[str, Any]) tuple[Any, dict[str, Any]][source]¶
Process the logging message and keyword arguments passed in to a logging call to insert contextual information.
Ensures
self.extracontainsdomain,topic, andagent(from init).Adds
contexttoself.extra, taking it fromself.agent.log_contextif available, otherwiseNone.Merges the adapter’s
extradictionary with anyextradictionary passed inkwargs, giving precedence to keys inkwargs['extra'].Stores the final merged
extradictionary intokwargs['extra'].
- class firebird.base.logging.ContextFilter(name='')[source]¶
Bases:
FilterLogging filter ensuring context fields exist on
LogRecordinstances.Checks for
domain,topic,agent, andcontextattributes on each log record. If any are missing (e.g., for records from standard loggers not usingContextLoggerAdapter), it adds them with a value ofNone.- Usage:
Attach an instance of this filter to
logging.Handlerobjects to ensure formatters expecting these fields do not raiseAttributeError.
Example:
handler = logging.StreamHandler() handler.addFilter(ContextFilter()) # ... add handler to logger ...
Logging manager¶
- class firebird.base.logging.LoggingManager[source]¶
Bases:
objectLogging manager.
- get_agent_name(agent: Any) str[source]¶
Determine the canonical string name for a given agent identifier.
- Parameters:
agent (Any) – Agent identifier (string, or object).
- Returns:
The resolved agent name (string).
- Return type:
Logic:
If
agentis a string, it’s used directly.If
agentis an object: - Usesagent._agent_name_if defined (converting to string if needed). - Otherwise, constructs name asmodule.ClassQualname.Applies any agent name mapping defined via
set_agent_mappingto the name determined in steps 1 or 2.Ensures the final result is a string.
Example:
> from firebird.base.logging import logging_manager > logging_manager.get_agent_name(logging_manager) 'firebird.base.logging.LoggingManager'
- get_logger(agent: Any, topic: str | None = None) ContextLoggerAdapter[source]¶
Get a ContextLoggerAdapter configured for the specified agent and topic.
This is the primary function for obtaining a logger in the context logging system. It determines the appropriate underlying
logging.Loggerbased on the agent’s domain and the topic, then wraps it in aContextLoggerAdapterto inject context information.- Parameters:
- Returns:
A
ContextLoggerAdapterinstance ready for logging.- Return type:
Process Flow:
Determine
agent_nameusingget_agent_name(agent).Determine
domainby looking upagent_namein the domain mapping, falling back toself.default_domain.Apply topic mapping to the input
topic(if any).Construct the final underlying
logging.Loggername usingself.logger_fmt, substituting the determineddomainand mappedtopic.Get/create the
logging.Loggerinstance usingself._logger_factorywith the constructed name.Create and return a
ContextLoggerAdapterwrapping the logger and carrying thedomain, mappedtopic, originalagent, andagent_name.
- get_logger_factory() Callable[source]¶
Return a callable which is used to create a Logger.
- Return type:
- reset() None[source]¶
Resets manager to “factory defaults”: no mappings, no
logger_fmtand undefineddefault_domain.- Return type:
None
- set_agent_mapping(agent: str, new_agent: str | None) None[source]¶
Sets or removes the mapping of an agent name to another name.
- Parameters:
- Return type:
None
Important
Does not validate the
new_agentvalue type, instead it’s converted to string.
- set_domain_mapping(domain: str, agents: Iterable[str] | str | None, *, replace: bool = False) None[source]¶
Sets, updates, or removes agent name mappings to a domain.
- Parameters:
- Return type:
None
Important
Passing
Nonetoagentsremoves all agent mappings for specified domain, regardless ofreplacevalue.
- set_logger_factory(factory) None[source]¶
Set a callable which is used to create a Logger.
- Parameters:
factory – The factory callable to be used to instantiate a logger.
- Return type:
None
The factory has the following signature:
factory(name, *args, **kwargs)
- set_topic_mapping(topic: str, new_topic: str | None) None[source]¶
Sets or removes the mapping of an topic name to another name.
- Parameters:
- Return type:
None
When
new_topicis a string, it mapstopictonew_topic. Empty string is likeNone.When
new_topicisNone, it removes any mapping.
Important
Does not validate the
new_topicvalue type, instead it’s converted to string.
- property default_domain: str | None¶
Default domain. Could be either a string or
None.Important
When assigned, it does not validate the value type, but converts it to string.
- property logger_fmt: list[str | FormatElement]¶
Logger format.
The list can contain any number of string values and at most one occurrence of
DOMAINorTOPICenum values. Empty strings are removed.The final
logging.Loggername is constructed by joining elements of this list with dots, and with sentinels replaced withdomainandtopicnames.Example:
logger_fmt = ['app', DOMAIN, TOPIC] domain = 'database' topic = 'trace' Logger name will be: "app.database.trace"
Messages¶
- class firebird.base.logging.FStrMessage(fmt: str, /, *args, **kwargs)[source]¶
Bases:
objectLazy logging message wrapper using f-string semantics via
eval.Defers the evaluation of the f-string until the message is actually formatted by a handler, improving performance if the message might be filtered out by log level settings.
Note
Uses
eval()internally. Ensure the format string and arguments do not contain untrusted user input.Example:
logger.debug(FStrMessage("Processing item {item_id} for user {user!r}", item_id=123, user="Alice")) # Formatting only happens if DEBUG level is enabled for the logger/handler.
- Parameters:
fmt (str) –
- class firebird.base.logging.BraceMessage(fmt: str, /, *args, **kwargs)[source]¶
Bases:
objectLazy logging message wrapper using brace (
str.format) style formatting.Defers the call to
str.format()until the message is actually formatted by a handler, improving performance for potentially filtered messages.Example:
logger.warning(BraceMessage("Connection failed: host={0}, port={1}", 'server.com', 8080)) logger.warning(BraceMessage(("Message with coordinates: ({point.x:.2f}, {point.y:.2f})", point=point))
- Parameters:
fmt (str) –
- class firebird.base.logging.DollarMessage(fmt: str, /, **kwargs)[source]¶
Bases:
objectLazy logging message wrapper using dollar (
string.Template) style formatting.Defers the substitution using
string.Templateuntil the message is actually formatted by a handler, improving performance for potentially filtered messages.Example:
from string import Template # Not strictly needed for caller logger.info(DollarMessage("Task $name completed with status $status", name='Cleanup', status='Success'))
- Parameters:
fmt (str) –
Globals¶
- firebird.base.logging.logging_manager: LoggingManager¶
Context logging manager.