Coverage for src / tracekit / extensibility / logging.py: 24%
54 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 23:04 +0000
1"""Plugin-specific logging with isolated namespaces.
3This module provides plugin-specific loggers with isolated namespaces
4and per-plugin configuration for proper log management.
7Example:
8 >>> from tracekit.extensibility.logging import get_plugin_logger
9 >>> logger = get_plugin_logger("my_plugin")
10 >>> logger.info("Plugin initialized")
12References:
13"""
15from __future__ import annotations
17import logging
18from typing import Any
20from tracekit.core.logging import get_logger
22# Plugin logger namespace prefix
23PLUGIN_LOGGER_PREFIX = "tracekit.plugins"
25# Per-plugin log level configuration
26_plugin_log_levels: dict[str, int] = {}
29def get_plugin_logger(plugin_name: str) -> logging.Logger:
30 """Get a logger for a specific plugin.
32 Creates a logger under the tracekit.plugins.<plugin_name> namespace
33 with plugin-specific configuration.
35 Args:
36 plugin_name: Name of the plugin.
38 Returns:
39 Configured logging.Logger for the plugin.
41 Example:
42 >>> logger = get_plugin_logger("my_decoder")
43 >>> logger.info("Decoding frame", frame_id=42)
45 References:
46 LOG-014: Isolated Logger Namespaces for Plugins
47 """
48 logger_name = f"{PLUGIN_LOGGER_PREFIX}.{plugin_name}"
49 logger = get_logger(logger_name)
51 # Apply per-plugin log level if configured
52 if plugin_name in _plugin_log_levels:
53 logger.setLevel(_plugin_log_levels[plugin_name])
55 return logger
58def set_plugin_log_level(plugin_name: str, level: str | int) -> None:
59 """Set log level for a specific plugin.
61 Args:
62 plugin_name: Name of the plugin.
63 level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL or int).
65 Example:
66 >>> set_plugin_log_level("my_plugin", "DEBUG")
68 References:
69 LOG-014: Isolated Logger Namespaces for Plugins
70 """
71 if isinstance(level, str):
72 level = getattr(logging, level.upper())
74 _plugin_log_levels[plugin_name] = level # type: ignore[assignment]
76 # Update existing logger if it exists
77 logger_name = f"{PLUGIN_LOGGER_PREFIX}.{plugin_name}"
78 logger = logging.getLogger(logger_name)
79 logger.setLevel(level)
82def get_plugin_log_level(plugin_name: str) -> int | None:
83 """Get the configured log level for a plugin.
85 Args:
86 plugin_name: Name of the plugin.
88 Returns:
89 Log level as integer, or None if not configured.
90 """
91 return _plugin_log_levels.get(plugin_name)
94class PluginLoggerAdapter(logging.LoggerAdapter): # type: ignore[type-arg]
95 """Logger adapter that adds plugin context to all log messages.
97 Automatically includes plugin name and version in all log records.
99 Example:
100 >>> adapter = PluginLoggerAdapter("my_plugin", "1.0.0")
101 >>> adapter.info("Processing data")
102 # Logs: "Processing data" with extra={'plugin': 'my_plugin', 'version': '1.0.0'}
104 References:
105 LOG-014: Isolated Logger Namespaces for Plugins
106 """
108 def __init__(
109 self,
110 plugin_name: str,
111 version: str = "0.0.0",
112 extra: dict[str, Any] | None = None,
113 ):
114 """Initialize plugin logger adapter.
116 Args:
117 plugin_name: Name of the plugin.
118 version: Plugin version string.
119 extra: Additional context to include in all logs.
120 """
121 logger = get_plugin_logger(plugin_name)
122 base_extra = {
123 "plugin": plugin_name,
124 "plugin_version": version,
125 }
126 if extra:
127 base_extra.update(extra)
128 super().__init__(logger, base_extra)
129 self.plugin_name = plugin_name
130 self.version = version
132 def process(self, msg: str, kwargs: dict[str, Any]) -> tuple[str, dict[str, Any]]: # type: ignore[override]
133 """Process log message to include plugin context.
135 Args:
136 msg: Log message.
137 kwargs: Keyword arguments.
139 Returns:
140 Tuple of (message, kwargs) with plugin context added.
141 """
142 extra = kwargs.get("extra", {})
143 extra.update(self.extra)
144 kwargs["extra"] = extra
145 return msg, kwargs
148def log_plugin_lifecycle(
149 plugin_name: str,
150 event: str,
151 *,
152 version: str | None = None,
153 details: dict[str, Any] | None = None,
154) -> None:
155 """Log a plugin lifecycle event.
157 Standard logging for plugin discovery, loading, registration,
158 unloading, and error events.
160 Args:
161 plugin_name: Name of the plugin.
162 event: Lifecycle event (discovered, loading, loaded, registered,
163 unloading, unloaded, error, reload).
164 version: Plugin version if available.
165 details: Additional event details.
167 Example:
168 >>> log_plugin_lifecycle("my_plugin", "loaded", version="1.0.0")
169 >>> log_plugin_lifecycle("my_plugin", "error", details={"error": "Import failed"})
171 References:
172 LOG-014: Isolated Logger Namespaces for Plugins
173 """
174 logger = get_logger(PLUGIN_LOGGER_PREFIX)
176 event_levels = {
177 "discovered": logging.DEBUG,
178 "loading": logging.DEBUG,
179 "loaded": logging.INFO,
180 "registered": logging.INFO,
181 "unloading": logging.DEBUG,
182 "unloaded": logging.INFO,
183 "error": logging.ERROR,
184 "reload": logging.INFO,
185 }
187 level = event_levels.get(event, logging.INFO)
188 extra: dict[str, Any] = {
189 "plugin": plugin_name,
190 "lifecycle_event": event,
191 }
192 if version:
193 extra["plugin_version"] = version
194 if details:
195 extra.update(details)
197 logger.log(
198 level,
199 "Plugin '%s' %s",
200 plugin_name,
201 event,
202 extra=extra,
203 )
206def configure_plugin_logging(
207 default_level: str = "INFO",
208 plugin_levels: dict[str, str] | None = None,
209) -> None:
210 """Configure logging for all plugins.
212 Sets up default and per-plugin log levels for the plugin
213 logger namespace.
215 Args:
216 default_level: Default log level for all plugins.
217 plugin_levels: Dict mapping plugin names to log levels.
219 Example:
220 >>> configure_plugin_logging(
221 ... default_level="WARNING",
222 ... plugin_levels={
223 ... "debug_plugin": "DEBUG",
224 ... "noisy_plugin": "ERROR",
225 ... }
226 ... )
228 References:
229 LOG-014: Isolated Logger Namespaces for Plugins
230 """
231 # Set default level for plugin namespace
232 plugin_root = logging.getLogger(PLUGIN_LOGGER_PREFIX)
233 plugin_root.setLevel(getattr(logging, default_level.upper()))
235 # Set per-plugin levels
236 if plugin_levels:
237 for plugin_name, level in plugin_levels.items():
238 set_plugin_log_level(plugin_name, level)
241def list_plugin_loggers() -> list[str]:
242 """List all registered plugin loggers.
244 Returns:
245 List of plugin names with configured loggers.
246 """
247 return list(_plugin_log_levels.keys())
250__all__ = [
251 "PLUGIN_LOGGER_PREFIX",
252 "PluginLoggerAdapter",
253 "configure_plugin_logging",
254 "get_plugin_log_level",
255 "get_plugin_logger",
256 "list_plugin_loggers",
257 "log_plugin_lifecycle",
258 "set_plugin_log_level",
259]