Skip to content

Commit 6288b00

Browse files
committed
feat(server): server-side response cache for proxied actuator endpoints
Signed-off-by: Matthieu MOREL <matthieu.morel35@gmail.com>
1 parent 7d7f312 commit 6288b00

17 files changed

Lines changed: 1588 additions & 76 deletions

spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerHazelcastAutoConfiguration.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2929
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3030
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
31+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3132
import org.springframework.boot.hazelcast.autoconfigure.HazelcastAutoConfiguration;
3233
import org.springframework.context.annotation.Bean;
3334
import org.springframework.context.annotation.Configuration;
@@ -40,22 +41,31 @@
4041
import de.codecentric.boot.admin.server.notify.HazelcastNotificationTrigger;
4142
import de.codecentric.boot.admin.server.notify.NotificationTrigger;
4243
import de.codecentric.boot.admin.server.notify.Notifier;
44+
import de.codecentric.boot.admin.server.web.cache.ActuatorResponseCache;
45+
import de.codecentric.boot.admin.server.web.cache.CacheEntry;
46+
import de.codecentric.boot.admin.server.web.cache.HazelcastActuatorResponseCache;
4347

4448
@Configuration(proxyBeanMethods = false)
4549
@ConditionalOnBean(AdminServerMarkerConfiguration.Marker.class)
4650
@ConditionalOnSingleCandidate(HazelcastInstance.class)
4751
@ConditionalOnProperty(prefix = "spring.boot.admin.hazelcast", name = "enabled", matchIfMissing = true)
4852
@AutoConfigureBefore({ AdminServerAutoConfiguration.class, AdminServerNotifierAutoConfiguration.class })
4953
@AutoConfigureAfter(HazelcastAutoConfiguration.class)
54+
@EnableConfigurationProperties(AdminServerProperties.class)
5055
@Lazy(false)
5156
public class AdminServerHazelcastAutoConfiguration {
5257

5358
public static final String DEFAULT_NAME_EVENT_STORE_MAP = "spring-boot-admin-event-store";
5459

5560
public static final String DEFAULT_NAME_SENT_NOTIFICATIONS_MAP = "spring-boot-admin-sent-notifications";
5661

62+
public static final String DEFAULT_NAME_RESPONSE_CACHE_MAP = "spring-boot-admin-actuator-response-cache";
63+
5764
@Value("${spring.boot.admin.hazelcast.event-store:" + DEFAULT_NAME_EVENT_STORE_MAP + "}")
58-
private final String nameEventStoreMap = DEFAULT_NAME_EVENT_STORE_MAP;
65+
private String nameEventStoreMap = DEFAULT_NAME_EVENT_STORE_MAP;
66+
67+
@Value("${spring.boot.admin.hazelcast.response-cache:" + DEFAULT_NAME_RESPONSE_CACHE_MAP + "}")
68+
private String nameResponseCacheMap = DEFAULT_NAME_RESPONSE_CACHE_MAP;
5969

6070
@Bean
6171
@ConditionalOnMissingBean(InstanceEventStore.class)
@@ -64,12 +74,21 @@ public HazelcastEventStore eventStore(HazelcastInstance hazelcastInstance) {
6474
return new HazelcastEventStore(map);
6575
}
6676

77+
@Bean
78+
@ConditionalOnMissingBean(ActuatorResponseCache.class)
79+
@ConditionalOnProperty(prefix = "spring.boot.admin.endpoint-cache", name = "enabled", matchIfMissing = true)
80+
public HazelcastActuatorResponseCache actuatorResponseCache(HazelcastInstance hazelcastInstance,
81+
AdminServerProperties properties) {
82+
IMap<String, CacheEntry> map = hazelcastInstance.getMap(this.nameResponseCacheMap);
83+
return new HazelcastActuatorResponseCache(map, properties.getEndpointCache());
84+
}
85+
6786
@Configuration(proxyBeanMethods = false)
6887
@ConditionalOnBean(Notifier.class)
6988
public static class NotifierTriggerConfiguration {
7089

7190
@Value("${spring.boot.admin.hazelcast.sent-notifications:" + DEFAULT_NAME_SENT_NOTIFICATIONS_MAP + "}")
72-
private final String nameSentNotificationsMap = DEFAULT_NAME_SENT_NOTIFICATIONS_MAP;
91+
private String nameSentNotificationsMap = DEFAULT_NAME_SENT_NOTIFICATIONS_MAP;
7392

7493
@Bean(initMethod = "start", destroyMethod = "stop")
7594
@ConditionalOnMissingBean(NotificationTrigger.class)

spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerProperties.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public class AdminServerProperties {
4949

5050
private InstanceProxyProperties instanceProxy = new InstanceProxyProperties();
5151

52+
private EndpointCacheProperties endpointCache = new EndpointCacheProperties();
53+
5254
/**
5355
* The metadata keys which should be sanitized when serializing to json
5456
*/
@@ -199,4 +201,40 @@ public static class InstanceProxyProperties {
199201

200202
}
201203

204+
@lombok.Data
205+
public static class EndpointCacheProperties {
206+
207+
/**
208+
* Whether server-side caching of proxied actuator GET responses is enabled.
209+
*/
210+
private boolean enabled = true;
211+
212+
/**
213+
* Default TTL for cached responses.
214+
*/
215+
@DurationUnit(ChronoUnit.MILLIS)
216+
private Duration defaultTtl = Duration.ofMinutes(5);
217+
218+
/**
219+
* TTL per endpoint id. Overrides default-ttl for a specific endpoint. Example:
220+
* {@code spring.boot.admin.endpoint-cache.ttl.mappings=10m}
221+
*/
222+
@DurationUnit(ChronoUnit.MILLIS)
223+
private Map<String, Duration> ttl = new HashMap<>();
224+
225+
/**
226+
* Endpoint ids whose responses should be cached. Only safe GET requests to these
227+
* endpoints are cached.
228+
*/
229+
private Set<String> endpoints = new HashSet<>(
230+
asList("mappings", "configprops", "beans", "conditions", "sbom", "startup"));
231+
232+
/**
233+
* Maximum response body size in bytes that will be cached. Responses larger than
234+
* this threshold are forwarded as-is without caching.
235+
*/
236+
private long maxPayloadSize = 10L * 1024 * 1024;
237+
238+
}
239+
202240
}

spring-boot-admin-server/src/main/java/de/codecentric/boot/admin/server/config/AdminServerWebConfiguration.java

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2024 the original author or authors.
2+
* Copyright 2014-2026 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,8 +16,12 @@
1616

1717
package de.codecentric.boot.admin.server.config;
1818

19+
import org.reactivestreams.Publisher;
20+
import org.springframework.beans.factory.ObjectProvider;
1921
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
22+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
2023
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
24+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
2125
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
2226
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
2327
import org.springframework.context.ApplicationEventPublisher;
@@ -27,12 +31,18 @@
2731
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
2832
import tools.jackson.databind.module.SimpleModule;
2933

34+
import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
3035
import de.codecentric.boot.admin.server.eventstore.InstanceEventStore;
3136
import de.codecentric.boot.admin.server.services.ApplicationRegistry;
3237
import de.codecentric.boot.admin.server.services.InstanceRegistry;
3338
import de.codecentric.boot.admin.server.utils.jackson.AdminServerModule;
3439
import de.codecentric.boot.admin.server.web.ApplicationsController;
40+
import de.codecentric.boot.admin.server.web.HttpHeaderFilter;
41+
import de.codecentric.boot.admin.server.web.InstanceWebProxy;
3542
import de.codecentric.boot.admin.server.web.InstancesController;
43+
import de.codecentric.boot.admin.server.web.cache.ActuatorResponseCache;
44+
import de.codecentric.boot.admin.server.web.cache.CacheInvalidationTrigger;
45+
import de.codecentric.boot.admin.server.web.cache.InMemoryActuatorResponseCache;
3646
import de.codecentric.boot.admin.server.web.client.InstanceWebClient;
3747

3848
@Configuration(proxyBeanMethods = false)
@@ -62,6 +72,21 @@ public ApplicationsController applicationsController(ApplicationRegistry applica
6272
return new ApplicationsController(applicationRegistry, applicationEventPublisher);
6373
}
6474

75+
@Bean
76+
@ConditionalOnMissingBean(ActuatorResponseCache.class)
77+
@ConditionalOnProperty(prefix = "spring.boot.admin.endpoint-cache", name = "enabled", matchIfMissing = true)
78+
public InMemoryActuatorResponseCache actuatorResponseCache() {
79+
return new InMemoryActuatorResponseCache(this.adminServerProperties.getEndpointCache());
80+
}
81+
82+
@Bean(initMethod = "start", destroyMethod = "stop")
83+
@ConditionalOnBean(ActuatorResponseCache.class)
84+
@ConditionalOnMissingBean(CacheInvalidationTrigger.class)
85+
public CacheInvalidationTrigger cacheInvalidationTrigger(ActuatorResponseCache responseCache,
86+
Publisher<InstanceEvent> events) {
87+
return new CacheInvalidationTrigger(events, responseCache);
88+
}
89+
6590
@Configuration(proxyBeanMethods = false)
6691
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
6792
public static class ReactiveRestApiConfiguration {
@@ -75,11 +100,14 @@ public ReactiveRestApiConfiguration(AdminServerProperties adminServerProperties)
75100
@Bean
76101
@ConditionalOnMissingBean
77102
public de.codecentric.boot.admin.server.web.reactive.InstancesProxyController instancesProxyController(
78-
InstanceRegistry instanceRegistry, InstanceWebClient.Builder instanceWebClientBuilder) {
103+
InstanceRegistry instanceRegistry, InstanceWebClient.Builder instanceWebClientBuilder,
104+
ObjectProvider<ActuatorResponseCache> responseCache) {
105+
HttpHeaderFilter headerFilter = new HttpHeaderFilter(
106+
this.adminServerProperties.getInstanceProxy().getIgnoredHeaders());
107+
InstanceWebProxy instanceWebProxy = new InstanceWebProxy(instanceWebClientBuilder.build(),
108+
responseCache.getIfAvailable(), headerFilter);
79109
return new de.codecentric.boot.admin.server.web.reactive.InstancesProxyController(
80-
this.adminServerProperties.getContextPath(),
81-
this.adminServerProperties.getInstanceProxy().getIgnoredHeaders(), instanceRegistry,
82-
instanceWebClientBuilder.build());
110+
this.adminServerProperties.getContextPath(), headerFilter, instanceRegistry, instanceWebProxy);
83111
}
84112

85113
@Bean
@@ -108,11 +136,14 @@ public ServletRestApiConfiguration(AdminServerProperties adminServerProperties)
108136
@Bean
109137
@ConditionalOnMissingBean
110138
public de.codecentric.boot.admin.server.web.servlet.InstancesProxyController instancesProxyController(
111-
InstanceRegistry instanceRegistry, InstanceWebClient.Builder instanceWebClientBuilder) {
139+
InstanceRegistry instanceRegistry, InstanceWebClient.Builder instanceWebClientBuilder,
140+
ObjectProvider<ActuatorResponseCache> responseCache) {
141+
HttpHeaderFilter headerFilter = new HttpHeaderFilter(
142+
this.adminServerProperties.getInstanceProxy().getIgnoredHeaders());
143+
InstanceWebProxy instanceWebProxy = new InstanceWebProxy(instanceWebClientBuilder.build(),
144+
responseCache.getIfAvailable(), headerFilter);
112145
return new de.codecentric.boot.admin.server.web.servlet.InstancesProxyController(
113-
this.adminServerProperties.getContextPath(),
114-
this.adminServerProperties.getInstanceProxy().getIgnoredHeaders(), instanceRegistry,
115-
instanceWebClientBuilder.build());
146+
this.adminServerProperties.getContextPath(), headerFilter, instanceRegistry, instanceWebProxy);
116147
}
117148

118149
@Bean

0 commit comments

Comments
 (0)