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,521 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI integration for Spring-Ready Python.
|
|
3
|
+
Adds actuator endpoints and integrates with Eureka/Config Server.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from fastapi import FastAPI, Response, Request, Body
|
|
9
|
+
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
10
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
11
|
+
|
|
12
|
+
from ..actuator import (
|
|
13
|
+
HealthEndpoint,
|
|
14
|
+
InfoEndpoint,
|
|
15
|
+
PrometheusEndpoint,
|
|
16
|
+
ActuatorDiscoveryEndpoint,
|
|
17
|
+
MetricsEndpoint,
|
|
18
|
+
EnvEndpoint,
|
|
19
|
+
LoggersEndpoint,
|
|
20
|
+
MappingsEndpoint,
|
|
21
|
+
ThreadDumpEndpoint,
|
|
22
|
+
HttpTraceEndpoint,
|
|
23
|
+
HttpExchangesEndpoint,
|
|
24
|
+
LogfileEndpoint,
|
|
25
|
+
RefreshEndpoint,
|
|
26
|
+
BeansEndpoint,
|
|
27
|
+
ConfigPropsEndpoint,
|
|
28
|
+
ScheduledTasksEndpoint,
|
|
29
|
+
HeapdumpEndpoint,
|
|
30
|
+
CachesEndpoint,
|
|
31
|
+
AuditEventsEndpoint,
|
|
32
|
+
create_default_health_endpoint,
|
|
33
|
+
create_default_info_endpoint,
|
|
34
|
+
create_default_prometheus_endpoint,
|
|
35
|
+
create_default_discovery_endpoint,
|
|
36
|
+
create_default_metrics_endpoint,
|
|
37
|
+
create_default_env_endpoint,
|
|
38
|
+
create_default_loggers_endpoint,
|
|
39
|
+
create_default_mappings_endpoint,
|
|
40
|
+
create_default_threaddump_endpoint,
|
|
41
|
+
create_default_httptrace_endpoint,
|
|
42
|
+
create_default_httpexchanges_endpoint,
|
|
43
|
+
create_default_logfile_endpoint,
|
|
44
|
+
create_default_refresh_endpoint,
|
|
45
|
+
create_default_beans_endpoint,
|
|
46
|
+
create_default_configprops_endpoint,
|
|
47
|
+
create_default_scheduledtasks_endpoint,
|
|
48
|
+
create_default_heapdump_endpoint,
|
|
49
|
+
create_default_caches_endpoint,
|
|
50
|
+
create_default_auditevents_endpoint
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
logger = logging.getLogger(__name__)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FastAPIActuatorIntegration:
|
|
57
|
+
"""
|
|
58
|
+
Integrates Spring Boot Actuator endpoints with FastAPI.
|
|
59
|
+
|
|
60
|
+
Adds the following endpoints:
|
|
61
|
+
- GET /actuator - Discovery endpoint with HAL JSON
|
|
62
|
+
- GET /actuator/health - Health status
|
|
63
|
+
- GET /actuator/info - Application information
|
|
64
|
+
- GET /actuator/prometheus - Prometheus metrics
|
|
65
|
+
- GET /actuator/metrics - Metrics list
|
|
66
|
+
- GET /actuator/metrics/{name} - Individual metric
|
|
67
|
+
- GET /actuator/env - Environment variables
|
|
68
|
+
- GET /actuator/env/{property} - Single property
|
|
69
|
+
- GET /actuator/loggers - All loggers
|
|
70
|
+
- GET /actuator/loggers/{name} - Single logger
|
|
71
|
+
- POST /actuator/loggers/{name} - Set logger level
|
|
72
|
+
- GET /actuator/mappings - Request mappings
|
|
73
|
+
- GET /actuator/threaddump - Thread dump
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
app: FastAPI,
|
|
79
|
+
base_url: str,
|
|
80
|
+
health_endpoint: Optional[HealthEndpoint] = None,
|
|
81
|
+
info_endpoint: Optional[InfoEndpoint] = None,
|
|
82
|
+
prometheus_endpoint: Optional[PrometheusEndpoint] = None,
|
|
83
|
+
discovery_endpoint: Optional[ActuatorDiscoveryEndpoint] = None,
|
|
84
|
+
metrics_endpoint: Optional[MetricsEndpoint] = None,
|
|
85
|
+
env_endpoint: Optional[EnvEndpoint] = None,
|
|
86
|
+
loggers_endpoint: Optional[LoggersEndpoint] = None,
|
|
87
|
+
mappings_endpoint: Optional[MappingsEndpoint] = None,
|
|
88
|
+
threaddump_endpoint: Optional[ThreadDumpEndpoint] = None,
|
|
89
|
+
httptrace_endpoint: Optional[HttpTraceEndpoint] = None,
|
|
90
|
+
httpexchanges_endpoint: Optional[HttpExchangesEndpoint] = None,
|
|
91
|
+
logfile_endpoint: Optional[LogfileEndpoint] = None,
|
|
92
|
+
refresh_endpoint: Optional[RefreshEndpoint] = None,
|
|
93
|
+
beans_endpoint: Optional[BeansEndpoint] = None,
|
|
94
|
+
configprops_endpoint: Optional[ConfigPropsEndpoint] = None,
|
|
95
|
+
scheduledtasks_endpoint: Optional[ScheduledTasksEndpoint] = None,
|
|
96
|
+
heapdump_endpoint: Optional[HeapdumpEndpoint] = None,
|
|
97
|
+
caches_endpoint: Optional[CachesEndpoint] = None,
|
|
98
|
+
auditevents_endpoint: Optional[AuditEventsEndpoint] = None,
|
|
99
|
+
enable_cors: bool = True
|
|
100
|
+
):
|
|
101
|
+
"""
|
|
102
|
+
Args:
|
|
103
|
+
app: FastAPI application
|
|
104
|
+
base_url: Base URL for the application (e.g., "http://localhost:8080")
|
|
105
|
+
health_endpoint: Health endpoint (creates default if None)
|
|
106
|
+
info_endpoint: Info endpoint (creates default if None)
|
|
107
|
+
prometheus_endpoint: Prometheus endpoint (creates default if None)
|
|
108
|
+
discovery_endpoint: Discovery endpoint (creates default if None)
|
|
109
|
+
metrics_endpoint: Metrics endpoint (creates default if None)
|
|
110
|
+
env_endpoint: Environment endpoint (creates default if None)
|
|
111
|
+
loggers_endpoint: Loggers endpoint (creates default if None)
|
|
112
|
+
mappings_endpoint: Mappings endpoint (creates default if None)
|
|
113
|
+
threaddump_endpoint: Thread dump endpoint (creates default if None)
|
|
114
|
+
httptrace_endpoint: HTTP trace endpoint (creates default if None)
|
|
115
|
+
httpexchanges_endpoint: HTTP exchanges endpoint (creates default if None)
|
|
116
|
+
logfile_endpoint: Logfile endpoint (creates default if None)
|
|
117
|
+
refresh_endpoint: Refresh endpoint (creates default if None)
|
|
118
|
+
enable_cors: Whether to enable CORS for actuator endpoints (default: True)
|
|
119
|
+
"""
|
|
120
|
+
self.app = app
|
|
121
|
+
self.base_url = base_url
|
|
122
|
+
self.health_endpoint = health_endpoint or create_default_health_endpoint()
|
|
123
|
+
self.info_endpoint = info_endpoint or create_default_info_endpoint()
|
|
124
|
+
self.prometheus_endpoint = prometheus_endpoint or create_default_prometheus_endpoint()
|
|
125
|
+
self.discovery_endpoint = discovery_endpoint or create_default_discovery_endpoint(base_url)
|
|
126
|
+
self.metrics_endpoint = metrics_endpoint or create_default_metrics_endpoint()
|
|
127
|
+
self.env_endpoint = env_endpoint or create_default_env_endpoint()
|
|
128
|
+
self.loggers_endpoint = loggers_endpoint or create_default_loggers_endpoint()
|
|
129
|
+
self.mappings_endpoint = mappings_endpoint or create_default_mappings_endpoint(app)
|
|
130
|
+
self.threaddump_endpoint = threaddump_endpoint or create_default_threaddump_endpoint()
|
|
131
|
+
self.httptrace_endpoint = httptrace_endpoint or create_default_httptrace_endpoint()
|
|
132
|
+
self.httpexchanges_endpoint = httpexchanges_endpoint or create_default_httpexchanges_endpoint()
|
|
133
|
+
self.logfile_endpoint = logfile_endpoint or create_default_logfile_endpoint()
|
|
134
|
+
self.refresh_endpoint = refresh_endpoint or create_default_refresh_endpoint()
|
|
135
|
+
self.beans_endpoint = beans_endpoint or create_default_beans_endpoint(app)
|
|
136
|
+
self.configprops_endpoint = configprops_endpoint or create_default_configprops_endpoint()
|
|
137
|
+
self.scheduledtasks_endpoint = scheduledtasks_endpoint or create_default_scheduledtasks_endpoint()
|
|
138
|
+
self.heapdump_endpoint = heapdump_endpoint or create_default_heapdump_endpoint()
|
|
139
|
+
self.caches_endpoint = caches_endpoint or create_default_caches_endpoint()
|
|
140
|
+
self.auditevents_endpoint = auditevents_endpoint or create_default_auditevents_endpoint()
|
|
141
|
+
|
|
142
|
+
# NOTE: Middleware is now added in core.py during __init__ (before app starts)
|
|
143
|
+
# This avoids the "Cannot add middleware after startup" error
|
|
144
|
+
|
|
145
|
+
if enable_cors:
|
|
146
|
+
self._enable_cors()
|
|
147
|
+
|
|
148
|
+
self._register_endpoints()
|
|
149
|
+
|
|
150
|
+
def _enable_cors(self) -> None:
|
|
151
|
+
"""Enable CORS for actuator endpoints"""
|
|
152
|
+
self.app.add_middleware(
|
|
153
|
+
CORSMiddleware,
|
|
154
|
+
allow_origins=["*"], # Allow all origins for Spring Boot Admin compatibility
|
|
155
|
+
allow_credentials=True,
|
|
156
|
+
allow_methods=["GET", "POST", "OPTIONS", "DELETE"],
|
|
157
|
+
allow_headers=["*"],
|
|
158
|
+
)
|
|
159
|
+
logger.info("CORS enabled for actuator endpoints")
|
|
160
|
+
|
|
161
|
+
def _add_options_handler(self, path: str) -> None:
|
|
162
|
+
"""Add OPTIONS handler for a specific path to support CORS preflight"""
|
|
163
|
+
@self.app.options(path, tags=["actuator"], include_in_schema=False)
|
|
164
|
+
async def options_handler():
|
|
165
|
+
"""Handle OPTIONS preflight request"""
|
|
166
|
+
return Response(status_code=200)
|
|
167
|
+
|
|
168
|
+
def _register_endpoints(self) -> None:
|
|
169
|
+
"""Register actuator endpoints with FastAPI"""
|
|
170
|
+
|
|
171
|
+
# Discovery endpoint - lists all available actuator endpoints
|
|
172
|
+
@self.app.get("/actuator", tags=["actuator"], include_in_schema=False)
|
|
173
|
+
async def actuator_discovery():
|
|
174
|
+
"""Actuator discovery endpoint with HAL JSON"""
|
|
175
|
+
return JSONResponse(content=self.discovery_endpoint.to_dict())
|
|
176
|
+
|
|
177
|
+
self._add_options_handler("/actuator")
|
|
178
|
+
|
|
179
|
+
# Health endpoint
|
|
180
|
+
@self.app.get("/actuator/health", tags=["actuator"], include_in_schema=False)
|
|
181
|
+
async def health():
|
|
182
|
+
"""Health check endpoint"""
|
|
183
|
+
health_data = self.health_endpoint.check()
|
|
184
|
+
status_code = 503 if health_data["status"] == "DOWN" else 200
|
|
185
|
+
return JSONResponse(content=health_data, status_code=status_code)
|
|
186
|
+
|
|
187
|
+
self._add_options_handler("/actuator/health")
|
|
188
|
+
|
|
189
|
+
# Info endpoint
|
|
190
|
+
@self.app.get("/actuator/info", tags=["actuator"], include_in_schema=False)
|
|
191
|
+
async def info():
|
|
192
|
+
"""Application info endpoint"""
|
|
193
|
+
return JSONResponse(content=self.info_endpoint.get_info())
|
|
194
|
+
|
|
195
|
+
self._add_options_handler("/actuator/info")
|
|
196
|
+
|
|
197
|
+
# Prometheus endpoint
|
|
198
|
+
@self.app.get("/actuator/prometheus", tags=["actuator"], include_in_schema=False)
|
|
199
|
+
async def prometheus():
|
|
200
|
+
"""Prometheus metrics endpoint"""
|
|
201
|
+
metrics = self.prometheus_endpoint.get_metrics()
|
|
202
|
+
return Response(
|
|
203
|
+
content=metrics,
|
|
204
|
+
media_type=self.prometheus_endpoint.content_type
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
self._add_options_handler("/actuator/prometheus")
|
|
208
|
+
|
|
209
|
+
# Metrics endpoint - list all metrics
|
|
210
|
+
@self.app.get("/actuator/metrics", tags=["actuator"], include_in_schema=False)
|
|
211
|
+
async def metrics_list():
|
|
212
|
+
"""List all available metrics"""
|
|
213
|
+
return JSONResponse(content=self.metrics_endpoint.get_metric_names())
|
|
214
|
+
|
|
215
|
+
self._add_options_handler("/actuator/metrics")
|
|
216
|
+
|
|
217
|
+
# Metrics endpoint - get individual metric
|
|
218
|
+
@self.app.get("/actuator/metrics/{metric_name}", tags=["actuator"], include_in_schema=False)
|
|
219
|
+
async def metrics_detail(metric_name: str):
|
|
220
|
+
"""Get individual metric details"""
|
|
221
|
+
metric = self.metrics_endpoint.get_metric(metric_name)
|
|
222
|
+
if metric is None:
|
|
223
|
+
return JSONResponse(
|
|
224
|
+
content={"error": f"Metric '{metric_name}' not found"},
|
|
225
|
+
status_code=404
|
|
226
|
+
)
|
|
227
|
+
return JSONResponse(content=metric)
|
|
228
|
+
|
|
229
|
+
# Environment endpoint - all environment variables
|
|
230
|
+
@self.app.get("/actuator/env", tags=["actuator"], include_in_schema=False)
|
|
231
|
+
async def env_all():
|
|
232
|
+
"""Get all environment variables (sanitized)"""
|
|
233
|
+
return JSONResponse(content=self.env_endpoint.get_environment())
|
|
234
|
+
|
|
235
|
+
self._add_options_handler("/actuator/env")
|
|
236
|
+
|
|
237
|
+
# Environment endpoint - single property
|
|
238
|
+
@self.app.get("/actuator/env/{property_name:path}", tags=["actuator"], include_in_schema=False)
|
|
239
|
+
async def env_property(property_name: str):
|
|
240
|
+
"""Get single environment property"""
|
|
241
|
+
prop = self.env_endpoint.get_property(property_name)
|
|
242
|
+
if prop is None:
|
|
243
|
+
return JSONResponse(
|
|
244
|
+
content={"error": f"Property '{property_name}' not found"},
|
|
245
|
+
status_code=404
|
|
246
|
+
)
|
|
247
|
+
return JSONResponse(content=prop)
|
|
248
|
+
|
|
249
|
+
# Loggers endpoint - all loggers
|
|
250
|
+
@self.app.get("/actuator/loggers", tags=["actuator"], include_in_schema=False)
|
|
251
|
+
async def loggers_all():
|
|
252
|
+
"""Get all loggers"""
|
|
253
|
+
return JSONResponse(content=self.loggers_endpoint.get_all_loggers())
|
|
254
|
+
|
|
255
|
+
self._add_options_handler("/actuator/loggers")
|
|
256
|
+
|
|
257
|
+
# Loggers endpoint - single logger
|
|
258
|
+
@self.app.get("/actuator/loggers/{logger_name:path}", tags=["actuator"], include_in_schema=False)
|
|
259
|
+
async def loggers_single(logger_name: str):
|
|
260
|
+
"""Get single logger"""
|
|
261
|
+
logger_info = self.loggers_endpoint.get_logger(logger_name)
|
|
262
|
+
if logger_info is None:
|
|
263
|
+
return JSONResponse(
|
|
264
|
+
content={"error": f"Logger '{logger_name}' not found"},
|
|
265
|
+
status_code=404
|
|
266
|
+
)
|
|
267
|
+
return JSONResponse(content=logger_info)
|
|
268
|
+
|
|
269
|
+
# Loggers endpoint - set logger level
|
|
270
|
+
@self.app.post("/actuator/loggers/{logger_name:path}", tags=["actuator"], include_in_schema=False)
|
|
271
|
+
async def loggers_set_level(logger_name: str, body: dict = Body(...)):
|
|
272
|
+
"""Set logger level"""
|
|
273
|
+
configured_level = body.get("configuredLevel")
|
|
274
|
+
success = self.loggers_endpoint.set_logger_level(logger_name, configured_level)
|
|
275
|
+
if not success:
|
|
276
|
+
return JSONResponse(
|
|
277
|
+
content={"error": f"Failed to set logger level for '{logger_name}'"},
|
|
278
|
+
status_code=400
|
|
279
|
+
)
|
|
280
|
+
return Response(status_code=204)
|
|
281
|
+
|
|
282
|
+
# Mappings endpoint
|
|
283
|
+
@self.app.get("/actuator/mappings", tags=["actuator"], include_in_schema=False)
|
|
284
|
+
async def mappings():
|
|
285
|
+
"""Get request mappings"""
|
|
286
|
+
return JSONResponse(content=self.mappings_endpoint.get_mappings())
|
|
287
|
+
|
|
288
|
+
self._add_options_handler("/actuator/mappings")
|
|
289
|
+
|
|
290
|
+
# Thread dump endpoint
|
|
291
|
+
@self.app.get("/actuator/threaddump", tags=["actuator"], include_in_schema=False)
|
|
292
|
+
async def threaddump():
|
|
293
|
+
"""Get thread dump"""
|
|
294
|
+
return JSONResponse(content=self.threaddump_endpoint.get_thread_dump())
|
|
295
|
+
|
|
296
|
+
self._add_options_handler("/actuator/threaddump")
|
|
297
|
+
|
|
298
|
+
# HTTP Trace endpoint (older Spring Boot versions)
|
|
299
|
+
@self.app.get("/actuator/httptrace", tags=["actuator"], include_in_schema=False)
|
|
300
|
+
async def httptrace():
|
|
301
|
+
"""Get HTTP trace (request/response history)"""
|
|
302
|
+
return JSONResponse(content=self.httptrace_endpoint.get_traces())
|
|
303
|
+
|
|
304
|
+
self._add_options_handler("/actuator/httptrace")
|
|
305
|
+
|
|
306
|
+
# HTTP Exchanges endpoint (newer Spring Boot 2.2+)
|
|
307
|
+
@self.app.get("/actuator/httpexchanges", tags=["actuator"], include_in_schema=False)
|
|
308
|
+
async def httpexchanges():
|
|
309
|
+
"""Get HTTP exchanges (request/response history)"""
|
|
310
|
+
return JSONResponse(content=self.httpexchanges_endpoint.get_exchanges())
|
|
311
|
+
|
|
312
|
+
self._add_options_handler("/actuator/httpexchanges")
|
|
313
|
+
|
|
314
|
+
# Dump/Trace endpoint (alias for threaddump, some Spring Boot Admin versions use this)
|
|
315
|
+
@self.app.get("/actuator/dump", tags=["actuator"], include_in_schema=False)
|
|
316
|
+
async def dump():
|
|
317
|
+
"""Get thread dump (alias)"""
|
|
318
|
+
return JSONResponse(content=self.threaddump_endpoint.get_thread_dump())
|
|
319
|
+
|
|
320
|
+
self._add_options_handler("/actuator/dump")
|
|
321
|
+
|
|
322
|
+
# Trace endpoint (simple trace, for older versions)
|
|
323
|
+
@self.app.get("/actuator/trace", tags=["actuator"], include_in_schema=False)
|
|
324
|
+
async def trace():
|
|
325
|
+
"""Get trace information"""
|
|
326
|
+
return JSONResponse(content=self.httptrace_endpoint.get_traces())
|
|
327
|
+
|
|
328
|
+
self._add_options_handler("/actuator/trace")
|
|
329
|
+
|
|
330
|
+
# Logfile endpoint - returns application log file
|
|
331
|
+
@self.app.get("/actuator/logfile", tags=["actuator"], include_in_schema=False)
|
|
332
|
+
async def logfile(request: Request):
|
|
333
|
+
"""Get application log file"""
|
|
334
|
+
# Check if logfile is available
|
|
335
|
+
if not self.logfile_endpoint.is_available():
|
|
336
|
+
return JSONResponse(
|
|
337
|
+
content={"error": "Log file not configured or not available"},
|
|
338
|
+
status_code=404
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Get Range header if present
|
|
342
|
+
range_header = request.headers.get("Range")
|
|
343
|
+
|
|
344
|
+
# Get log file content
|
|
345
|
+
content, content_range, status_code = self.logfile_endpoint.get_logfile(range_header)
|
|
346
|
+
|
|
347
|
+
if status_code == 404:
|
|
348
|
+
return JSONResponse(
|
|
349
|
+
content={"error": "Log file not found"},
|
|
350
|
+
status_code=404
|
|
351
|
+
)
|
|
352
|
+
elif status_code == 416:
|
|
353
|
+
return Response(status_code=416) # Range Not Satisfiable
|
|
354
|
+
elif status_code == 500:
|
|
355
|
+
return JSONResponse(
|
|
356
|
+
content={"error": "Error reading log file"},
|
|
357
|
+
status_code=500
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Return log file content
|
|
361
|
+
headers = {}
|
|
362
|
+
if content_range:
|
|
363
|
+
headers["Content-Range"] = content_range
|
|
364
|
+
|
|
365
|
+
return Response(
|
|
366
|
+
content=content,
|
|
367
|
+
media_type="text/plain; charset=UTF-8",
|
|
368
|
+
status_code=status_code,
|
|
369
|
+
headers=headers
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
self._add_options_handler("/actuator/logfile")
|
|
373
|
+
|
|
374
|
+
# Refresh endpoint - reload configuration from Config Server
|
|
375
|
+
@self.app.post("/actuator/refresh", tags=["actuator"], include_in_schema=False)
|
|
376
|
+
async def refresh():
|
|
377
|
+
"""Refresh configuration from Config Server"""
|
|
378
|
+
changed_keys = self.refresh_endpoint.refresh()
|
|
379
|
+
return JSONResponse(content=changed_keys)
|
|
380
|
+
|
|
381
|
+
self._add_options_handler("/actuator/refresh")
|
|
382
|
+
|
|
383
|
+
# Beans endpoint
|
|
384
|
+
@self.app.get("/actuator/beans", tags=["actuator"], include_in_schema=False)
|
|
385
|
+
async def beans():
|
|
386
|
+
"""Get application beans/components"""
|
|
387
|
+
return JSONResponse(content=self.beans_endpoint.get_beans())
|
|
388
|
+
|
|
389
|
+
self._add_options_handler("/actuator/beans")
|
|
390
|
+
|
|
391
|
+
# ConfigProps endpoint - all configuration properties
|
|
392
|
+
@self.app.get("/actuator/configprops", tags=["actuator"], include_in_schema=False)
|
|
393
|
+
async def configprops_all():
|
|
394
|
+
"""Get all configuration properties"""
|
|
395
|
+
return JSONResponse(content=self.configprops_endpoint.get_config_props())
|
|
396
|
+
|
|
397
|
+
self._add_options_handler("/actuator/configprops")
|
|
398
|
+
|
|
399
|
+
# ConfigProps endpoint - filtered by prefix
|
|
400
|
+
@self.app.get("/actuator/configprops/{prefix}", tags=["actuator"], include_in_schema=False)
|
|
401
|
+
async def configprops_prefix(prefix: str):
|
|
402
|
+
"""Get configuration properties by prefix"""
|
|
403
|
+
props = self.configprops_endpoint.get_config_props_by_prefix(prefix)
|
|
404
|
+
if not props.get("contexts", {}).get("application", {}).get("beans"):
|
|
405
|
+
return JSONResponse(content={}, status_code=404)
|
|
406
|
+
return JSONResponse(content=props)
|
|
407
|
+
|
|
408
|
+
# ScheduledTasks endpoint
|
|
409
|
+
@self.app.get("/actuator/scheduledtasks", tags=["actuator"], include_in_schema=False)
|
|
410
|
+
async def scheduledtasks():
|
|
411
|
+
"""Get scheduled tasks"""
|
|
412
|
+
return JSONResponse(content=self.scheduledtasks_endpoint.get_scheduled_tasks())
|
|
413
|
+
|
|
414
|
+
self._add_options_handler("/actuator/scheduledtasks")
|
|
415
|
+
|
|
416
|
+
# Heapdump endpoint - returns memory statistics (JSON, not binary)
|
|
417
|
+
@self.app.get("/actuator/heapdump", tags=["actuator"], include_in_schema=False)
|
|
418
|
+
async def heapdump():
|
|
419
|
+
"""Get memory statistics"""
|
|
420
|
+
return JSONResponse(content=self.heapdump_endpoint.get_memory_stats())
|
|
421
|
+
|
|
422
|
+
self._add_options_handler("/actuator/heapdump")
|
|
423
|
+
|
|
424
|
+
# Caches endpoint - all caches
|
|
425
|
+
@self.app.get("/actuator/caches", tags=["actuator"], include_in_schema=False)
|
|
426
|
+
async def caches_all():
|
|
427
|
+
"""Get all caches"""
|
|
428
|
+
return JSONResponse(content=self.caches_endpoint.get_caches())
|
|
429
|
+
|
|
430
|
+
self._add_options_handler("/actuator/caches")
|
|
431
|
+
|
|
432
|
+
# Caches endpoint - single cache
|
|
433
|
+
@self.app.get("/actuator/caches/{cache_name}", tags=["actuator"], include_in_schema=False)
|
|
434
|
+
async def caches_single(cache_name: str, cacheManager: Optional[str] = None):
|
|
435
|
+
"""Get single cache"""
|
|
436
|
+
cache = self.caches_endpoint.get_cache(cache_name, cacheManager)
|
|
437
|
+
if cache is None:
|
|
438
|
+
return JSONResponse(
|
|
439
|
+
content={"error": f"Cache '{cache_name}' not found"},
|
|
440
|
+
status_code=404
|
|
441
|
+
)
|
|
442
|
+
return JSONResponse(content=cache)
|
|
443
|
+
|
|
444
|
+
# Caches endpoint - evict all
|
|
445
|
+
@self.app.delete("/actuator/caches", tags=["actuator"], include_in_schema=False)
|
|
446
|
+
async def caches_evict_all():
|
|
447
|
+
"""Evict all caches"""
|
|
448
|
+
self.caches_endpoint.evict_all_caches()
|
|
449
|
+
return Response(status_code=204)
|
|
450
|
+
|
|
451
|
+
# Caches endpoint - evict single cache
|
|
452
|
+
@self.app.delete("/actuator/caches/{cache_name}", tags=["actuator"], include_in_schema=False)
|
|
453
|
+
async def caches_evict(cache_name: str, cacheManager: Optional[str] = None):
|
|
454
|
+
"""Evict single cache"""
|
|
455
|
+
success = self.caches_endpoint.evict_cache(cache_name, cacheManager)
|
|
456
|
+
if not success:
|
|
457
|
+
return JSONResponse(
|
|
458
|
+
content={"error": f"Cache '{cache_name}' not found"},
|
|
459
|
+
status_code=404
|
|
460
|
+
)
|
|
461
|
+
return Response(status_code=204)
|
|
462
|
+
|
|
463
|
+
# AuditEvents endpoint
|
|
464
|
+
@self.app.get("/actuator/auditevents", tags=["actuator"], include_in_schema=False)
|
|
465
|
+
async def auditevents(
|
|
466
|
+
principal: Optional[str] = None,
|
|
467
|
+
after: Optional[str] = None,
|
|
468
|
+
type: Optional[str] = None
|
|
469
|
+
):
|
|
470
|
+
"""Get audit events with optional filtering"""
|
|
471
|
+
return JSONResponse(
|
|
472
|
+
content=self.auditevents_endpoint.get_events(
|
|
473
|
+
principal=principal,
|
|
474
|
+
after=after,
|
|
475
|
+
event_type=type
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
self._add_options_handler("/actuator/auditevents")
|
|
480
|
+
|
|
481
|
+
# OpenAPI v3 endpoint (Spring Boot Admin compatibility)
|
|
482
|
+
@self.app.get("/v3/api-docs", tags=["openapi"])
|
|
483
|
+
async def openapi_v3():
|
|
484
|
+
"""OpenAPI v3 specification (Spring Boot Admin compatibility)"""
|
|
485
|
+
return JSONResponse(content=self.app.openapi())
|
|
486
|
+
|
|
487
|
+
self._add_options_handler("/v3/api-docs")
|
|
488
|
+
|
|
489
|
+
logger.info("Actuator endpoints registered with OPTIONS support: discovery, health, info, prometheus, metrics, env, loggers, mappings, threaddump, httptrace, httpexchanges, dump, trace, logfile, refresh, beans, configprops, scheduledtasks, heapdump, caches, auditevents, v3/api-docs")
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def add_actuator_endpoints(
|
|
493
|
+
app: FastAPI,
|
|
494
|
+
base_url: str,
|
|
495
|
+
health_endpoint: Optional[HealthEndpoint] = None,
|
|
496
|
+
info_endpoint: Optional[InfoEndpoint] = None,
|
|
497
|
+
prometheus_endpoint: Optional[PrometheusEndpoint] = None,
|
|
498
|
+
enable_cors: bool = True
|
|
499
|
+
) -> FastAPIActuatorIntegration:
|
|
500
|
+
"""
|
|
501
|
+
Add Spring Boot Actuator endpoints to FastAPI app.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
app: FastAPI application
|
|
505
|
+
base_url: Base URL for the application (e.g., "http://localhost:8080")
|
|
506
|
+
health_endpoint: Custom health endpoint (optional)
|
|
507
|
+
info_endpoint: Custom info endpoint (optional)
|
|
508
|
+
prometheus_endpoint: Custom prometheus endpoint (optional)
|
|
509
|
+
enable_cors: Enable CORS for actuator endpoints (default: True)
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
FastAPIActuatorIntegration instance
|
|
513
|
+
"""
|
|
514
|
+
return FastAPIActuatorIntegration(
|
|
515
|
+
app=app,
|
|
516
|
+
base_url=base_url,
|
|
517
|
+
health_endpoint=health_endpoint,
|
|
518
|
+
info_endpoint=info_endpoint,
|
|
519
|
+
prometheus_endpoint=prometheus_endpoint,
|
|
520
|
+
enable_cors=enable_cors
|
|
521
|
+
)
|
spring_ready/retry.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exponential backoff retry logic.
|
|
3
|
+
Matches Spring Cloud Config's retry behavior.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Callable, TypeVar, Optional
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
T = TypeVar('T')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RetryConfig:
|
|
16
|
+
"""Configuration for exponential backoff retry"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
max_attempts: int = 6,
|
|
21
|
+
initial_interval: float = 1.0,
|
|
22
|
+
max_interval: float = 2.0,
|
|
23
|
+
multiplier: float = 1.1
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Args:
|
|
27
|
+
max_attempts: Maximum number of retry attempts
|
|
28
|
+
initial_interval: Initial retry interval in seconds
|
|
29
|
+
max_interval: Maximum retry interval in seconds
|
|
30
|
+
multiplier: Multiplier for exponential backoff
|
|
31
|
+
"""
|
|
32
|
+
self.max_attempts = max_attempts
|
|
33
|
+
self.initial_interval = initial_interval
|
|
34
|
+
self.max_interval = max_interval
|
|
35
|
+
self.multiplier = multiplier
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def retry_with_backoff(
|
|
39
|
+
func: Callable[[], T],
|
|
40
|
+
config: RetryConfig,
|
|
41
|
+
operation_name: str,
|
|
42
|
+
fail_fast: bool = True
|
|
43
|
+
) -> Optional[T]:
|
|
44
|
+
"""
|
|
45
|
+
Execute function with exponential backoff retry.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
func: Function to execute
|
|
49
|
+
config: Retry configuration
|
|
50
|
+
operation_name: Name of operation for logging
|
|
51
|
+
fail_fast: If True, raise exception after max attempts. If False, return None.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Result of func() if successful, None if fail_fast=False and all retries exhausted
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
Last exception if fail_fast=True and all retries exhausted
|
|
58
|
+
"""
|
|
59
|
+
last_exception = None
|
|
60
|
+
interval = config.initial_interval
|
|
61
|
+
|
|
62
|
+
for attempt in range(1, config.max_attempts + 1):
|
|
63
|
+
try:
|
|
64
|
+
logger.debug(f"{operation_name}: Attempt {attempt}/{config.max_attempts}")
|
|
65
|
+
result = func()
|
|
66
|
+
|
|
67
|
+
if attempt > 1:
|
|
68
|
+
logger.info(f"{operation_name}: Succeeded on attempt {attempt}")
|
|
69
|
+
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
last_exception = e
|
|
74
|
+
|
|
75
|
+
if attempt == config.max_attempts:
|
|
76
|
+
logger.error(
|
|
77
|
+
f"{operation_name}: Failed after {config.max_attempts} attempts. "
|
|
78
|
+
f"Last error: {e}"
|
|
79
|
+
)
|
|
80
|
+
if fail_fast:
|
|
81
|
+
raise
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
logger.warning(
|
|
85
|
+
f"{operation_name}: Attempt {attempt} failed: {e}. "
|
|
86
|
+
f"Retrying in {interval:.2f}s..."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
time.sleep(interval)
|
|
90
|
+
|
|
91
|
+
# Exponential backoff with max limit
|
|
92
|
+
interval = min(interval * config.multiplier, config.max_interval)
|
|
93
|
+
|
|
94
|
+
# Should not reach here, but just in case
|
|
95
|
+
if fail_fast and last_exception:
|
|
96
|
+
raise last_exception
|
|
97
|
+
return None
|