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

55 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-09 11:03 +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 

12# Standard 

13import asyncio 

14from datetime import datetime, timezone 

15import logging 

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

17 

18# First-Party 

19from mcpgateway.models import LogLevel 

20 

21 

22class LoggingService: 

23 """MCP logging service. 

24 

25 Implements structured logging with: 

26 - RFC 5424 severity levels 

27 - Log level management 

28 - Log event subscriptions 

29 - Logger name tracking 

30 """ 

31 

32 def __init__(self): 

33 """Initialize logging service.""" 

34 self._level = LogLevel.INFO 

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

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

37 

38 async def initialize(self) -> None: 

39 """Initialize logging service.""" 

40 # Configure root logger 

41 logging.basicConfig( 

42 level=logging.INFO, 

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

44 ) 

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

46 logging.info("Logging service initialized") 

47 

48 async def shutdown(self) -> None: 

49 """Shutdown logging service.""" 

50 # Clear subscribers 

51 self._subscribers.clear() 

52 logging.info("Logging service shutdown") 

53 

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

55 """Get or create logger instance. 

56 

57 Args: 

58 name: Logger name 

59 

60 Returns: 

61 Logger instance 

62 """ 

63 if name not in self._loggers: 

64 logger = logging.getLogger(name) 

65 

66 # Set level to match service level 

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

68 logger.setLevel(log_level) 

69 

70 self._loggers[name] = logger 

71 

72 return self._loggers[name] 

73 

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

75 """Set minimum log level. 

76 

77 This updates the level for all registered loggers. 

78 

79 Args: 

80 level: New log level 

81 """ 

82 self._level = level 

83 

84 # Update all loggers 

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

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

87 logger.setLevel(log_level) 

88 

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

90 

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

92 """Send log notification to subscribers. 

93 

94 Args: 

95 data: Log message data 

96 level: Log severity level 

97 logger_name: Optional logger name 

98 """ 

99 # Skip if below current level 

100 if not self._should_log(level): 

101 return 

102 

103 # Format notification message 

104 message = { 

105 "type": "log", 

106 "data": { 

107 "level": level, 

108 "data": data, 

109 "timestamp": datetime.now(timezone.utc).isoformat(), 

110 }, 

111 } 

112 if logger_name: 

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

114 

115 # Log through standard logging 

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

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

118 log_func(data) 

119 

120 # Notify subscribers 

121 for queue in self._subscribers: 

122 try: 

123 await queue.put(message) 

124 except Exception as e: 

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

126 

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

128 """Subscribe to log messages. 

129 

130 Returns a generator yielding log message events. 

131 

132 Yields: 

133 Log message events 

134 """ 

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

136 self._subscribers.append(queue) 

137 try: 

138 while True: 

139 message = await queue.get() 

140 yield message 

141 finally: 

142 self._subscribers.remove(queue) 

143 

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

145 """Check if level meets minimum threshold. 

146 

147 Args: 

148 level: Log level to check 

149 

150 Returns: 

151 True if should log 

152 """ 

153 level_values = { 

154 LogLevel.DEBUG: 0, 

155 LogLevel.INFO: 1, 

156 LogLevel.NOTICE: 2, 

157 LogLevel.WARNING: 3, 

158 LogLevel.ERROR: 4, 

159 LogLevel.CRITICAL: 5, 

160 LogLevel.ALERT: 6, 

161 LogLevel.EMERGENCY: 7, 

162 } 

163 

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