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

1"""Plugin-specific logging with isolated namespaces. 

2 

3This module provides plugin-specific loggers with isolated namespaces 

4and per-plugin configuration for proper log management. 

5 

6 

7Example: 

8 >>> from tracekit.extensibility.logging import get_plugin_logger 

9 >>> logger = get_plugin_logger("my_plugin") 

10 >>> logger.info("Plugin initialized") 

11 

12References: 

13""" 

14 

15from __future__ import annotations 

16 

17import logging 

18from typing import Any 

19 

20from tracekit.core.logging import get_logger 

21 

22# Plugin logger namespace prefix 

23PLUGIN_LOGGER_PREFIX = "tracekit.plugins" 

24 

25# Per-plugin log level configuration 

26_plugin_log_levels: dict[str, int] = {} 

27 

28 

29def get_plugin_logger(plugin_name: str) -> logging.Logger: 

30 """Get a logger for a specific plugin. 

31 

32 Creates a logger under the tracekit.plugins.<plugin_name> namespace 

33 with plugin-specific configuration. 

34 

35 Args: 

36 plugin_name: Name of the plugin. 

37 

38 Returns: 

39 Configured logging.Logger for the plugin. 

40 

41 Example: 

42 >>> logger = get_plugin_logger("my_decoder") 

43 >>> logger.info("Decoding frame", frame_id=42) 

44 

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) 

50 

51 # Apply per-plugin log level if configured 

52 if plugin_name in _plugin_log_levels: 

53 logger.setLevel(_plugin_log_levels[plugin_name]) 

54 

55 return logger 

56 

57 

58def set_plugin_log_level(plugin_name: str, level: str | int) -> None: 

59 """Set log level for a specific plugin. 

60 

61 Args: 

62 plugin_name: Name of the plugin. 

63 level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL or int). 

64 

65 Example: 

66 >>> set_plugin_log_level("my_plugin", "DEBUG") 

67 

68 References: 

69 LOG-014: Isolated Logger Namespaces for Plugins 

70 """ 

71 if isinstance(level, str): 

72 level = getattr(logging, level.upper()) 

73 

74 _plugin_log_levels[plugin_name] = level # type: ignore[assignment] 

75 

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) 

80 

81 

82def get_plugin_log_level(plugin_name: str) -> int | None: 

83 """Get the configured log level for a plugin. 

84 

85 Args: 

86 plugin_name: Name of the plugin. 

87 

88 Returns: 

89 Log level as integer, or None if not configured. 

90 """ 

91 return _plugin_log_levels.get(plugin_name) 

92 

93 

94class PluginLoggerAdapter(logging.LoggerAdapter): # type: ignore[type-arg] 

95 """Logger adapter that adds plugin context to all log messages. 

96 

97 Automatically includes plugin name and version in all log records. 

98 

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'} 

103 

104 References: 

105 LOG-014: Isolated Logger Namespaces for Plugins 

106 """ 

107 

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. 

115 

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 

131 

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. 

134 

135 Args: 

136 msg: Log message. 

137 kwargs: Keyword arguments. 

138 

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 

146 

147 

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. 

156 

157 Standard logging for plugin discovery, loading, registration, 

158 unloading, and error events. 

159 

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. 

166 

167 Example: 

168 >>> log_plugin_lifecycle("my_plugin", "loaded", version="1.0.0") 

169 >>> log_plugin_lifecycle("my_plugin", "error", details={"error": "Import failed"}) 

170 

171 References: 

172 LOG-014: Isolated Logger Namespaces for Plugins 

173 """ 

174 logger = get_logger(PLUGIN_LOGGER_PREFIX) 

175 

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 } 

186 

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) 

196 

197 logger.log( 

198 level, 

199 "Plugin '%s' %s", 

200 plugin_name, 

201 event, 

202 extra=extra, 

203 ) 

204 

205 

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. 

211 

212 Sets up default and per-plugin log levels for the plugin 

213 logger namespace. 

214 

215 Args: 

216 default_level: Default log level for all plugins. 

217 plugin_levels: Dict mapping plugin names to log levels. 

218 

219 Example: 

220 >>> configure_plugin_logging( 

221 ... default_level="WARNING", 

222 ... plugin_levels={ 

223 ... "debug_plugin": "DEBUG", 

224 ... "noisy_plugin": "ERROR", 

225 ... } 

226 ... ) 

227 

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())) 

234 

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) 

239 

240 

241def list_plugin_loggers() -> list[str]: 

242 """List all registered plugin loggers. 

243 

244 Returns: 

245 List of plugin names with configured loggers. 

246 """ 

247 return list(_plugin_log_levels.keys()) 

248 

249 

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]