Coverage for frappe_manager / logger / contextual.py: 94%
52 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-07-02 18:13 +0530
« prev ^ index » next coverage.py v7.13.5, created at 2026-07-02 18:13 +0530
1"""
2Contextual logger adapter.
4This module provides ContextualLogger which wraps a standard Python logger
5to automatically add context information (bench name, operation, component)
6to all log messages.
7"""
9import logging
10from typing import Any
12from frappe_manager.logger.context import LoggerContext
15class ContextualLogger:
16 """
17 Adapter that wraps logging.Logger to add context to all messages.
19 Automatically prefixes all log messages with formatted context information.
20 Supports creating child loggers with extended context for nested operations.
22 Example:
23 >>> import logging
24 >>> base_logger = logging.getLogger("fm")
25 >>> context = LoggerContext(bench="mybench", operation="create")
26 >>> logger = ContextualLogger(base_logger, context)
27 >>> logger.info("Starting operation")
28 # Logs: "[bench=mybench] [op=create] Starting operation"
30 >>> child = logger.child(component="docker")
31 >>> child.info("Building containers")
32 # Logs: "[bench=mybench] [op=create] [component=docker] Building containers"
34 >>> logger.info("Site created", extra_fields={"environment": "dev", "apps": "frappe,erpnext"})
35 # Logs: "[bench=mybench] [op=create] Site created | environment=dev | apps=frappe,erpnext"
36 """
38 def __init__(self, logger: logging.Logger, context: LoggerContext | None = None):
39 """
40 Initialize contextual logger.
42 Args:
43 logger: Base Python logger to wrap
44 context: Optional context to add to messages (defaults to empty context)
45 """
46 self.logger = logger
47 self.context = context or LoggerContext()
49 def _format_message(self, msg: str, extra_fields: dict[str, Any] | None = None) -> str:
50 prefix = self.context.format()
51 extra_suffix = (
52 self._format_extra_fields(extra_fields) if extra_fields and isinstance(extra_fields, dict) else ""
53 )
55 if prefix and extra_suffix:
56 return f"{prefix} {msg}{extra_suffix}"
57 if prefix:
58 return f"{prefix} {msg}"
59 if extra_suffix:
60 return f"{msg}{extra_suffix}"
61 return msg
63 def _format_extra_fields(self, extra_fields: dict[str, Any]) -> str:
64 if not extra_fields:
65 return ""
67 parts = []
68 for key, value in extra_fields.items():
69 if value is not None:
70 parts.append(f"{key}={value}")
72 if parts:
73 return " | " + " | ".join(parts)
74 return ""
76 def debug(self, msg: str, *args, extra_fields: dict[str, Any] | None = None, **kwargs):
77 self.logger.debug(self._format_message(msg, extra_fields), *args, **kwargs)
79 def info(self, msg: str, *args, extra_fields: dict[str, Any] | None = None, **kwargs):
80 self.logger.info(self._format_message(msg, extra_fields), *args, **kwargs)
82 def warning(self, msg: str, *args, extra_fields: dict[str, Any] | None = None, **kwargs):
83 self.logger.warning(self._format_message(msg, extra_fields), *args, **kwargs)
85 def error(self, msg: str, *args, extra_fields: dict[str, Any] | None = None, **kwargs):
86 self.logger.error(self._format_message(msg, extra_fields), *args, **kwargs)
88 def exception(self, msg: str, *args, extra_fields: dict[str, Any] | None = None, **kwargs):
89 self.logger.exception(self._format_message(msg, extra_fields), *args, **kwargs)
91 def child(self, **context_overrides) -> "ContextualLogger":
92 """
93 Create child logger with extended context.
95 The child inherits all context from parent and can override or extend it.
96 Useful for nested operations that need additional context.
98 Args:
99 **context_overrides: Context fields to add/override (bench, operation, component, extra)
101 Returns:
102 New ContextualLogger with extended context
104 Example:
105 >>> parent = ContextualLogger(logger, LoggerContext(bench="mybench"))
106 >>> child = parent.child(operation="create", component="docker")
107 >>> child.info("Starting containers")
108 # Logs: "[bench=mybench] [op=create] [component=docker] Starting containers"
109 """
110 new_context = self.context.child(**context_overrides)
111 return ContextualLogger(self.logger, new_context)
113 # Pass-through properties and methods for compatibility
115 @property
116 def level(self) -> int:
117 """Get the logger's current level."""
118 return self.logger.level
120 def setLevel(self, level: int):
121 """
122 Set the logger's level.
124 Args:
125 level: Logging level (logging.DEBUG, logging.INFO, etc.)
126 """
127 self.logger.setLevel(level)
129 def isEnabledFor(self, level: int) -> bool:
130 """
131 Check if logger is enabled for a given level.
133 Args:
134 level: Logging level to check
136 Returns:
137 True if logger would emit a message at this level
138 """
139 return self.logger.isEnabledFor(level)
141 @property
142 def name(self) -> str:
143 """Get the logger's name."""
144 return self.logger.name
146 def get_context(self) -> LoggerContext:
147 """
148 Get the current context.
150 Returns:
151 Current LoggerContext
152 """
153 return self.context