2929
3030def make_starlette_actuator_routes (
3131 registry : ActuatorRegistry ,
32+ exposed_ids : set [str ] | None = None ,
33+ base_path : str = "/actuator" ,
3234) -> list [Route ]:
33- """Build Starlette ``Route`` objects from all enabled endpoints in *registry*."""
35+ """Build Starlette ``Route`` objects from all enabled endpoints in *registry*.
36+
37+ *exposed_ids* — if provided, only endpoints whose id is in the set are mounted
38+ over HTTP (Spring Boot ``management.endpoints.web.exposure``). ``None`` exposes
39+ every enabled endpoint (used by low-level callers/tests).
40+ *base_path* — the actuator base path (Spring ``management.endpoints.web.base-path``).
41+ """
3442 enabled = registry .get_enabled_endpoints ()
43+ if exposed_ids is not None :
44+ enabled = {eid : ep for eid , ep in enabled .items () if eid in exposed_ids }
45+
46+ bp = base_path .rstrip ("/" )
3547 routes : list [Route ] = []
3648
37- # Index endpoint: /actuator — lists all enabled endpoints with _links
49+ # Index endpoint: /actuator — lists all exposed endpoints with _links
3850 async def index_endpoint (request : Request ) -> JSONResponse :
39- links : dict [str , dict [str , str ]] = {"self" : {"href" : "/actuator " }}
51+ links : dict [str , dict [str , str ]] = {"self" : {"href" : bp or "/ " }}
4052 for eid in enabled :
41- links [eid ] = {"href" : f"/actuator /{ eid } " }
53+ links [eid ] = {"href" : f"{ bp } /{ eid } " }
4254 if "health" in enabled :
43- links ["health/liveness" ] = {"href" : "/actuator /health/liveness" }
44- links ["health/readiness" ] = {"href" : "/actuator /health/readiness" }
55+ links ["health/liveness" ] = {"href" : f" { bp } /health/liveness" }
56+ links ["health/readiness" ] = {"href" : f" { bp } /health/readiness" }
4557 return JSONResponse ({"_links" : links })
4658
47- routes .append (Route ("/actuator " , index_endpoint , methods = ["GET" ]))
59+ routes .append (Route (bp or "/ " , index_endpoint , methods = ["GET" ]))
4860
4961 for eid , ep in enabled .items ():
5062 if isinstance (ep , HealthEndpoint ):
51- routes .extend (_make_health_routes (ep ))
63+ routes .extend (_make_health_routes (ep , bp ))
5264 elif isinstance (ep , LoggersEndpoint ):
53- routes .extend (_make_loggers_routes (ep ))
65+ routes .extend (_make_loggers_routes (ep , bp ))
5466 elif isinstance (ep , PrometheusEndpoint ):
55- routes .append (_make_prometheus_route (ep ))
67+ routes .append (_make_prometheus_route (ep , bp ))
5668 else :
57- routes .append ( _make_generic_route (eid , ep ))
69+ routes .extend ( _make_generic_routes (eid , ep , bp ))
5870
5971 return routes
6072
6173
62- def _make_prometheus_route (ep : PrometheusEndpoint ) -> Route :
74+ def _make_prometheus_route (ep : PrometheusEndpoint , bp : str ) -> Route :
6375 """Prometheus scrape endpoint — must serve the raw text exposition format
6476 (``text/plain; version=0.0.4``), not a JSON wrapper."""
6577
@@ -70,11 +82,15 @@ async def handler(request: Request) -> Response:
7082 media_type = data .get ("content_type" ) or "text/plain; version=0.0.4; charset=utf-8" ,
7183 )
7284
73- return Route ("/actuator/prometheus" , handler , methods = ["GET" ])
85+ return Route (f"{ bp } /prometheus" , handler , methods = ["GET" ])
86+
7487
88+ def _make_health_routes (ep : HealthEndpoint , bp : str ) -> list [Route ]:
89+ """Health endpoint returns dynamic status codes (200/503).
7590
76- def _make_health_routes (ep : HealthEndpoint ) -> list [Route ]:
77- """Health endpoint returns dynamic status codes (200/503)."""
91+ Supports the built-in ``liveness``/``readiness`` probe groups plus a generic
92+ ``/health/{path}`` selector for any other group or single component.
93+ """
7894
7995 async def handler (request : Request ) -> JSONResponse :
8096 data = await ep .handle ()
@@ -91,41 +107,77 @@ async def readiness_handler(request: Request) -> JSONResponse:
91107 status_code = await ep .get_readiness_status_code ()
92108 return JSONResponse (data , status_code = status_code )
93109
110+ async def selector_handler (request : Request ) -> JSONResponse :
111+ # /actuator/health/{path} — a configured group, or a single component.
112+ path = request .path_params ["path" ]
113+ data , status_code = await ep .handle_path (path )
114+ if data is None :
115+ return JSONResponse ({"error" : f"No such health component or group: { path } " }, status_code = 404 )
116+ return JSONResponse (data , status_code = status_code )
117+
94118 return [
95- Route ("/actuator/health" , handler , methods = ["GET" ]),
96- Route ("/actuator/health/liveness" , liveness_handler , methods = ["GET" ]),
97- Route ("/actuator/health/readiness" , readiness_handler , methods = ["GET" ]),
119+ Route (f"{ bp } /health" , handler , methods = ["GET" ]),
120+ Route (f"{ bp } /health/liveness" , liveness_handler , methods = ["GET" ]),
121+ Route (f"{ bp } /health/readiness" , readiness_handler , methods = ["GET" ]),
122+ Route (f"{ bp } /health/{{path:path}}" , selector_handler , methods = ["GET" ]),
98123 ]
99124
100125
101- def _make_loggers_routes (ep : LoggersEndpoint ) -> list [Route ]:
102- """Loggers endpoint supports GET (list) and POST (change level)."""
126+ def _make_loggers_routes (ep : LoggersEndpoint , bp : str ) -> list [Route ]:
127+ """Loggers endpoint — Spring Boot shape.
128+
129+ GET /loggers -> levels + loggers + groups
130+ GET /loggers/{name} -> {configuredLevel, effectiveLevel}
131+ POST /loggers/{name} -> body {"configuredLevel": "DEBUG"} (or null to reset), 204
132+ """
103133
104134 async def get_handler (request : Request ) -> JSONResponse :
105135 data = await ep .handle ()
106136 return JSONResponse (data )
107137
108- async def post_handler (request : Request ) -> JSONResponse :
138+ async def get_named_handler (request : Request ) -> JSONResponse :
139+ name = request .path_params ["name" ]
140+ return JSONResponse (await ep .get_logger (name ))
141+
142+ async def post_named_handler (request : Request ) -> Response :
143+ name = request .path_params ["name" ]
109144 body = await request .body ()
110145 payload = json .loads (body ) if body else {}
111- logger_name = payload .get ("logger" , "ROOT" )
112- level = payload .get ("level" , "INFO" )
113- result = await ep .set_logger_level (logger_name , level )
114- if "error" in result :
146+ level = payload .get ("configuredLevel" )
147+ result = await ep .set_logger_level (name , level )
148+ if isinstance (result , dict ) and "error" in result :
115149 return JSONResponse (result , status_code = 400 )
116- return JSONResponse ( result )
150+ return Response ( status_code = 204 )
117151
118152 return [
119- Route ("/actuator/loggers" , get_handler , methods = ["GET" ]),
120- Route ("/actuator/loggers" , post_handler , methods = ["POST" ]),
153+ Route (f"{ bp } /loggers" , get_handler , methods = ["GET" ]),
154+ Route (f"{ bp } /loggers/{{name}}" , get_named_handler , methods = ["GET" ]),
155+ Route (f"{ bp } /loggers/{{name}}" , post_named_handler , methods = ["POST" ]),
121156 ]
122157
123158
124- def _make_generic_route (eid : str , ep : object ) -> Route :
125- """Generic endpoint — calls ``handle()`` and returns 200 JSON."""
159+ def _make_generic_routes (eid : str , ep : object , bp : str ) -> list [Route ]:
160+ """Generic endpoint — ``GET /actuator/{id}`` plus an optional
161+ ``GET /actuator/{id}/{selector}`` drill-down when the endpoint opts in via
162+ ``supports_selector = True``."""
126163
127164 async def handler (request : Request ) -> JSONResponse :
128- data = await ep .handle () # type: ignore[attr-defined]
165+ data = await ep .handle ({ "query" : dict ( request . query_params )} ) # type: ignore[attr-defined]
129166 return JSONResponse (data )
130167
131- return Route (f"/actuator/{ eid } " , handler , methods = ["GET" ])
168+ routes = [Route (f"{ bp } /{ eid } " , handler , methods = ["GET" ])]
169+
170+ if getattr (ep , "supports_selector" , False ):
171+
172+ async def selector_handler (request : Request ) -> JSONResponse :
173+ selector = request .path_params ["selector" ]
174+ data = await ep .handle ( # type: ignore[attr-defined]
175+ {"selector" : selector , "query" : dict (request .query_params )}
176+ )
177+ if data is None :
178+ return JSONResponse ({"error" : f"No such { eid } : { selector } " }, status_code = 404 )
179+ return JSONResponse (data )
180+
181+ routes .append (Route (f"{ bp } /{ eid } /{{selector:path}}" , selector_handler , methods = ["GET" ]))
182+
183+ return routes
0 commit comments