Closed mhalbritter closed 6 months ago
We have some endpoints where we wanted to be able to include app and/or actuator links in the response. The low friction way we found was to move to @RestControllerEndpoint
and get the HttpServletRequest
and use getRequestURL()
to build them. It was a while ago so I don't know if we missed some way to do that with a @WebEndpoint
then or added since then...
Or if the idea of those links is an anathema to the endpoint abstraction entirely... they are strange endpoints I won't defend in general so no objections to deprecation, just a data point...
We have quite a few @RestControllerEndpoint
annotations used here.
Main use cases that I think would be useful and helpful to support going forward would be:
@GetMapping
, @PostMapping
, @ResponseBody
, etc.I think most of these could likely be converted and written to conform to the new way of doing this.
HttpServletRequest#getInputStream()
It might be doable to do this today, but I wasn't able to find an alternative.
Thank you, @mmoayyed.
Support the convenience of
@GetMapping
,@PostMapping
,@ResponseBody
In Actuator endpoint terms, @GetMapping
is a @ReadOperation
and @PostMapping
is a @WriteOperation
. There's also @DeleteOperation
which is the equivalent of @DeleteMapping
.
Can you expand a bit on the need for @ResponseBody
? The value returned from an endpoint operation is already written to the body of the response.
Ability to read the servlet request body in raw form … It might be doable to do this today
We don't support this at the moment. Any body is read up front as a source of arguments for the endpoint operation's method parameters. There's an implicit assumption here that the body can be converted into key-value pairs.
Thank you Andy.
The main example that I found for @ResponseBody
is dealing with exporting data out in form of a zip file. The endpoint creates a zip file Resource
, and returns it in the form of new ResponseEntity<>(resource, headers, HttpStatus.OK)
where the header contains content-disposition entries, etc. The endpoint itself is tagged as
@GetMapping(path = "/export", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
@ResponseBody
Likewise, the reverse of this operation is handled as an import
endpoint in the same component that reads the request.getInputStream()
, and stores the processed data somewhere, returning some http status endpoint. The body could contain on object or a collection of objects (as would be the classical import use case)
You can return a Resource
from a @ReadOperation
as we do in Boot's LogFileWebEndpoint
:
I guess a missing piece is the ability to set a Content-Disposition
header which WebEndpointResponse
does not allow you to do at the moment.
Likewise, the reverse of this operation is handled as an import endpoint in the same component that reads the
request.getInputStream()
, and stores the processed data somewhere, returning some http status endpoint
On first impressions, this doesn't sound like something that you'd do as part of operating an app that's running in production so it isn't necessarily something that we'd expect to support in Actuator. Can you expand a bit on the use case please?
Sure. I'd have to go back a few steps, so please bear with me.
This project ships a number auto-starters that control the implementation of a particular repository type. The ultimate build script (put together by an adopter in form of a overlay), allows an adopter to include the appropriate starter in their build and let that repository manage their data. I suppose there are scenarios where one would want to take data from one repository in one version, and move it over to another repository in the next version. Let's say JSON to Cassandra, Mongo to DynamoDb, etc. So the import/export operations provide a facility to do that as a point of convenience, behind an actuator endpoint that of course needs to be enabled, protected, used likely once or twice in dev, and then turned off in production as you note. Certainly, it's not something to actively use in production.
I suppose one could always use native tooling, if and when available, to handle the data migration task, or fall back onto dedicated libraries and frameworks that take care of such things. While all should be better options in theory, (and surely there is nothing stopping the advanced user to play around with such things) they add additional points of complexity and learning curve that in this particular context, a very select group of users would want to deal with. The overwhelming preference generally is to deal with an endpoint that handles such things (IIRC, there is a command-line utility available that does this as well in form a spring shell command, but I digress).
We currently use the management endpoints as a means of providing an internal API that is served on a different port. These annotations allow us to actually provide dedicated endpoints on the management port as well as handling security on these differently.
This way we can provide an Kubernetes ingress for the public API on one port while providing management actions an internal API without ingress on another.
Thanks, @frederic-kneier. Would it be fair to say that you're using an Actuator endpoint as a convenient way of exposing the API on a different port rather than because it's used as part of operating the application in production?
Not entirely, but regarding @ControllerEndpoint
and @RestControllerEndpoint
I think that is true. It would be ok to use simple @Controller
and @RestController
annotations if we could specify the actual endpoint/port to be used and if security could be configured based on the endpoint/port. There are still management endpoints that do not match this description, but I think they could be implemented using the normal endpoints / operations.
We have a few in spring cloud. I'll get details in a bit
One other thing that might be useful here: @RestControllerEndpoint
allows for overloading methods that address different requirements. For example, one could have multiple @GetMapping
methods that handle different concerns, with different parameters or path variables. This seems more natural vs one @ReadOperation
with 2-3 parameters some marked as selectors and some marked as not. Perhaps the replacement could provide the same programming model.
We have discussed this again earlier today and decided that we should deprecate this for 3.3.0-RC1.
We think that since this was introduced, the @Endpoint
/ @*Operation
model has evolved quite a bit and that we declined low-level web support feature requests in this area. Right now it seems that the abstraction leaks here and that it prevents us from evolving the implementation.
We don't plan on removing this feature soon - we will honor at least our 2 minor releases policy. In the meantime, we will also gather feedback from the community.
We had a look before scheduling this, and it seems this endpoint would work well with @ReadOperation
and @WriteOperation
.
I've run into this issue after upgrading to Spring Boot 3.3.0 and I'm not sure how to proceed. I've been using webflux in Spring Boot 3.2.x and I've implemented a custom actuator endpoint like this:
@Component
@RestControllerEndpoint(id = "api", enableByDefault = true)
class ApiProxyEndpoint {
@Value("\${management.endpoints.web.base-path}")
lateinit var managementPath: String
@Autowired
lateinit var webHandler: WebHandler
@RequestMapping("**") // Match all path segments under <management-path>/api/ until the end of the path
fun apiProxyEndpoint(exchange: ServerWebExchange): Mono<Void> {
val originalPath = exchange.request.path.toString()
val apiPathWithoutManagementPath = originalPath.substringAfter(managementPath)
val updatedRequest = exchange.request.mutate().contextPath("/").path(apiPathWithoutManagementPath).header("X-Api-RootPath" managementPath).build()
val updatedExchange = exchange.mutate().request(updatedRequest).build()
return webHandler.handle(updatedExchange)
}
}
The reason for doing this is that I want to expose the hal-browser as an actuator endpoint (so that I can browse the API from using the hal-browser from the actuator). What's happening is that when the hal-browser calls a URI (by calling the "api" endpoint in actuator) I redirect all requests to the actual api endpoint (not behind actuator). This endpoint acts as a proxy, just forwarding all requests made to "api" to somewhere else. This has worked great for several years, but since Spring Boot 3.3.0, the RestControllerEndpoint
has been deprecated and I can't find a way to replace it. I've tried this:
@Component
@WebEndpoint(id = "api", enableByDefault = true)
class ApiProxyEndpoint {
@Value("\${management.endpoints.web.base-path}")
lateinit var managementPath: String
@Autowired
lateinit var webHandler: WebHandler
@WriteOperation
@ReadOperation
@DeleteOperation
fun apiProxyEndpoint(exchange: ServerWebExchange): Mono<Void> {
val originalPath = exchange.request.path.toString()
val apiPathWithoutManagementPath = originalPath.substringAfter(managementPath)
val updatedRequest = exchange.request.mutate().contextPath("/").path(apiPathWithoutManagementPath).header("X-Api-RootPath" managementPath).build()
val updatedExchange = exchange.mutate().request(updatedRequest).build()
return webHandler.handle(updatedExchange)
}
}
but this doesn't work. The example above will show me this error in the HAL browser:
{
"timestamp": "2024-06-12T10:23:17.366+00:00",
"status": 400,
"error": "Bad Request",
"requestId": "efc7c7b9-4",
"message": "Missing parameters: exchange"
}
How can I achieve the same thing (i.e. proxying) in Spring Boot 3.3.0 without using the deprecated RestControllerEndpoint
?
It looks to me like your ApiProxyEndpoint
should be a @RestController
and use management.endpoints.web.base-path
to map it to /actuator/api
. If you're running actuator on the same port as the rest of the application, a standard @RestController
should be all that you need. If you're running actuator on a separate port, you'll need to use a @ManagementContextConfiguration
class registered in META-INF/spring.factories
to define the controller as a bean.
@wilkinsona I'm using a different port so I guess I should try the @ManagementContextConfiguration
path. I'll let you know how it goes. Thank you for your suggestion.
Unfortunately I only found this ticket after the 3.3 release. Our reason for using @RestControllerEndpoint
- and on a casual read this seems to have not come up in the discussion - is because we have several custom endpoints that require a JSON object with a couple of nested properties as input. These are actuator endpoints and not regular RestControllers as they serve operational purposes, are accessed on a separate management port and require our operational Basic auth whereas the application uses OAuth2 with a completely separate user base - configured conveniently with the EndpointRequest matchers. The approach to register a regular controller in the management context seems complicated and low-level in contrast, and make configuration and security configuration more complicated and less declarative. Removing this is a functional regression for us. Supporting POJO (and/or records) as input for @WebEndpoint
would mitigate this for our use case.
@rainerfrey-inxmail Are you able to share a sample application that shows one of your more complicated inputs? It doesn't need to be your production code, just something that gives us an idea of how you're currently using @RestControllerEndpoint
. We can then consider how we might be able to evolve @WebEndpoint
.
@johanhaleby We are currently having the same issue did you get it to work using the @ManagementContextConfiguration
method?
@IfvJonas I haven't tried yet unfortunately.
@philwebb I second @rainerfrey-inxmail use case. We're synchronizing data between our environments and use actuator endpoints for that.
Right now we have an endpoint like
@PostMapping
fun synchronize(@RequestBody model: SynchronizationRequestModel): Mono<ResponseEntity<Void>> =
…
which basically accepts a list of entities that should be synced. It seems that this is no longer possible with @WebEndpoint
and @WriteOperation
.
Unfortunately I only found this ticket after the 3.3 release. Our reason for using
@RestControllerEndpoint
- and on a casual read this seems to have not come up in the discussion - is because we have several custom endpoints that require a JSON object with a couple of nested properties as input. These are actuator endpoints and not regular RestControllers as they serve operational purposes, are accessed on a separate management port and require our operational Basic auth whereas the application uses OAuth2 with a completely separate user base - configured conveniently with the EndpointRequest matchers. The approach to register a regular controller in the management context seems complicated and low-level in contrast, and make configuration and security configuration more complicated and less declarative. Removing this is a functional regression for us. Supporting POJO (and/or records) as input for@WebEndpoint
would mitigate this for our use case.
We have the same issue.
Replacing @WebEndpoint
with @RestController
as suggested by @wilkinsona does not work as it also stops the "fake actuator" endpoint from being discovered via the /actuator
endpoint which we also use for auto-discover of our management interfaces (similar tool as Spring Admin).
@rainerfrey-inxmail Are you able to share a sample application that shows one of your more complicated inputs? It doesn't need to be your production code, just something that gives us an idea of how you're currently using @RestControllerEndpoint. We can then consider how we might be able to evolve @WebEndpoint.
I can also help providing a minimal app for that but there's nothing fancy there, just:
@RestControllerEndpoint (id = "customer")
@Component
public class CustomerEndpoint {
@PostMapping ("/{id}")
public Customer createCustomer(final @PathVariable (value = "id") Integer id, final @Valid @RequestBody Customer customer)
{
return customer;
}
public static record Customer(String name, Address address){}
public static record Address(String street){}
}
Now you can POST to /actuator/customer/1 with
{ "name": "cust", "address": {"street": "1"}}
And placing a get to /actuator, it returns:
"customer": {
"href": "http://localhost:8081/actuator/customer",
"templated": false
}
There's a limitation where the templated
will always be false when using RestControllerEndpoint so auto discovery does not not work 100% semantically correct for those endpoints.
@philwebb Any news on this. This is a blocker on our side as well :-(
@rainerfrey-inxmail Are you able to share a sample application that shows one of your more complicated inputs?
Sorry I was otherwise engaged for a couple of weeks. Do you need a runnable application or is code of endpoint and request body sufficient?
@kschlesselmann We've not had a chance to discuss it again yet, but the issue is flagged so we won't forget.
@rainerfrey-inxmail Ideally something that we can run so we can investigate alternative API ideas.
@philwebb Maybe this issue should be re-opened to show that there is an active discussion (an a pending one as well) here?
Thanks for the suggestion. This issue was tracking the deprecation which has been done and shipped in 3.3.0-RC1. It doesn't make sense to have an open issue for work that has been completed so it won't be re-opened. Please rest assured that the feedback that people have provided is still very much on our radar and it will all be considered before anything is actually removed.
Thanks @wilkinsona Using RestControllerEndpoint solved the problem of creating a custom endpoint with context type as event-stream., Since we are deprecating these annotationsit from 3.3.0 and removing these annotations (@RestControllerEndpoint
and @ControllerEndpoint
), how are we going to create a custom actuator endpoint with context type as event-stream, since it is not working for both @Endpoint
and @WebEndpoint
?
how are we going to create a custom actuator endpoint with context type as event-stream, since it is not working for both
@Endpoint
and@WebEndpoint
?
We may consider it as a future enhancement to the @WebEndpoint
support, but we'd need a compelling use case for event stream responses from Actuator. What's your need for it?
We were actually in the midst of constructing a custom actuator endpoint since we have many circuit breakers and we wanted to send our circuit breaker events to the developer management portal managing bby us. Although we can accomplish the same thing with a controller or rest controller, this isn't what I presume it's meant for. Given that this is management configuration data, it made sense to expose it on the actuator endpoint.
We had the same use case as @talasila-sairam before when using Hystrix and Turbine.
Hi we use Rest Actuator for some tech operation at work. We do not use jmx exposure, so we use all the flexibility a REST controller could provide such as this kind of actuator:
@Component
@RestControllerEndpoint(id = "kafka")
class KafkaActuator {
@GetMapping("/consumers")
List<ConsumerGroupWithStatus> consumers() {
//...
}
@PostMapping("/suspend/{consumerGroup}")
void suspend(@PathVariable String consumerGroup) {
LOGGER.info("ask suspend for consumer group {}", consumerGroup);
MessageListenerContainer consumer = getConsumer(consumerGroup);
consumer.pause();
LOGGER.info("consumer group {} suspended", consumerGroup);
}
@PostMapping("/resume/{consumerGroup}")
void resume(@PathVariable String consumerGroup) {
LOGGER.info("ask resume for consumer group {}", consumerGroup);
MessageListenerContainer consumer = getConsumer(consumerGroup);
consumer.resume();
LOGGER.info("consumer group {} resumed", consumerGroup);
}
@PostMapping("/dump")
ResponseEntity<DumpCommandResult> dump(@RequestBody DumpOrder dumpOrder) {
...
}
@DeleteMapping("/dump")
ResponseEntity<DumpCommandResult> stopDump() {
...
}
Now, RestControllerEndpoint is deprecated, but I didn't find any equivalent migration path in spring documentation. Of course, we can convert this actuator to a RestController, but we'll lose actuator built-in configuration, such as exposure properties: management.endpoints.web.base-path=/management, management.endpoints.web.exposure.include=*...
We should decide if we want to deprecate and later remove
@ControllerEndpoint
and@RestControllerEndpoint
from the actuator. Using them ties the user to WebMVC or WebFlux and they were meant to ease the upgrade path to the weblayer-abstracting@Endpoint
with@ReadOperation
, etc.Getting rid of those would pave the way for https://github.com/spring-projects/spring-boot/issues/20290.
If you're seeing this ticket and object to this idea, please comment, your feedback is very valuable. Please also explain your use case and why this use case can't be solved with
@Endpoint
or@WebEndpoint
.