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
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-09 11:03 +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"""
12# Standard
13import asyncio
14from datetime import datetime, timezone
15import logging
16from typing import Any, AsyncGenerator, Dict, List, Optional
18# First-Party
19from mcpgateway.models import LogLevel
22class LoggingService:
23 """MCP logging service.
25 Implements structured logging with:
26 - RFC 5424 severity levels
27 - Log level management
28 - Log event subscriptions
29 - Logger name tracking
30 """
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] = {}
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")
48 async def shutdown(self) -> None:
49 """Shutdown logging service."""
50 # Clear subscribers
51 self._subscribers.clear()
52 logging.info("Logging service shutdown")
54 def get_logger(self, name: str) -> logging.Logger:
55 """Get or create logger instance.
57 Args:
58 name: Logger name
60 Returns:
61 Logger instance
62 """
63 if name not in self._loggers:
64 logger = logging.getLogger(name)
66 # Set level to match service level
67 log_level = getattr(logging, self._level.upper())
68 logger.setLevel(log_level)
70 self._loggers[name] = logger
72 return self._loggers[name]
74 async def set_level(self, level: LogLevel) -> None:
75 """Set minimum log level.
77 This updates the level for all registered loggers.
79 Args:
80 level: New log level
81 """
82 self._level = level
84 # Update all loggers
85 log_level = getattr(logging, level.upper())
86 for logger in self._loggers.values():
87 logger.setLevel(log_level)
89 await self.notify(f"Log level set to {level}", LogLevel.INFO, "logging")
91 async def notify(self, data: Any, level: LogLevel, logger_name: Optional[str] = None) -> None:
92 """Send log notification to subscribers.
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
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
115 # Log through standard logging
116 logger = self.get_logger(logger_name or "")
117 log_func = getattr(logger, level.lower())
118 log_func(data)
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}")
127 async def subscribe(self) -> AsyncGenerator[Dict[str, Any], None]:
128 """Subscribe to log messages.
130 Returns a generator yielding log message events.
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)
144 def _should_log(self, level: LogLevel) -> bool:
145 """Check if level meets minimum threshold.
147 Args:
148 level: Log level to check
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 }
164 return level_values[level] >= level_values[self._level]