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

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

2"""Logging Service Implementation.

3

4 Copyright 2025

5 SPDX-License-Identifier: Apache-2.0

6 Authors: Mihai Criveti

7

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

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

10"""

11

12# Standard

13 import asyncio

14 from datetime import datetime, timezone

15 import logging

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

17

18# First-Party

19 from mcpgateway.models import LogLevel

20

21

22 class 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]