Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.hazelcast.autoconfigure.HazelcastAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -40,22 +41,31 @@
import de.codecentric.boot.admin.server.notify.HazelcastNotificationTrigger;
import de.codecentric.boot.admin.server.notify.NotificationTrigger;
import de.codecentric.boot.admin.server.notify.Notifier;
import de.codecentric.boot.admin.server.web.cache.ActuatorResponseCache;
import de.codecentric.boot.admin.server.web.cache.CacheEntry;
import de.codecentric.boot.admin.server.web.cache.HazelcastActuatorResponseCache;

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(AdminServerMarkerConfiguration.Marker.class)
@ConditionalOnSingleCandidate(HazelcastInstance.class)
@ConditionalOnProperty(prefix = "spring.boot.admin.hazelcast", name = "enabled", matchIfMissing = true)
@AutoConfigureBefore({ AdminServerAutoConfiguration.class, AdminServerNotifierAutoConfiguration.class })
@AutoConfigureAfter(HazelcastAutoConfiguration.class)
@EnableConfigurationProperties(AdminServerProperties.class)
@Lazy(false)
public class AdminServerHazelcastAutoConfiguration {

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

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

public static final String DEFAULT_NAME_RESPONSE_CACHE_MAP = "spring-boot-admin-actuator-response-cache";

@Value("${spring.boot.admin.hazelcast.event-store:" + DEFAULT_NAME_EVENT_STORE_MAP + "}")
private final String nameEventStoreMap = DEFAULT_NAME_EVENT_STORE_MAP;
private String nameEventStoreMap = DEFAULT_NAME_EVENT_STORE_MAP;

@Value("${spring.boot.admin.hazelcast.response-cache:" + DEFAULT_NAME_RESPONSE_CACHE_MAP + "}")
private String nameResponseCacheMap = DEFAULT_NAME_RESPONSE_CACHE_MAP;

@Bean
@ConditionalOnMissingBean(InstanceEventStore.class)
Expand All @@ -64,12 +74,21 @@ public HazelcastEventStore eventStore(HazelcastInstance hazelcastInstance) {
return new HazelcastEventStore(map);
}

@Bean
@ConditionalOnMissingBean(ActuatorResponseCache.class)
@ConditionalOnProperty(prefix = "spring.boot.admin.endpoint-cache", name = "enabled", matchIfMissing = true)
public HazelcastActuatorResponseCache actuatorResponseCache(HazelcastInstance hazelcastInstance,
AdminServerProperties properties) {
IMap<String, CacheEntry> map = hazelcastInstance.getMap(this.nameResponseCacheMap);
return new HazelcastActuatorResponseCache(map, properties.getEndpointCache());
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(Notifier.class)
public static class NotifierTriggerConfiguration {

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

@Bean(initMethod = "start", destroyMethod = "stop")
@ConditionalOnMissingBean(NotificationTrigger.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public class AdminServerProperties {

private InstanceProxyProperties instanceProxy = new InstanceProxyProperties();

private EndpointCacheProperties endpointCache = new EndpointCacheProperties();

/**
* The metadata keys which should be sanitized when serializing to json
*/
Expand Down Expand Up @@ -199,4 +201,40 @@ public static class InstanceProxyProperties {

}

@lombok.Data
public static class EndpointCacheProperties {

/**
* Whether server-side caching of proxied actuator GET responses is enabled.
*/
private boolean enabled = true;

/**
* Default TTL for cached responses.
*/
@DurationUnit(ChronoUnit.MILLIS)
private Duration defaultTtl = Duration.ofMinutes(5);

/**
* TTL per endpoint id. Overrides default-ttl for a specific endpoint. Example:
* {@code spring.boot.admin.endpoint-cache.ttl.mappings=10m}
*/
@DurationUnit(ChronoUnit.MILLIS)
private Map<String, Duration> ttl = new HashMap<>();

/**
* Endpoint ids whose responses should be cached. Only safe GET requests to these
* endpoints are cached.
*/
private Set<String> endpoints = new HashSet<>(
asList("mappings", "configprops", "beans", "conditions", "sbom", "startup"));

/**
* Maximum response body size in bytes that will be cached. Responses larger than
* this threshold are forwarded as-is without caching.
*/
private long maxPayloadSize = 10L * 1024 * 1024;

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2024 the original author or authors.
* Copyright 2014-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,8 +16,12 @@

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

import org.reactivestreams.Publisher;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.context.ApplicationEventPublisher;
Expand All @@ -27,12 +31,18 @@
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import tools.jackson.databind.module.SimpleModule;

import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
import de.codecentric.boot.admin.server.eventstore.InstanceEventStore;
import de.codecentric.boot.admin.server.services.ApplicationRegistry;
import de.codecentric.boot.admin.server.services.InstanceRegistry;
import de.codecentric.boot.admin.server.utils.jackson.AdminServerModule;
import de.codecentric.boot.admin.server.web.ApplicationsController;
import de.codecentric.boot.admin.server.web.HttpHeaderFilter;
import de.codecentric.boot.admin.server.web.InstanceWebProxy;
import de.codecentric.boot.admin.server.web.InstancesController;
import de.codecentric.boot.admin.server.web.cache.ActuatorResponseCache;
import de.codecentric.boot.admin.server.web.cache.CacheInvalidationTrigger;
import de.codecentric.boot.admin.server.web.cache.InMemoryActuatorResponseCache;
import de.codecentric.boot.admin.server.web.client.InstanceWebClient;

@Configuration(proxyBeanMethods = false)
Expand Down Expand Up @@ -62,6 +72,21 @@ public ApplicationsController applicationsController(ApplicationRegistry applica
return new ApplicationsController(applicationRegistry, applicationEventPublisher);
}

@Bean
@ConditionalOnMissingBean(ActuatorResponseCache.class)
@ConditionalOnProperty(prefix = "spring.boot.admin.endpoint-cache", name = "enabled", matchIfMissing = true)
public InMemoryActuatorResponseCache actuatorResponseCache() {
return new InMemoryActuatorResponseCache(this.adminServerProperties.getEndpointCache());
}

@Bean(initMethod = "start", destroyMethod = "stop")
@ConditionalOnBean(ActuatorResponseCache.class)
@ConditionalOnMissingBean(CacheInvalidationTrigger.class)
public CacheInvalidationTrigger cacheInvalidationTrigger(ActuatorResponseCache responseCache,
Publisher<InstanceEvent> events) {
return new CacheInvalidationTrigger(events, responseCache);
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public static class ReactiveRestApiConfiguration {
Expand All @@ -75,11 +100,14 @@ public ReactiveRestApiConfiguration(AdminServerProperties adminServerProperties)
@Bean
@ConditionalOnMissingBean
public de.codecentric.boot.admin.server.web.reactive.InstancesProxyController instancesProxyController(
InstanceRegistry instanceRegistry, InstanceWebClient.Builder instanceWebClientBuilder) {
InstanceRegistry instanceRegistry, InstanceWebClient.Builder instanceWebClientBuilder,
ObjectProvider<ActuatorResponseCache> responseCache) {
HttpHeaderFilter headerFilter = new HttpHeaderFilter(
this.adminServerProperties.getInstanceProxy().getIgnoredHeaders());
InstanceWebProxy instanceWebProxy = new InstanceWebProxy(instanceWebClientBuilder.build(),
responseCache.getIfAvailable(), headerFilter);
return new de.codecentric.boot.admin.server.web.reactive.InstancesProxyController(
this.adminServerProperties.getContextPath(),
this.adminServerProperties.getInstanceProxy().getIgnoredHeaders(), instanceRegistry,
instanceWebClientBuilder.build());
this.adminServerProperties.getContextPath(), headerFilter, instanceRegistry, instanceWebProxy);
}

@Bean
Expand Down Expand Up @@ -108,11 +136,14 @@ public ServletRestApiConfiguration(AdminServerProperties adminServerProperties)
@Bean
@ConditionalOnMissingBean
public de.codecentric.boot.admin.server.web.servlet.InstancesProxyController instancesProxyController(
InstanceRegistry instanceRegistry, InstanceWebClient.Builder instanceWebClientBuilder) {
InstanceRegistry instanceRegistry, InstanceWebClient.Builder instanceWebClientBuilder,
ObjectProvider<ActuatorResponseCache> responseCache) {
HttpHeaderFilter headerFilter = new HttpHeaderFilter(
this.adminServerProperties.getInstanceProxy().getIgnoredHeaders());
InstanceWebProxy instanceWebProxy = new InstanceWebProxy(instanceWebClientBuilder.build(),
responseCache.getIfAvailable(), headerFilter);
return new de.codecentric.boot.admin.server.web.servlet.InstancesProxyController(
this.adminServerProperties.getContextPath(),
this.adminServerProperties.getInstanceProxy().getIgnoredHeaders(), instanceRegistry,
instanceWebClientBuilder.build());
this.adminServerProperties.getContextPath(), headerFilter, instanceRegistry, instanceWebProxy);
}

@Bean
Expand Down
Loading