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
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-22 12:53 +0100
1# -*- coding: utf-8 -*-
2"""Logging Service Implementation.
4Copyright 2025
5SPDX-License-Identifier: Apache-2.0
6Authors: Mihai Criveti
8This module implements structured logging according to the MCP specification.
9It supports RFC 5424 severity levels, log level management, and log event subscriptions.
10"""
12import asyncio
13import logging
14from datetime import datetime
15from typing import Any, AsyncGenerator, Dict, List, Optional
17from mcpgateway.types import LogLevel
20class LoggingService:
21 """MCP logging service.
23 Implements structured logging with:
24 - RFC 5424 severity levels
25 - Log level management
26 - Log event subscriptions
27 - Logger name tracking
28 """
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] = {}
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")
46 async def shutdown(self) -> None:
47 """Shutdown logging service."""
48 # Clear subscribers
49 self._subscribers.clear()
50 logging.info("Logging service shutdown")
52 def get_logger(self, name: str) -> logging.Logger:
53 """Get or create logger instance.
55 Args:
56 name: Logger name
58 Returns:
59 Logger instance
60 """
61 if name not in self._loggers:
62 logger = logging.getLogger(name)
64 # Set level to match service level
65 log_level = getattr(logging, self._level.upper())
66 logger.setLevel(log_level)
68 self._loggers[name] = logger
70 return self._loggers[name]
72 async def set_level(self, level: LogLevel) -> None:
73 """Set minimum log level.
75 This updates the level for all registered loggers.
77 Args:
78 level: New log level
79 """
80 self._level = level
82 # Update all loggers
83 log_level = getattr(logging, level.upper())
84 for logger in self._loggers.values():
85 logger.setLevel(log_level)
87 await self.notify(f"Log level set to {level}", LogLevel.INFO, "logging")
89 async def notify(self, data: Any, level: LogLevel, logger_name: Optional[str] = None) -> None:
90 """Send log notification to subscribers.
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
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
113 # Log through standard logging
114 logger = self.get_logger(logger_name or "")
115 log_func = getattr(logger, level.lower())
116 log_func(data)
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}")
125 async def subscribe(self) -> AsyncGenerator[Dict[str, Any], None]:
126 """Subscribe to log messages.
128 Returns a generator yielding log message events.
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)
142 def _should_log(self, level: LogLevel) -> bool:
143 """Check if level meets minimum threshold.
145 Args:
146 level: Log level to check
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 }
162 return level_values[level] >= level_values[self._level]