spring-ready-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spring_ready/__init__.py +30 -0
- spring_ready/actuator/__init__.py +68 -0
- spring_ready/actuator/auditevents.py +114 -0
- spring_ready/actuator/beans.py +141 -0
- spring_ready/actuator/caches.py +111 -0
- spring_ready/actuator/configprops.py +109 -0
- spring_ready/actuator/discovery.py +131 -0
- spring_ready/actuator/env.py +205 -0
- spring_ready/actuator/health.py +145 -0
- spring_ready/actuator/heapdump.py +80 -0
- spring_ready/actuator/httptrace.py +138 -0
- spring_ready/actuator/info.py +123 -0
- spring_ready/actuator/logfile.py +201 -0
- spring_ready/actuator/loggers.py +184 -0
- spring_ready/actuator/mappings.py +88 -0
- spring_ready/actuator/metrics.py +260 -0
- spring_ready/actuator/prometheus.py +354 -0
- spring_ready/actuator/refresh.py +86 -0
- spring_ready/actuator/scheduledtasks.py +95 -0
- spring_ready/actuator/threaddump.py +116 -0
- spring_ready/config/__init__.py +5 -0
- spring_ready/config/loader.py +215 -0
- spring_ready/core.py +479 -0
- spring_ready/eureka/__init__.py +15 -0
- spring_ready/eureka/client.py +291 -0
- spring_ready/eureka/discovery.py +198 -0
- spring_ready/eureka/instance.py +200 -0
- spring_ready/eureka/registry.py +191 -0
- spring_ready/exceptions.py +26 -0
- spring_ready/integrations/__init__.py +8 -0
- spring_ready/integrations/fastapi.py +521 -0
- spring_ready/retry.py +97 -0
- spring_ready_python-0.1.0.dist-info/METADATA +459 -0
- spring_ready_python-0.1.0.dist-info/RECORD +35 -0
- spring_ready_python-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Actuator info endpoint.
|
|
3
|
+
Provides application metadata like Spring Boot Actuator's /actuator/info.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InfoEndpoint:
|
|
16
|
+
"""
|
|
17
|
+
Info endpoint that provides application metadata.
|
|
18
|
+
|
|
19
|
+
Matches Spring Boot Actuator's info endpoint format:
|
|
20
|
+
- app: Application info (name, version, description)
|
|
21
|
+
- build: Build information
|
|
22
|
+
- git: Git commit info (if available)
|
|
23
|
+
- java: Runtime info (adapted for Python)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
app_name: Optional[str] = None,
|
|
29
|
+
app_version: Optional[str] = None,
|
|
30
|
+
app_description: Optional[str] = None
|
|
31
|
+
):
|
|
32
|
+
self.app_name = app_name or os.getenv("SPRING_APPLICATION_NAME", "unknown")
|
|
33
|
+
self.app_version = app_version or os.getenv("APP_VERSION", "unknown")
|
|
34
|
+
self.app_description = app_description or os.getenv("APP_DESCRIPTION", "")
|
|
35
|
+
self.custom_info: Dict[str, Any] = {}
|
|
36
|
+
|
|
37
|
+
def add_info(self, key: str, value: Any) -> None:
|
|
38
|
+
"""Add custom info field"""
|
|
39
|
+
self.custom_info[key] = value
|
|
40
|
+
|
|
41
|
+
def get_info(self) -> Dict[str, Any]:
|
|
42
|
+
"""
|
|
43
|
+
Get application info.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Info response matching Spring Boot Actuator format
|
|
47
|
+
"""
|
|
48
|
+
info = {
|
|
49
|
+
"app": {
|
|
50
|
+
"name": self.app_name,
|
|
51
|
+
"version": self.app_version,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if self.app_description:
|
|
56
|
+
info["app"]["description"] = self.app_description
|
|
57
|
+
|
|
58
|
+
# Python runtime info (equivalent to java info in Spring)
|
|
59
|
+
info["python"] = {
|
|
60
|
+
"version": sys.version,
|
|
61
|
+
"vendor": sys.copyright.split("\n")[0] if sys.copyright else "Python Software Foundation",
|
|
62
|
+
"runtime": {
|
|
63
|
+
"name": "CPython",
|
|
64
|
+
"version": sys.version.split()[0]
|
|
65
|
+
},
|
|
66
|
+
"jvm": { # Keep "jvm" key for compatibility with Spring Admin
|
|
67
|
+
"name": f"Python {sys.version.split()[0]}",
|
|
68
|
+
"vendor": "Python Software Foundation",
|
|
69
|
+
"version": sys.version.split()[0]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Build info from environment (if available)
|
|
74
|
+
if any(os.getenv(k) for k in ["BUILD_NUMBER", "BUILD_TIME", "GIT_COMMIT"]):
|
|
75
|
+
info["build"] = {}
|
|
76
|
+
|
|
77
|
+
if build_number := os.getenv("BUILD_NUMBER"):
|
|
78
|
+
info["build"]["number"] = build_number
|
|
79
|
+
|
|
80
|
+
if build_time := os.getenv("BUILD_TIME"):
|
|
81
|
+
info["build"]["time"] = build_time
|
|
82
|
+
|
|
83
|
+
if git_commit := os.getenv("GIT_COMMIT"):
|
|
84
|
+
info["build"]["commit"] = git_commit
|
|
85
|
+
|
|
86
|
+
# Git info (if available)
|
|
87
|
+
if git_commit := os.getenv("GIT_COMMIT"):
|
|
88
|
+
info["git"] = {
|
|
89
|
+
"commit": {
|
|
90
|
+
"id": git_commit,
|
|
91
|
+
"time": os.getenv("GIT_COMMIT_TIME", "")
|
|
92
|
+
},
|
|
93
|
+
"branch": os.getenv("GIT_BRANCH", "")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Add custom info
|
|
97
|
+
if self.custom_info:
|
|
98
|
+
info.update(self.custom_info)
|
|
99
|
+
|
|
100
|
+
return info
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def create_default_info_endpoint(
|
|
104
|
+
app_name: Optional[str] = None,
|
|
105
|
+
app_version: Optional[str] = None,
|
|
106
|
+
app_description: Optional[str] = None
|
|
107
|
+
) -> InfoEndpoint:
|
|
108
|
+
"""
|
|
109
|
+
Create info endpoint with default configuration.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
app_name: Application name (default from env)
|
|
113
|
+
app_version: Application version (default from env)
|
|
114
|
+
app_description: Application description (default from env)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
InfoEndpoint instance
|
|
118
|
+
"""
|
|
119
|
+
return InfoEndpoint(
|
|
120
|
+
app_name=app_name,
|
|
121
|
+
app_version=app_version,
|
|
122
|
+
app_description=app_description
|
|
123
|
+
)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Actuator Logfile Endpoint.
|
|
3
|
+
Provides access to application log files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LogfileEndpoint:
|
|
15
|
+
"""
|
|
16
|
+
Logfile endpoint for Spring Boot Actuator compatibility.
|
|
17
|
+
|
|
18
|
+
Provides access to application log files with support for:
|
|
19
|
+
- Full log file retrieval
|
|
20
|
+
- Partial retrieval with Range header support
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, log_file_path: Optional[str] = None):
|
|
24
|
+
"""
|
|
25
|
+
Args:
|
|
26
|
+
log_file_path: Path to the log file (default: None, which means no log file)
|
|
27
|
+
"""
|
|
28
|
+
self.log_file_path = log_file_path
|
|
29
|
+
|
|
30
|
+
# Verify log file exists if path provided
|
|
31
|
+
if self.log_file_path and not os.path.exists(self.log_file_path):
|
|
32
|
+
logger.warning(f"Log file not found: {self.log_file_path}")
|
|
33
|
+
|
|
34
|
+
def get_logfile(self, range_header: Optional[str] = None) -> tuple[Optional[bytes], Optional[str], int]:
|
|
35
|
+
"""
|
|
36
|
+
Get log file contents.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
range_header: HTTP Range header value (e.g., "bytes=0-1023")
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Tuple of (content, content_range_header, status_code)
|
|
43
|
+
- content: Log file bytes or None if not available
|
|
44
|
+
- content_range_header: Content-Range header value or None
|
|
45
|
+
- status_code: HTTP status code (200, 206, or 404)
|
|
46
|
+
"""
|
|
47
|
+
# If no log file configured
|
|
48
|
+
if not self.log_file_path:
|
|
49
|
+
logger.warning("No log file configured for logfile endpoint")
|
|
50
|
+
return None, None, 404
|
|
51
|
+
|
|
52
|
+
# If log file doesn't exist
|
|
53
|
+
if not os.path.exists(self.log_file_path):
|
|
54
|
+
logger.warning(f"Log file not found: {self.log_file_path}")
|
|
55
|
+
return None, None, 404
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
file_size = os.path.getsize(self.log_file_path)
|
|
59
|
+
|
|
60
|
+
# Handle range request
|
|
61
|
+
if range_header:
|
|
62
|
+
# Parse range header (e.g., "bytes=0-1023")
|
|
63
|
+
if range_header.startswith("bytes="):
|
|
64
|
+
range_spec = range_header[6:]
|
|
65
|
+
start, end = self._parse_range(range_spec, file_size)
|
|
66
|
+
|
|
67
|
+
if start is None or end is None:
|
|
68
|
+
# Invalid range
|
|
69
|
+
return None, None, 416 # Range Not Satisfiable
|
|
70
|
+
|
|
71
|
+
# Read the specified range
|
|
72
|
+
with open(self.log_file_path, 'rb') as f:
|
|
73
|
+
f.seek(start)
|
|
74
|
+
content = f.read(end - start + 1)
|
|
75
|
+
|
|
76
|
+
# Build Content-Range header
|
|
77
|
+
content_range = f"bytes {start}-{end}/{file_size}"
|
|
78
|
+
return content, content_range, 206 # Partial Content
|
|
79
|
+
|
|
80
|
+
# Read entire file
|
|
81
|
+
with open(self.log_file_path, 'rb') as f:
|
|
82
|
+
content = f.read()
|
|
83
|
+
|
|
84
|
+
return content, None, 200
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f"Error reading log file: {e}", exc_info=True)
|
|
88
|
+
return None, None, 500
|
|
89
|
+
|
|
90
|
+
def _parse_range(self, range_spec: str, file_size: int) -> tuple[Optional[int], Optional[int]]:
|
|
91
|
+
"""
|
|
92
|
+
Parse HTTP Range specification.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
range_spec: Range specification (e.g., "0-1023" or "-1024" or "1024-")
|
|
96
|
+
file_size: Total file size
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Tuple of (start, end) byte positions, or (None, None) if invalid
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
if '-' not in range_spec:
|
|
103
|
+
return None, None
|
|
104
|
+
|
|
105
|
+
parts = range_spec.split('-', 1)
|
|
106
|
+
|
|
107
|
+
# Handle "-1024" (last 1024 bytes)
|
|
108
|
+
if not parts[0]:
|
|
109
|
+
suffix_length = int(parts[1])
|
|
110
|
+
start = max(0, file_size - suffix_length)
|
|
111
|
+
end = file_size - 1
|
|
112
|
+
return start, end
|
|
113
|
+
|
|
114
|
+
# Handle "1024-" (from byte 1024 to end)
|
|
115
|
+
if not parts[1]:
|
|
116
|
+
start = int(parts[0])
|
|
117
|
+
end = file_size - 1
|
|
118
|
+
return start, end
|
|
119
|
+
|
|
120
|
+
# Handle "0-1023" (bytes 0 to 1023)
|
|
121
|
+
start = int(parts[0])
|
|
122
|
+
end = int(parts[1])
|
|
123
|
+
|
|
124
|
+
# Validate range
|
|
125
|
+
if start < 0 or end >= file_size or start > end:
|
|
126
|
+
return None, None
|
|
127
|
+
|
|
128
|
+
return start, end
|
|
129
|
+
|
|
130
|
+
except (ValueError, IndexError):
|
|
131
|
+
return None, None
|
|
132
|
+
|
|
133
|
+
def is_available(self) -> bool:
|
|
134
|
+
"""Check if log file is available"""
|
|
135
|
+
return (
|
|
136
|
+
self.log_file_path is not None
|
|
137
|
+
and os.path.exists(self.log_file_path)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def create_default_logfile_endpoint(log_file_path: Optional[str] = None) -> LogfileEndpoint:
|
|
142
|
+
"""
|
|
143
|
+
Create logfile endpoint with default configuration.
|
|
144
|
+
|
|
145
|
+
Automatically detects log file from Python logging handlers if not specified.
|
|
146
|
+
Priority: explicit parameter > LOG_FILE_PATH env var > auto-detected from logging
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
log_file_path: Path to log file (default: auto-detect or from env LOG_FILE_PATH)
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
LogfileEndpoint instance
|
|
153
|
+
"""
|
|
154
|
+
# Priority 1: Explicit parameter
|
|
155
|
+
if log_file_path is not None:
|
|
156
|
+
return LogfileEndpoint(log_file_path=log_file_path)
|
|
157
|
+
|
|
158
|
+
# Priority 2: Environment variable
|
|
159
|
+
log_file_path = os.getenv("LOG_FILE_PATH")
|
|
160
|
+
if log_file_path is not None:
|
|
161
|
+
return LogfileEndpoint(log_file_path=log_file_path)
|
|
162
|
+
|
|
163
|
+
# Priority 3: Auto-detect from Python logging handlers
|
|
164
|
+
log_file_path = _auto_detect_log_file()
|
|
165
|
+
|
|
166
|
+
return LogfileEndpoint(log_file_path=log_file_path)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _auto_detect_log_file() -> Optional[str]:
|
|
170
|
+
"""
|
|
171
|
+
Auto-detect log file path from Python logging configuration.
|
|
172
|
+
|
|
173
|
+
Scans all logging handlers for FileHandler, RotatingFileHandler,
|
|
174
|
+
TimedRotatingFileHandler and extracts the file path.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Detected log file path or None if no file handler found
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
# Check root logger first
|
|
181
|
+
for handler in logging.root.handlers:
|
|
182
|
+
if isinstance(handler, logging.FileHandler):
|
|
183
|
+
log_path = handler.baseFilename
|
|
184
|
+
logger.info(f"Auto-detected log file: {log_path}")
|
|
185
|
+
return log_path
|
|
186
|
+
|
|
187
|
+
# Check all other loggers
|
|
188
|
+
for logger_name in logging.Logger.manager.loggerDict:
|
|
189
|
+
log_instance = logging.getLogger(logger_name)
|
|
190
|
+
if hasattr(log_instance, 'handlers'):
|
|
191
|
+
for handler in log_instance.handlers:
|
|
192
|
+
if isinstance(handler, logging.FileHandler):
|
|
193
|
+
log_path = handler.baseFilename
|
|
194
|
+
logger.info(f"Auto-detected log file from logger '{logger_name}': {log_path}")
|
|
195
|
+
return log_path
|
|
196
|
+
|
|
197
|
+
logger.debug("No log file found in logging configuration")
|
|
198
|
+
return None
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.warning(f"Failed to auto-detect log file: {e}")
|
|
201
|
+
return None
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Actuator Loggers Endpoint.
|
|
3
|
+
Shows and manages logger configurations and levels.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Dict, Any, Optional, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Valid log levels
|
|
11
|
+
VALID_LEVELS = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL", "OFF"]
|
|
12
|
+
|
|
13
|
+
# Map Spring Boot levels to Python levels
|
|
14
|
+
LEVEL_MAPPING = {
|
|
15
|
+
"TRACE": logging.DEBUG, # Python doesn't have TRACE, use DEBUG
|
|
16
|
+
"DEBUG": logging.DEBUG,
|
|
17
|
+
"INFO": logging.INFO,
|
|
18
|
+
"WARN": logging.WARNING,
|
|
19
|
+
"ERROR": logging.ERROR,
|
|
20
|
+
"CRITICAL": logging.CRITICAL,
|
|
21
|
+
"OFF": logging.CRITICAL + 10, # Effectively disable logging
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Reverse mapping for display
|
|
25
|
+
PYTHON_TO_SPRING = {
|
|
26
|
+
logging.DEBUG: "DEBUG",
|
|
27
|
+
logging.INFO: "INFO",
|
|
28
|
+
logging.WARNING: "WARN",
|
|
29
|
+
logging.ERROR: "ERROR",
|
|
30
|
+
logging.CRITICAL: "CRITICAL",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LoggersEndpoint:
|
|
35
|
+
"""
|
|
36
|
+
Loggers endpoint for Spring Boot Actuator compatibility.
|
|
37
|
+
|
|
38
|
+
Provides:
|
|
39
|
+
- List all loggers with their levels
|
|
40
|
+
- Get individual logger configuration
|
|
41
|
+
- Set/update logger levels
|
|
42
|
+
- Clear logger levels (reset to inherited)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self):
|
|
46
|
+
"""Initialize loggers endpoint"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
def _get_logger_level_name(self, level: Optional[int]) -> Optional[str]:
|
|
50
|
+
"""
|
|
51
|
+
Convert Python log level to Spring Boot level name.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
level: Python log level integer
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Spring Boot level name or None
|
|
58
|
+
"""
|
|
59
|
+
if level is None:
|
|
60
|
+
return None
|
|
61
|
+
return PYTHON_TO_SPRING.get(level, "DEBUG")
|
|
62
|
+
|
|
63
|
+
def _get_logger_info(self, logger: logging.Logger) -> Dict[str, Any]:
|
|
64
|
+
"""
|
|
65
|
+
Get logger information.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
logger: Logger instance
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dictionary with configuredLevel and effectiveLevel
|
|
72
|
+
"""
|
|
73
|
+
# Get configured level (may be None if inherited)
|
|
74
|
+
configured_level = logger.level if logger.level != logging.NOTSET else None
|
|
75
|
+
configured_level_name = self._get_logger_level_name(configured_level)
|
|
76
|
+
|
|
77
|
+
# Get effective level (always has a value due to inheritance)
|
|
78
|
+
effective_level = logger.getEffectiveLevel()
|
|
79
|
+
effective_level_name = self._get_logger_level_name(effective_level)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"configuredLevel": configured_level_name,
|
|
83
|
+
"effectiveLevel": effective_level_name
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
def get_all_loggers(self) -> Dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
Get all loggers.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dictionary with levels list and loggers dict
|
|
92
|
+
"""
|
|
93
|
+
loggers_dict = {}
|
|
94
|
+
|
|
95
|
+
# Get root logger
|
|
96
|
+
root_logger = logging.getLogger()
|
|
97
|
+
loggers_dict["ROOT"] = self._get_logger_info(root_logger)
|
|
98
|
+
|
|
99
|
+
# Get all other loggers
|
|
100
|
+
# Access the internal logger dictionary
|
|
101
|
+
logger_dict = logging.Logger.manager.loggerDict
|
|
102
|
+
for name, logger_item in sorted(logger_dict.items()):
|
|
103
|
+
# Skip PlaceHolder objects, only include actual Logger instances
|
|
104
|
+
if isinstance(logger_item, logging.Logger):
|
|
105
|
+
loggers_dict[name] = self._get_logger_info(logger_item)
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"levels": VALID_LEVELS,
|
|
109
|
+
"loggers": loggers_dict,
|
|
110
|
+
"groups": {}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
def get_logger(self, name: str) -> Optional[Dict[str, Any]]:
|
|
114
|
+
"""
|
|
115
|
+
Get a single logger by name.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
name: Logger name (use "ROOT" for root logger)
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Logger info or None if not found
|
|
122
|
+
"""
|
|
123
|
+
if name == "ROOT":
|
|
124
|
+
logger = logging.getLogger()
|
|
125
|
+
else:
|
|
126
|
+
# Check if logger exists
|
|
127
|
+
if name not in logging.Logger.manager.loggerDict:
|
|
128
|
+
return None
|
|
129
|
+
logger = logging.getLogger(name)
|
|
130
|
+
|
|
131
|
+
return self._get_logger_info(logger)
|
|
132
|
+
|
|
133
|
+
def set_logger_level(self, name: str, level: Optional[str]) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Set logger level.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
name: Logger name (use "ROOT" for root logger)
|
|
139
|
+
level: Level name (DEBUG, INFO, WARN, ERROR, etc.) or None to clear
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if successful, False if logger not found or invalid level
|
|
143
|
+
"""
|
|
144
|
+
# Validate level
|
|
145
|
+
if level is not None and level not in LEVEL_MAPPING:
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
# Get logger
|
|
149
|
+
if name == "ROOT":
|
|
150
|
+
logger = logging.getLogger()
|
|
151
|
+
else:
|
|
152
|
+
logger = logging.getLogger(name)
|
|
153
|
+
|
|
154
|
+
# Set level
|
|
155
|
+
if level is None:
|
|
156
|
+
# Clear level (set to NOTSET to inherit from parent)
|
|
157
|
+
logger.setLevel(logging.NOTSET)
|
|
158
|
+
else:
|
|
159
|
+
python_level = LEVEL_MAPPING[level]
|
|
160
|
+
logger.setLevel(python_level)
|
|
161
|
+
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
def clear_logger_level(self, name: str) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Clear logger level (reset to inherited).
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
name: Logger name
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True if successful
|
|
173
|
+
"""
|
|
174
|
+
return self.set_logger_level(name, None)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def create_default_loggers_endpoint() -> LoggersEndpoint:
|
|
178
|
+
"""
|
|
179
|
+
Create loggers endpoint.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
LoggersEndpoint instance
|
|
183
|
+
"""
|
|
184
|
+
return LoggersEndpoint()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Actuator Mappings Endpoint.
|
|
3
|
+
Shows all registered request mappings (routes) in the application.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, List
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from fastapi.routing import APIRoute
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MappingsEndpoint:
|
|
12
|
+
"""
|
|
13
|
+
Mappings endpoint for Spring Boot Actuator compatibility.
|
|
14
|
+
|
|
15
|
+
Shows all registered FastAPI routes with their HTTP methods and handlers.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, app: FastAPI):
|
|
19
|
+
"""
|
|
20
|
+
Args:
|
|
21
|
+
app: FastAPI application instance
|
|
22
|
+
"""
|
|
23
|
+
self.app = app
|
|
24
|
+
|
|
25
|
+
def get_mappings(self) -> Dict[str, Any]:
|
|
26
|
+
"""
|
|
27
|
+
Get all request mappings.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dictionary with contexts and dispatcher servlet mappings
|
|
31
|
+
"""
|
|
32
|
+
mappings = []
|
|
33
|
+
|
|
34
|
+
# Iterate through all routes
|
|
35
|
+
for route in self.app.routes:
|
|
36
|
+
if isinstance(route, APIRoute):
|
|
37
|
+
# Get handler info
|
|
38
|
+
handler_name = route.endpoint.__name__ if hasattr(route, 'endpoint') else "unknown"
|
|
39
|
+
handler_module = route.endpoint.__module__ if hasattr(route, 'endpoint') else "unknown"
|
|
40
|
+
|
|
41
|
+
# Get methods
|
|
42
|
+
methods = list(route.methods) if hasattr(route, 'methods') else []
|
|
43
|
+
|
|
44
|
+
# Build mapping entry
|
|
45
|
+
mapping = {
|
|
46
|
+
"handler": f"{handler_module}.{handler_name}",
|
|
47
|
+
"predicate": f"{{{', '.join(methods)}}} {route.path}",
|
|
48
|
+
"details": {
|
|
49
|
+
"requestMappingConditions": {
|
|
50
|
+
"methods": methods,
|
|
51
|
+
"patterns": [route.path]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
mappings.append(mapping)
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
"contexts": {
|
|
60
|
+
"application": {
|
|
61
|
+
"mappings": {
|
|
62
|
+
"dispatcherServlet": {
|
|
63
|
+
"details": {
|
|
64
|
+
"requestMappingConditions": {
|
|
65
|
+
"patterns": []
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"dispatcherHandlers": {
|
|
69
|
+
"webHandler": mappings
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def create_default_mappings_endpoint(app: FastAPI) -> MappingsEndpoint:
|
|
79
|
+
"""
|
|
80
|
+
Create mappings endpoint for a FastAPI application.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
app: FastAPI application instance
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
MappingsEndpoint instance
|
|
87
|
+
"""
|
|
88
|
+
return MappingsEndpoint(app)
|