Coverage for mcpgateway/services/logging_service.py: 92%

55 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-22 12:53 +0100

1# -*- coding: utf-8 -*- 

2"""Logging Service Implementation. 

3 

4Copyright 2025 

5SPDX-License-Identifier: Apache-2.0 

6Authors: Mihai Criveti 

7 

8This module implements structured logging according to the MCP specification. 

9It supports RFC 5424 severity levels, log level management, and log event subscriptions. 

10""" 

11 

12import asyncio 

13import logging 

14from datetime import datetime 

15from typing import Any, AsyncGenerator, Dict, List, Optional 

16 

17from mcpgateway.types import LogLevel 

18 

19 

20class LoggingService: 

21 """MCP logging service. 

22 

23 Implements structured logging with: 

24 - RFC 5424 severity levels 

25 - Log level management 

26 - Log event subscriptions 

27 - Logger name tracking 

28 """ 

29 

30 def __init__(self): 

31 """Initialize logging service.""" 

32 self._level = LogLevel.INFO 

33 self._subscribers: List[asyncio.Queue] = [] 

34 self._loggers: Dict[str, logging.Logger] = {} 

35 

36 async def initialize(self) -> None: 

37 """Initialize logging service.""" 

38 # Configure root logger 

39 logging.basicConfig( 

40 level=logging.INFO, 

41 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 

42 ) 

43 self._loggers[""] = logging.getLogger() 

44 logging.info("Logging service initialized") 

45 

46 async def shutdown(self) -> None: 

47 """Shutdown logging service.""" 

48 # Clear subscribers 

49 self._subscribers.clear() 

50 logging.info("Logging service shutdown") 

51 

52 def get_logger(self, name: str) -> logging.Logger: 

53 """Get or create logger instance. 

54 

55 Args: 

56 name: Logger name 

57 

58 Returns: 

59 Logger instance 

60 """ 

61 if name not in self._loggers: 

62 logger = logging.getLogger(name) 

63 

64 # Set level to match service level 

65 log_level = getattr(logging, self._level.upper()) 

66 logger.setLevel(log_level) 

67 

68 self._loggers[name] = logger 

69 

70 return self._loggers[name] 

71 

72 async def set_level(self, level: LogLevel) -> None: 

73 """Set minimum log level. 

74 

75 This updates the level for all registered loggers. 

76 

77 Args: 

78 level: New log level 

79 """ 

80 self._level = level 

81 

82 # Update all loggers 

83 log_level = getattr(logging, level.upper()) 

84 for logger in self._loggers.values(): 

85 logger.setLevel(log_level) 

86 

87 await self.notify(f"Log level set to {level}", LogLevel.INFO, "logging") 

88 

89 async def notify(self, data: Any, level: LogLevel, logger_name: Optional[str] = None) -> None: 

90 """Send log notification to subscribers. 

91 

92 Args: 

93 data: Log message data 

94 level: Log severity level 

95 logger_name: Optional logger name 

96 """ 

97 # Skip if below current level 

98 if not self._should_log(level): 

99 return 

100 

101 # Format notification message 

102 message = { 

103 "type": "log", 

104 "data": { 

105 "level": level, 

106 "data": data, 

107 "timestamp": datetime.utcnow().isoformat(), 

108 }, 

109 } 

110 if logger_name: 

111 message["data"]["logger"] = logger_name 

112 

113 # Log through standard logging 

114 logger = self.get_logger(logger_name or "") 

115 log_func = getattr(logger, level.lower()) 

116 log_func(data) 

117 

118 # Notify subscribers 

119 for queue in self._subscribers: 

120 try: 

121 await queue.put(message) 

122 except Exception as e: 

123 logger.error(f"Failed to notify subscriber: {e}") 

124 

125 async def subscribe(self) -> AsyncGenerator[Dict[str, Any], None]: 

126 """Subscribe to log messages. 

127 

128 Returns a generator yielding log message events. 

129 

130 Yields: 

131 Log message events 

132 """ 

133 queue: asyncio.Queue = asyncio.Queue() 

134 self._subscribers.append(queue) 

135 try: 

136 while True: 

137 message = await queue.get() 

138 yield message 

139 finally: 

140 self._subscribers.remove(queue) 

141 

142 def _should_log(self, level: LogLevel) -> bool: 

143 """Check if level meets minimum threshold. 

144 

145 Args: 

146 level: Log level to check 

147 

148 Returns: 

149 True if should log 

150 """ 

151 level_values = { 

152 LogLevel.DEBUG: 0, 

153 LogLevel.INFO: 1, 

154 LogLevel.NOTICE: 2, 

155 LogLevel.WARNING: 3, 

156 LogLevel.ERROR: 4, 

157 LogLevel.CRITICAL: 5, 

158 LogLevel.ALERT: 6, 

159 LogLevel.EMERGENCY: 7, 

160 } 

161 

162 return level_values[level] >= level_values[self._level]