diff --git a/.editorconfig b/.editorconfig index 4e7ca1f9d4f..00b3003c0f0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,6 +6,9 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false +max_line_length = 120 +ij_markdown_wrap_text_if_long = true +ij_markdown_format_tables = true [*.java] indent_style = tab @@ -23,7 +26,7 @@ ij_java_blank_lines_after_class_header = 0 ij_java_doc_enable_formatting = false ij_java_class_count_to_use_import_on_demand = 100 ij_java_names_count_to_use_import_on_demand = 100 -ij_java_imports_layout = |, java.**, |, jakarta.**, *, |, de.codecentric.boot.admin.**, |, $* +ij_java_imports_layout = |, java.**, |, jakarta.**, *, tools.**, |, de.codecentric.boot.admin.**, |, $* ij_java_layout_static_imports_separately = true [*.xml] diff --git a/.github/workflows/deploy-documentation.yml b/.github/workflows/deploy-documentation.yml index db601a03f85..b181b221f9f 100644 --- a/.github/workflows/deploy-documentation.yml +++ b/.github/workflows/deploy-documentation.yml @@ -4,11 +4,11 @@ on: workflow_dispatch: inputs: releaseversion: - description: 'Release version' + description: 'Version to publish the documentation for. This should be a tag that exists in the repository.' + type: string required: true - default: '3.0.0' copyDocsToCurrent: - description: "Should the docs be published at https://docs.spring-boot-admin.com? Otherwise they will be accessible by version number only." + description: "Mark docs as 'latest'? This will create a redirect from /current to the version being published. This should only be set for the latest version of the documentation." required: true type: boolean default: false diff --git a/.github/workflows/release-to-maven-central.yml b/.github/workflows/release-to-maven-central.yml index 8db018e2645..70296023308 100644 --- a/.github/workflows/release-to-maven-central.yml +++ b/.github/workflows/release-to-maven-central.yml @@ -4,11 +4,10 @@ on: workflow_dispatch: inputs: releaseversion: - description: 'Release version' + description: "Version to release and publish to maven central." required: true - default: '3.0.0' copyDocsToCurrent: - description: "Should the docs be published at https://docs.spring-boot-admin.com? Otherwise they will be accessible by version number only." + description: "Mark docs as 'latest'? This will create a redirect from /current to the version being published. This should only be set for the latest version of the documentation." required: true type: boolean default: false diff --git a/.gitignore b/.gitignore index ef7d5b97a97..d42edd459f9 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ node/ .DS_Store mockServiceWorker.js +/.github/agents/ diff --git a/pom.xml b/pom.xml index 80bb6002e2e..efcdde00219 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ https://github.com/codecentric/spring-boot-admin/ - 3.5.8-SNAPSHOT + 4.0.0-SNAPSHOT 17 v22.12.0 @@ -47,10 +47,10 @@ true - 3.5.10 - 2025.0.0 + 4.0.2 + 2025.1.1 - 2.4.3 + 2.5.0 12.3.1 3.0.2 @@ -58,7 +58,6 @@ 3.13.2 5.6.0 4.3.0 - 2.0.3 12.1.6 diff --git a/spring-boot-admin-build/pom.xml b/spring-boot-admin-build/pom.xml index dd04572d055..a40ba486ca9 100644 --- a/spring-boot-admin-build/pom.xml +++ b/spring-boot-admin-build/pom.xml @@ -51,8 +51,8 @@ org.jolokia - jolokia-support-spring - ${jolokia-support-spring.version} + jolokia-support-springboot + ${jolokia-support-springboot.version} org.wiremock @@ -85,18 +85,6 @@ ${awaitility.version} test - - org.testcontainers - testcontainers - ${testcontainers.version} - test - - - org.testcontainers - testcontainers-junit-jupiter - ${testcontainers.version} - test - diff --git a/spring-boot-admin-client/pom.xml b/spring-boot-admin-client/pom.xml index 1a5334c620f..ead3f1533f8 100644 --- a/spring-boot-admin-client/pom.xml +++ b/spring-boot-admin-client/pom.xml @@ -41,27 +41,27 @@ spring-boot-starter-actuator - org.springframework - spring-web + org.springframework.boot + spring-boot-starter-restclient org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webflux true org.springframework.boot - spring-boot-autoconfigure-processor + spring-boot-starter-webmvc true org.springframework.boot - spring-boot-configuration-processor + spring-boot-autoconfigure-processor true - org.springframework - spring-webflux + org.springframework.boot + spring-boot-configuration-processor true @@ -74,11 +74,6 @@ spring-boot-starter-test test - - org.springframework.boot - spring-boot-starter-webflux - test - org.wiremock wiremock-standalone diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/ClientProperties.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/ClientProperties.java index de378ce6360..dc55dcddc2d 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/ClientProperties.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/ClientProperties.java @@ -19,11 +19,11 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; +import org.jspecify.annotations.Nullable; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.convert.DurationUnit; import org.springframework.core.env.Environment; -import org.springframework.lang.Nullable; @lombok.Data @ConfigurationProperties(prefix = "spring.boot.admin.client") @@ -60,21 +60,18 @@ public class ClientProperties { /** * Username for basic authentication on admin server */ - @Nullable - private String username; + @Nullable private String username; /** * Password for basic authentication on admin server */ - @Nullable - private String password; + @Nullable private String password; /** * Enable automatic deregistration on shutdown If not set it defaults to true if an * active {@link CloudPlatform} is present; */ - @Nullable - private Boolean autoDeregistration = null; + @Nullable private Boolean autoDeregistration = null; /** * Enable automatic registration when the application is ready. diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/ClientRuntimeHints.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/ClientRuntimeHints.java index b8f5ffaf2f8..67d4efea819 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/ClientRuntimeHints.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/ClientRuntimeHints.java @@ -21,7 +21,7 @@ import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.boot.web.server.context.WebServerInitializedEvent; import org.springframework.context.annotation.Configuration; import de.codecentric.boot.admin.client.registration.Application; diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/CloudFoundryApplicationProperties.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/CloudFoundryApplicationProperties.java index 4f376a22680..4fa321c701d 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/CloudFoundryApplicationProperties.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/CloudFoundryApplicationProperties.java @@ -19,18 +19,16 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.lang.Nullable; @lombok.Data @ConfigurationProperties("vcap.application") public class CloudFoundryApplicationProperties { - @Nullable - private String applicationId; + @Nullable private String applicationId; - @Nullable - private String instanceIndex; + @Nullable private String instanceIndex; private List uris = new ArrayList<>(); diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/InstanceProperties.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/InstanceProperties.java index 3d1a49cd921..4a6e63c42c7 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/InstanceProperties.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/InstanceProperties.java @@ -19,9 +19,9 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.lang.Nullable; @lombok.Data @ConfigurationProperties(prefix = "spring.boot.admin.client.instance") @@ -31,43 +31,37 @@ public class InstanceProperties { * Management-url to register with. Inferred at runtime, can be overridden in case the * reachable URL is different (e.g. Docker). */ - @Nullable - private String managementUrl; + @Nullable private String managementUrl; /** * Base url for computing the management-url to register with. The path is inferred at * runtime, and appended to the base url. */ - @Nullable - private String managementBaseUrl; + @Nullable private String managementBaseUrl; /** * Client-service-URL register with. Inferred at runtime, can be overridden in case * the reachable URL is different (e.g. Docker). */ - @Nullable - private String serviceUrl; + @Nullable private String serviceUrl; /** * Base url for computing the service-url to register with. The path is inferred at * runtime, and appended to the base url. */ - @Nullable - private String serviceBaseUrl; + @Nullable private String serviceBaseUrl; /** * Path for computing the service-url to register with. If not specified, defaults to * "/" */ - @Nullable - private String servicePath; + @Nullable private String servicePath; /** * Client-health-URL to register with. Inferred at runtime, can be overridden in case * the reachable URL is different (e.g. Docker). Must be unique all services registry. */ - @Nullable - private String healthUrl; + @Nullable private String healthUrl; /** * Name to register with. Defaults to ${spring.application.name} diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfiguration.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfiguration.java index 1c361bf02dd..75e609dc351 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfiguration.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfiguration.java @@ -29,33 +29,29 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties; -import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; -import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; -import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.web.server.autoconfigure.ServerProperties; +import org.springframework.boot.webflux.autoconfigure.WebFluxProperties; +import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration; +import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.core.env.Environment; import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; import org.springframework.web.client.RestClient; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.reactive.function.client.WebClient; +import tools.jackson.databind.json.JsonMapper; import de.codecentric.boot.admin.client.registration.ApplicationFactory; import de.codecentric.boot.admin.client.registration.ApplicationRegistrator; -import de.codecentric.boot.admin.client.registration.BlockingRegistrationClient; import de.codecentric.boot.admin.client.registration.DefaultApplicationRegistrator; import de.codecentric.boot.admin.client.registration.ReactiveApplicationFactory; -import de.codecentric.boot.admin.client.registration.ReactiveRegistrationClient; import de.codecentric.boot.admin.client.registration.RegistrationApplicationListener; import de.codecentric.boot.admin.client.registration.RegistrationClient; import de.codecentric.boot.admin.client.registration.RestClientRegistrationClient; @@ -64,14 +60,10 @@ import de.codecentric.boot.admin.client.registration.metadata.MetadataContributor; import de.codecentric.boot.admin.client.registration.metadata.StartupDateMetadataContributor; -import static org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; -import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; - @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication @Conditional(SpringBootAdminClientEnabledCondition.class) -@AutoConfigureAfter({ WebEndpointAutoConfiguration.class, RestClientAutoConfiguration.class, - RestTemplateAutoConfiguration.class, WebClientAutoConfiguration.class }) +@AutoConfigureAfter({ WebEndpointAutoConfiguration.class, RestClientAutoConfiguration.class }) @EnableConfigurationProperties({ ClientProperties.class, InstanceProperties.class, ServerProperties.class, ManagementServerProperties.class }) public class SpringBootAdminClientAutoConfiguration { @@ -139,59 +131,33 @@ public ApplicationFactory applicationFactory(InstanceProperties instance, Manage } @Configuration(proxyBeanMethods = false) - @ConditionalOnBean(RestTemplateBuilder.class) - public static class BlockingRegistrationClientConfig { - - @Bean - @ConditionalOnMissingBean - public RegistrationClient registrationClient(ClientProperties client) { - RestTemplateBuilder builder = new RestTemplateBuilder().connectTimeout(client.getConnectTimeout()) - .readTimeout(client.getReadTimeout()); - - if (client.getUsername() != null && client.getPassword() != null) { - builder = builder.basicAuthentication(client.getUsername(), client.getPassword()); - } - - RestTemplate build = builder.build(); - return new BlockingRegistrationClient(build); - } - - } - - @Configuration(proxyBeanMethods = false) - @ConditionalOnBean({ RestClient.Builder.class, ClientHttpRequestFactoryBuilder.class }) + @ConditionalOnBean(RestClient.Builder.class) public static class RestClientRegistrationClientConfig { @Bean @ConditionalOnMissingBean public RegistrationClient registrationClient(ClientProperties client, RestClient.Builder restClientBuilder, - ClientHttpRequestFactoryBuilder clientHttpRequestFactoryBuilder) { - var factorySettings = ClientHttpRequestFactorySettings.defaults() + ObjectProvider objectMapper) { + var factorySettings = HttpClientSettings.defaults() .withConnectTimeout(client.getConnectTimeout()) .withReadTimeout(client.getReadTimeout()); - var clientHttpRequestFactory = clientHttpRequestFactoryBuilder.build(factorySettings); - restClientBuilder = restClientBuilder.requestFactory(clientHttpRequestFactory); - if (client.getUsername() != null && client.getPassword() != null) { - restClientBuilder = restClientBuilder - .requestInterceptor(new BasicAuthenticationInterceptor(client.getUsername(), client.getPassword())); - } - var restClient = restClientBuilder.build(); - return new RestClientRegistrationClient(restClient); - } - } + var clientHttpRequestFactory = ClientHttpRequestFactoryBuilder.detect().build(factorySettings); - @Configuration(proxyBeanMethods = false) - @ConditionalOnBean(WebClient.Builder.class) - public static class ReactiveRegistrationClientConfig { + restClientBuilder.requestFactory(clientHttpRequestFactory); + + objectMapper.ifAvailable((mapper) -> restClientBuilder.messageConverters((configurer) -> { + configurer.removeIf(JacksonJsonHttpMessageConverter.class::isInstance); + configurer.add(new JacksonJsonHttpMessageConverter(mapper)); + })); - @Bean - @ConditionalOnMissingBean - public RegistrationClient registrationClient(ClientProperties client, WebClient.Builder webClient) { if (client.getUsername() != null && client.getPassword() != null) { - webClient = webClient.filter(basicAuthentication(client.getUsername(), client.getPassword())); + restClientBuilder + .requestInterceptor(new BasicAuthenticationInterceptor(client.getUsername(), client.getPassword())); } - return new ReactiveRegistrationClient(webClient.build(), client.getReadTimeout()); + + var restClient = restClientBuilder.build(); + return new RestClientRegistrationClient(restClient); } } diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientCloudFoundryAutoConfiguration.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientCloudFoundryAutoConfiguration.java index a9326ab20fd..1e062e30bb5 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientCloudFoundryAutoConfiguration.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientCloudFoundryAutoConfiguration.java @@ -27,9 +27,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.cloud.CloudPlatform; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.server.autoconfigure.ServerProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringNativeClientAutoConfiguration.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringNativeClientAutoConfiguration.java index 198a9a2a843..c171328a768 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringNativeClientAutoConfiguration.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/SpringNativeClientAutoConfiguration.java @@ -19,8 +19,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; +import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportRuntimeHints; @@ -28,8 +27,7 @@ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication @Conditional(SpringBootAdminClientEnabledCondition.class) -@AutoConfigureAfter({ WebEndpointAutoConfiguration.class, RestTemplateAutoConfiguration.class, - WebClientAutoConfiguration.class }) +@AutoConfigureAfter({ WebEndpointAutoConfiguration.class, RestClientAutoConfiguration.class }) @ImportRuntimeHints({ ClientRuntimeHints.class }) public class SpringNativeClientAutoConfiguration { diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/package-info.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/package-info.java index 9ac974012b4..1600cf8c7dc 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/package-info.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/config/package-info.java @@ -14,9 +14,7 @@ * limitations under the License. */ -@NonNullApi -@NonNullFields +@NullMarked package de.codecentric.boot.admin.client.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/BlockingRegistrationClient.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/BlockingRegistrationClient.java deleted file mode 100644 index a34b61bcba5..00000000000 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/BlockingRegistrationClient.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2014-2023 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.codecentric.boot.admin.client.registration; - -import java.util.Collections; -import java.util.Map; - -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.client.RestTemplate; - -public class BlockingRegistrationClient implements RegistrationClient { - - private static final ParameterizedTypeReference> RESPONSE_TYPE = new ParameterizedTypeReference<>() { - }; - - private final RestTemplate restTemplate; - - public BlockingRegistrationClient(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - @Override - public String register(String adminUrl, Application application) { - ResponseEntity> response = this.restTemplate.exchange(adminUrl, HttpMethod.POST, - new HttpEntity<>(application, this.createRequestHeaders()), RESPONSE_TYPE); - return response.getBody().get("id").toString(); - } - - @Override - public void deregister(String adminUrl, String id) { - this.restTemplate.delete(adminUrl + '/' + id); - } - - protected HttpHeaders createRequestHeaders() { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - return HttpHeaders.readOnlyHttpHeaders(headers); - } - -} diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/CloudFoundryApplicationFactory.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/CloudFoundryApplicationFactory.java index bdab8c466ea..07f337fb1c0 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/CloudFoundryApplicationFactory.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/CloudFoundryApplicationFactory.java @@ -19,7 +19,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; -import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.web.server.autoconfigure.ServerProperties; import org.springframework.util.StringUtils; import de.codecentric.boot.admin.client.config.CloudFoundryApplicationProperties; diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/DefaultApplicationFactory.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/DefaultApplicationFactory.java index 4efe51cfd5b..93f1669ec3f 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/DefaultApplicationFactory.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/DefaultApplicationFactory.java @@ -21,15 +21,15 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.web.context.WebServerInitializedEvent; import org.springframework.boot.web.server.Ssl; +import org.springframework.boot.web.server.autoconfigure.ServerProperties; +import org.springframework.boot.web.server.context.WebServerInitializedEvent; import org.springframework.context.event.EventListener; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; @@ -57,11 +57,9 @@ public class DefaultApplicationFactory implements ApplicationFactory { private final MetadataContributor metadataContributor; - @Nullable - private Integer localServerPort; + @Nullable private Integer localServerPort; - @Nullable - private Integer localManagementPort; + @Nullable private Integer localManagementPort; public DefaultApplicationFactory(InstanceProperties instance, ManagementServerProperties management, ServerProperties server, PathMappedEndpoints pathMappedEndpoints, WebEndpointProperties webEndpoint, diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ReactiveApplicationFactory.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ReactiveApplicationFactory.java index 1c65f63d2f2..6dd86c18440 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ReactiveApplicationFactory.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ReactiveApplicationFactory.java @@ -19,9 +19,9 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties; import org.springframework.boot.web.server.Ssl; +import org.springframework.boot.web.server.autoconfigure.ServerProperties; +import org.springframework.boot.webflux.autoconfigure.WebFluxProperties; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ReactiveRegistrationClient.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ReactiveRegistrationClient.java deleted file mode 100644 index d622b952f21..00000000000 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ReactiveRegistrationClient.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2014-2023 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.codecentric.boot.admin.client.registration; - -import java.time.Duration; -import java.util.Collections; -import java.util.Map; - -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.web.reactive.function.client.WebClient; - -public class ReactiveRegistrationClient implements RegistrationClient { - - private static final ParameterizedTypeReference> RESPONSE_TYPE = new ParameterizedTypeReference<>() { - }; - - private final WebClient webclient; - - private final Duration timeout; - - public ReactiveRegistrationClient(WebClient webclient, Duration timeout) { - this.webclient = webclient; - this.timeout = timeout; - } - - @Override - public String register(String adminUrl, Application application) { - Map response = this.webclient.post() - .uri(adminUrl) - .headers(this::setRequestHeaders) - .bodyValue(application) - .retrieve() - .bodyToMono(RESPONSE_TYPE) - .timeout(this.timeout) - .block(); - return response.get("id").toString(); - } - - @Override - public void deregister(String adminUrl, String id) { - this.webclient.delete().uri(adminUrl + '/' + id).retrieve().toBodilessEntity().timeout(this.timeout).block(); - } - - protected void setRequestHeaders(HttpHeaders headers) { - headers.setContentType(MediaType.APPLICATION_JSON); - headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - } - -} diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/RegistrationApplicationListener.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/RegistrationApplicationListener.java index c226f10027b..35fd9c5e277 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/RegistrationApplicationListener.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/RegistrationApplicationListener.java @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.concurrent.ScheduledFuture; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; @@ -28,7 +29,6 @@ import org.springframework.context.event.EventListener; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.lang.Nullable; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; /** @@ -51,13 +51,17 @@ public class RegistrationApplicationListener implements InitializingBean, Dispos private Duration registerPeriod = Duration.ofSeconds(10); - @Nullable - private volatile ScheduledFuture scheduledTask; + @Nullable private volatile ScheduledFuture scheduledTask; public RegistrationApplicationListener(ApplicationRegistrator registrator) { this(registrator, registrationTaskScheduler()); } + RegistrationApplicationListener(ApplicationRegistrator registrator, ThreadPoolTaskScheduler taskScheduler) { + this.registrator = registrator; + this.taskScheduler = taskScheduler; + } + private static ThreadPoolTaskScheduler registrationTaskScheduler() { ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.setPoolSize(1); @@ -66,11 +70,6 @@ private static ThreadPoolTaskScheduler registrationTaskScheduler() { return taskScheduler; } - RegistrationApplicationListener(ApplicationRegistrator registrator, ThreadPoolTaskScheduler taskScheduler) { - this.registrator = registrator; - this.taskScheduler = taskScheduler; - } - @EventListener @Order(Ordered.LOWEST_PRECEDENCE) public void onApplicationReady(ApplicationReadyEvent event) { diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ServletApplicationFactory.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ServletApplicationFactory.java index 0c867c963d4..3873418f2d3 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ServletApplicationFactory.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/ServletApplicationFactory.java @@ -20,9 +20,9 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath; import org.springframework.boot.web.server.Ssl; +import org.springframework.boot.web.server.autoconfigure.ServerProperties; +import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/metadata/package-info.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/metadata/package-info.java index 922ad64db5c..2a50faa7601 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/metadata/package-info.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/metadata/package-info.java @@ -14,9 +14,7 @@ * limitations under the License. */ -@NonNullApi -@NonNullFields +@NullMarked package de.codecentric.boot.admin.client.registration.metadata; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/package-info.java b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/package-info.java index e332f2b2c6c..bbba736f6d5 100644 --- a/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/package-info.java +++ b/spring-boot-admin-client/src/main/java/de/codecentric/boot/admin/client/registration/package-info.java @@ -14,9 +14,7 @@ * limitations under the License. */ -@NonNullApi -@NonNullFields +@NullMarked package de.codecentric.boot.admin.client.registration; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfigurationTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfigurationTest.java index 8b53562256d..1041524e2be 100644 --- a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfigurationTest.java +++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientAutoConfigurationTest.java @@ -22,26 +22,19 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.http.client.HttpClientAutoConfiguration; import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties; -import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; -import org.springframework.boot.context.annotation.UserConfigurations; +import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration; +import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import org.springframework.boot.webflux.autoconfigure.WebFluxProperties; +import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestClient; -import org.springframework.web.client.RestTemplate; import de.codecentric.boot.admin.client.registration.ApplicationRegistrator; -import de.codecentric.boot.admin.client.registration.BlockingRegistrationClient; import de.codecentric.boot.admin.client.registration.RegistrationClient; import static org.assertj.core.api.Assertions.assertThat; @@ -50,8 +43,8 @@ class SpringBootAdminClientAutoConfigurationTest { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, RestTemplateAutoConfiguration.class, - SpringBootAdminClientAutoConfiguration.class)); + DispatcherServletAutoConfiguration.class, HttpClientAutoConfiguration.class, + RestClientAutoConfiguration.class, SpringBootAdminClientAutoConfiguration.class)); @Test void not_active() { @@ -84,39 +77,14 @@ void nonWebEnvironment() { @Test void reactiveEnvironment() { ReactiveWebApplicationContextRunner reactiveContextRunner = new ReactiveWebApplicationContextRunner() - .withConfiguration( - AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, - WebClientAutoConfiguration.class, SpringBootAdminClientAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, HttpClientAutoConfiguration.class, + RestClientAutoConfiguration.class, SpringBootAdminClientAutoConfiguration.class)) .withBean(WebFluxProperties.class); reactiveContextRunner.withPropertyValues("spring.boot.admin.client.url:http://localhost:8081") .run((context) -> assertThat(context).hasSingleBean(ApplicationRegistrator.class)); } - @Test - void blockingClientInBlockingEnvironment() { - WebApplicationContextRunner webApplicationContextRunner = new WebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class, - WebEndpointAutoConfiguration.class, DispatcherServletAutoConfiguration.class, - RestTemplateAutoConfiguration.class, SpringBootAdminClientAutoConfiguration.class)); - - webApplicationContextRunner - .withPropertyValues("spring.boot.admin.client.url:http://localhost:8081", - "spring.boot.admin.client.connectTimeout=1337", "spring.boot.admin.client.readTimeout=42") - .run((context) -> { - RegistrationClient registrationClient = context.getBean(RegistrationClient.class); - RestTemplate restTemplate = (RestTemplate) ReflectionTestUtils.getField(registrationClient, - "restTemplate"); - assertThat(restTemplate).isNotNull(); - - ClientHttpRequestFactory requestFactory = restTemplate.getRequestFactory(); - - Integer connectTimeout = (Integer) ReflectionTestUtils.getField(requestFactory, "connectTimeout"); - assertThat(connectTimeout).isEqualTo(1337); - Duration readTimeout = (Duration) ReflectionTestUtils.getField(requestFactory, "readTimeout"); - assertThat(readTimeout).isEqualTo(Duration.ofMillis(42)); - }); - } - @Test void restClientRegistrationClientInBlockingEnvironment() { WebApplicationContextRunner webApplicationContextRunner = new WebApplicationContextRunner().withConfiguration( @@ -143,49 +111,4 @@ void restClientRegistrationClientInBlockingEnvironment() { }); } - @Test - void customBlockingClientInReactiveEnvironment() { - ReactiveWebApplicationContextRunner reactiveContextRunner = new ReactiveWebApplicationContextRunner() - .withConfiguration(UserConfigurations.of(CustomBlockingConfiguration.class)) - .withConfiguration( - AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, - WebClientAutoConfiguration.class, SpringBootAdminClientAutoConfiguration.class)) - .withBean(WebFluxProperties.class); - - reactiveContextRunner.withPropertyValues("spring.boot.admin.client.url:http://localhost:8081") - .run((context) -> { - assertThat(context).hasSingleBean(ApplicationRegistrator.class); - assertThat(context).getBean("registrationClient") - .isEqualTo(context.getBean(CustomBlockingConfiguration.class).registrationClient); - }); - } - - @Test - void customBlockingClientInBlockingEnvironment() { - WebApplicationContextRunner webApplicationContextRunner = new WebApplicationContextRunner() - .withConfiguration(UserConfigurations.of(CustomBlockingConfiguration.class)) - .withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class, - WebEndpointAutoConfiguration.class, DispatcherServletAutoConfiguration.class, - RestTemplateAutoConfiguration.class, SpringBootAdminClientAutoConfiguration.class)); - - webApplicationContextRunner.withPropertyValues("spring.boot.admin.client.url:http://localhost:8081") - .run((context) -> { - assertThat(context).hasSingleBean(ApplicationRegistrator.class); - assertThat(context).getBean("registrationClient") - .isEqualTo(context.getBean(CustomBlockingConfiguration.class).registrationClient); - }); - } - - @Configuration - public static class CustomBlockingConfiguration { - - final RegistrationClient registrationClient = new BlockingRegistrationClient(new RestTemplate()); - - @Bean - public RegistrationClient registrationClient() { - return registrationClient; - } - - } - } diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientCloudFoundryAutoConfigurationTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientCloudFoundryAutoConfigurationTest.java index 9dfc12b08f7..f7b31be1f25 100644 --- a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientCloudFoundryAutoConfigurationTest.java +++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientCloudFoundryAutoConfigurationTest.java @@ -20,10 +20,11 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.http.client.autoconfigure.HttpClientAutoConfiguration; +import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration; +import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; import de.codecentric.boot.admin.client.registration.ApplicationFactory; import de.codecentric.boot.admin.client.registration.CloudFoundryApplicationFactory; @@ -37,7 +38,8 @@ class SpringBootAdminClientCloudFoundryAutoConfigurationTest { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, WebMvcAutoConfiguration.class, DispatcherServletAutoConfiguration.class, - RestTemplateAutoConfiguration.class, SpringBootAdminClientAutoConfiguration.class, + HttpClientAutoConfiguration.class, RestClientAutoConfiguration.class, + SpringBootAdminClientAutoConfiguration.class, SpringBootAdminClientCloudFoundryAutoConfiguration.class)); @Test diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientRegistrationClientAutoConfigurationTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientRegistrationClientAutoConfigurationTest.java index cce6ba8086c..d52af7bc8e5 100644 --- a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientRegistrationClientAutoConfigurationTest.java +++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/config/SpringBootAdminClientRegistrationClientAutoConfigurationTest.java @@ -26,15 +26,11 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener; -import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; import org.springframework.boot.test.context.runner.WebApplicationContextRunner; -import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration; import org.springframework.web.client.RestClient; -import org.springframework.web.reactive.function.client.WebClient; -import de.codecentric.boot.admin.client.registration.BlockingRegistrationClient; -import de.codecentric.boot.admin.client.registration.ReactiveRegistrationClient; import de.codecentric.boot.admin.client.registration.RegistrationClient; import de.codecentric.boot.admin.client.registration.RestClientRegistrationClient; @@ -46,7 +42,7 @@ public class SpringBootAdminClientRegistrationClientAutoConfigurationTest { @MethodSource("contextRunnerCustomizations") void autoConfiguresRegistrationClient(String testCaseName, Function customizer, - Class expectedRegistrationClient) { + Class expectedRegistrationClient) { WebApplicationContextRunner webApplicationContextRunner = new WebApplicationContextRunner() .withConfiguration( AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, @@ -62,107 +58,21 @@ void autoConfiguresRegistrationClient(String testCaseName, }); } - /** - * - * @return context runner customizations - */ public static Stream contextRunnerCustomizations() { return Stream.of(// Arguments.of(// - "Test case 01", // + "RestClientBuilder with ClientHttpRequestFactoryBuilder", // customizer() // - .withRestTemplateBuilder() - .withRestClientBuilder() - .withClientHttpRequestFactoryBuilder() - .withWebClientBuilder() - .build(), // - ReactiveRegistrationClient.class), - Arguments.of(// - "Test case 02", // - customizer() // - .withRestTemplateBuilder() .withRestClientBuilder() .withClientHttpRequestFactoryBuilder() .build(), // RestClientRegistrationClient.class), Arguments.of(// - "Test case 03", // + "RestClientBuilder only", // customizer() // - .withRestTemplateBuilder() .withRestClientBuilder() - .withWebClientBuilder() - .build(), // - ReactiveRegistrationClient.class), - Arguments.of(// - "Test case 04", // - customizer() // - .withRestTemplateBuilder() - .withRestClientBuilder() - .build(), // - BlockingRegistrationClient.class), - Arguments.of(// - "Test case 05", // - customizer() // - .withRestTemplateBuilder() - .withClientHttpRequestFactoryBuilder() - .withWebClientBuilder() .build(), // - ReactiveRegistrationClient.class), - Arguments.of(// - "Test case 06", // - customizer() // - .withRestTemplateBuilder() - .withClientHttpRequestFactoryBuilder() - .build(), // - BlockingRegistrationClient.class), - Arguments.of(// - "Test case 07", // - customizer() // - .withRestTemplateBuilder() - .withWebClientBuilder() - .build(), // - ReactiveRegistrationClient.class), - Arguments.of(// - "Test case 08", // - customizer() // - .withRestTemplateBuilder() - .build(), // - BlockingRegistrationClient.class), - Arguments.of(// - "Test case 09", // - customizer() // - .withRestClientBuilder() - .withClientHttpRequestFactoryBuilder() - .withWebClientBuilder() - .build(), // - ReactiveRegistrationClient.class), - Arguments.of(// - "Test case 10", // - customizer() // - .withRestClientBuilder() - .withClientHttpRequestFactoryBuilder() - .build(), // - RestClientRegistrationClient.class), - Arguments.of(// - "Test case 11", // - customizer() // - .withRestClientBuilder() - .withWebClientBuilder() - .build(), // - ReactiveRegistrationClient.class), - Arguments.of(// - "Test case 13", // - customizer() // - .withClientHttpRequestFactoryBuilder() - .withWebClientBuilder() - .build(), // - ReactiveRegistrationClient.class), - Arguments.of(// - "Test case 15", // - customizer() // - .withWebClientBuilder() - .build(), // - ReactiveRegistrationClient.class) // + RestClientRegistrationClient.class) // ); } @@ -174,12 +84,6 @@ private static final class ContextRunnerCustomizerBuilder { private Function customizer = (runner) -> runner; - ContextRunnerCustomizerBuilder withRestTemplateBuilder() { - customizer = customizer - .andThen((runner) -> runner.withBean(RestTemplateBuilder.class, RestTemplateBuilder::new)); - return this; - } - ContextRunnerCustomizerBuilder withRestClientBuilder() { customizer = customizer.andThen((runner) -> runner.withBean(RestClient.Builder.class, RestClient::builder)); return this; @@ -191,11 +95,6 @@ ContextRunnerCustomizerBuilder withClientHttpRequestFactoryBuilder() { return this; } - ContextRunnerCustomizerBuilder withWebClientBuilder() { - customizer = customizer.andThen((runner) -> runner.withBean(WebClient.Builder.class, WebClient::builder)); - return this; - } - Function build() { return customizer; } diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ApplicationTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ApplicationTest.java index 8bb18054bfa..91f1adff66c 100644 --- a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ApplicationTest.java +++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ApplicationTest.java @@ -16,21 +16,18 @@ package de.codecentric.boot.admin.client.registration; -import java.io.IOException; - -import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; import org.junit.jupiter.api.Test; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import tools.jackson.databind.json.JsonMapper; import static org.assertj.core.api.Assertions.assertThat; class ApplicationTest { @Test - void test_json_format() throws IOException { - ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build(); + void test_json_format() { + JsonMapper jsonMapper = JsonMapper.builder().build(); Application app = Application.create("test") .healthUrl("http://health") @@ -38,7 +35,7 @@ void test_json_format() throws IOException { .managementUrl("http://management") .build(); - DocumentContext json = JsonPath.parse(objectMapper.writeValueAsString(app)); + DocumentContext json = JsonPath.parse(jsonMapper.writeValueAsString(app)); assertThat((String) json.read("$.name")).isEqualTo("test"); assertThat((String) json.read("$.serviceUrl")).isEqualTo("http://service"); diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/BlockingRegistrationClientTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/BlockingRegistrationClientTest.java deleted file mode 100644 index cd891f6042a..00000000000 --- a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/BlockingRegistrationClientTest.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2014-2023 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.codecentric.boot.admin.client.registration; - -import org.junit.jupiter.api.BeforeEach; -import org.springframework.web.client.RestTemplate; - -class BlockingRegistrationClientTest extends AbstractRegistrationClientTest { - - @BeforeEach - void setUp() { - super.setUp(new BlockingRegistrationClient(new RestTemplate())); - } - -} diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/CloudFoundryApplicationFactoryTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/CloudFoundryApplicationFactoryTest.java index c0fcae7c33f..995e8868ab9 100644 --- a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/CloudFoundryApplicationFactoryTest.java +++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/CloudFoundryApplicationFactoryTest.java @@ -23,7 +23,7 @@ import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; -import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.web.server.autoconfigure.ServerProperties; import de.codecentric.boot.admin.client.config.CloudFoundryApplicationProperties; import de.codecentric.boot.admin.client.config.InstanceProperties; diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/DefaultApplicationFactoryTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/DefaultApplicationFactoryTest.java index 512ac881273..bc698ac4ca7 100644 --- a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/DefaultApplicationFactoryTest.java +++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/DefaultApplicationFactoryTest.java @@ -25,11 +25,11 @@ import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.web.context.WebServerApplicationContext; -import org.springframework.boot.web.context.WebServerInitializedEvent; import org.springframework.boot.web.server.Ssl; import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.server.autoconfigure.ServerProperties; +import org.springframework.boot.web.server.context.WebServerApplicationContext; +import org.springframework.boot.web.server.context.WebServerInitializedEvent; import de.codecentric.boot.admin.client.config.InstanceProperties; diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ReactiveApplicationFactoryTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ReactiveApplicationFactoryTest.java index d27d768eb3c..195d82d0f77 100644 --- a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ReactiveApplicationFactoryTest.java +++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ReactiveApplicationFactoryTest.java @@ -25,11 +25,11 @@ import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties; -import org.springframework.boot.web.context.WebServerApplicationContext; -import org.springframework.boot.web.context.WebServerInitializedEvent; import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.server.autoconfigure.ServerProperties; +import org.springframework.boot.web.server.context.WebServerApplicationContext; +import org.springframework.boot.web.server.context.WebServerInitializedEvent; +import org.springframework.boot.webflux.autoconfigure.WebFluxProperties; import de.codecentric.boot.admin.client.config.InstanceProperties; diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ReactiveRegistrationClientTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ReactiveRegistrationClientTest.java deleted file mode 100644 index 15fbd54bc07..00000000000 --- a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ReactiveRegistrationClientTest.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2014-2023 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. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package de.codecentric.boot.admin.client.registration; - -import java.time.Duration; - -import org.junit.jupiter.api.BeforeEach; -import org.springframework.web.reactive.function.client.WebClient; - -class ReactiveRegistrationClientTest extends AbstractRegistrationClientTest { - - @BeforeEach - void setUp() { - super.setUp(new ReactiveRegistrationClient(WebClient.create(), Duration.ofSeconds(10))); - } - -} diff --git a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ServletApplicationFactoryTest.java b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ServletApplicationFactoryTest.java index 24351d67aef..0522ca48fcc 100644 --- a/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ServletApplicationFactoryTest.java +++ b/spring-boot-admin-client/src/test/java/de/codecentric/boot/admin/client/registration/ServletApplicationFactoryTest.java @@ -26,11 +26,11 @@ import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties; import org.springframework.boot.actuate.endpoint.EndpointId; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath; -import org.springframework.boot.web.context.WebServerApplicationContext; -import org.springframework.boot.web.context.WebServerInitializedEvent; import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.server.autoconfigure.ServerProperties; +import org.springframework.boot.web.server.context.WebServerApplicationContext; +import org.springframework.boot.web.server.context.WebServerInitializedEvent; +import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath; import org.springframework.mock.web.MockServletContext; import de.codecentric.boot.admin.client.config.InstanceProperties; diff --git a/spring-boot-admin-docs/src/site/docs/01-getting-started/10-server-setup.md b/spring-boot-admin-docs/src/site/docs/01-getting-started/10-server-setup.md new file mode 100644 index 00000000000..60cc7e6a3ca --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/01-getting-started/10-server-setup.md @@ -0,0 +1,196 @@ +--- +sidebar_position: 10 +sidebar_custom_props: + icon: 'server' +--- + +# Server Setup + +Setting up a Spring Boot Admin Server is straightforward and requires only a few steps. The server acts as the central +monitoring hub for all your Spring Boot applications. + +## Creating the Admin Server + +### Step 1: Create a Spring Boot Project + +Use [Spring Initializr](https://start.spring.io) to create a new Spring Boot project, or add the dependencies to an +existing project. + +### Step 2: Add Maven Dependencies + +Add the Spring Boot Admin Server starter and a web starter to your `pom.xml`: + +```xml title="pom.xml" + + + + de.codecentric + spring-boot-admin-starter-server + @VERSION@ + + + org.springframework.boot + spring-boot-starter-webmvc + + +``` + +For Gradle: + +```groovy title="build.gradle" +dependencies { + implementation 'de.codecentric:spring-boot-admin-starter-server:@VERSION@' + implementation 'org.springframework.boot:spring-boot-starter-webmvc' +} +``` + +:::tip +You can choose either Servlet (WebMVC) or Reactive (WebFlux) as your web stack. For reactive applications, use +`spring-boot-starter-webflux` instead. +::: + +### Step 3: Enable Admin Server + +Annotate your main application class with `@EnableAdminServer`: + +```java title="SpringBootAdminApplication.java" +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import de.codecentric.boot.admin.server.config.EnableAdminServer; + +@SpringBootApplication +@EnableAdminServer +public class SpringBootAdminApplication { + public static void main(String[] args) { + SpringApplication.run(SpringBootAdminApplication.class, args); + } +} +``` + +The `@EnableAdminServer` annotation enables Spring Boot Admin Server by loading all required configuration through +Spring's auto-discovery feature. + +### Step 4: Configure Application Properties + +Create or update your `application.yml` or `application.properties`: + +```yaml title="application.yml" +spring: + application: + name: spring-boot-admin-server + +server: + port: 8080 + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS +``` + +### Step 5: Run the Server + +Start your application and navigate to `http://localhost:8080` to access the Spring Boot Admin UI. + +## Server Configuration Options + +### Custom Context Path + +If you want to run the Admin Server under a different context path: + +```yaml title="application.yml" +spring: + boot: + admin: + context-path: /admin # UI will be available at http://localhost:8080/admin +``` + +### Customizing the Server Port + +```yaml title="application.yml" +server: + port: 9090 # Run on a different port +``` + +## Servlet vs. Reactive + +Spring Boot Admin Server can run on either a Servlet or Reactive stack: + +### Servlet (Default) + +```xml + + + org.springframework.boot + spring-boot-starter-webmvc + +``` + +Best for traditional servlet-based applications and when you need features like Jolokia (JMX support). + +### Reactive (WebFlux) + +```xml + + + org.springframework.boot + spring-boot-starter-webflux + +``` + +Best for fully reactive applications and high-concurrency scenarios. + +## Deployment Options + +### Standalone JAR + +Build and run as a standalone application: + +```bash +mvn clean package +java -jar target/spring-boot-admin-server.jar +``` + +### WAR Deployment + +For deployment to an external servlet container, see +the [spring-boot-admin-sample-war](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-war/) +example. + +### Docker + +Create a `Dockerfile`: + +```dockerfile +FROM eclipse-temurin:17-jre +COPY target/spring-boot-admin-server.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "/app.jar"] +``` + +Build and run: + +```bash +docker build -t spring-boot-admin-server . +docker run -p 8080:8080 spring-boot-admin-server +``` + +## Next Steps + +Now that your server is running, you need to register your applications: + +- [Client Registration](./20-client-registration.md) - Learn how to register applications with the server +- [Server Configuration](../02-server/01-server.mdx) - Explore advanced server configuration options +- [Security](../02-server/02-security.md) - Secure your Admin Server + +## Example Projects + +- [spring-boot-admin-sample-servlet](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-servlet/) - + Complete servlet-based example with security +- [spring-boot-admin-sample-reactive](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-reactive/) - + Reactive (WebFlux) example diff --git a/spring-boot-admin-docs/src/site/docs/01-getting-started/20-client-registration.md b/spring-boot-admin-docs/src/site/docs/01-getting-started/20-client-registration.md new file mode 100644 index 00000000000..9c5af408a16 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/01-getting-started/20-client-registration.md @@ -0,0 +1,311 @@ +--- +sidebar_position: 20 +sidebar_custom_props: + icon: 'link' +--- + +# Client Registration + +To monitor your applications with Spring Boot Admin, they need to register with the Admin Server. There are three main +approaches to achieve this: + +1. **Spring Boot Admin Client** - Direct registration +2. **Spring Cloud Discovery** - Automatic registration via service discovery +3. **Static Configuration** - Manual configuration on the server side + +## Using Spring Boot Admin Client + +The Spring Boot Admin Client library enables applications to register themselves directly with the Admin Server. + +### Step 1: Add Dependencies + +Add the Spring Boot Admin Client starter to your application: + +```xml title="pom.xml" + + de.codecentric + spring-boot-admin-starter-client + @VERSION@ + +``` + +For Gradle: + +```groovy title="build.gradle" +implementation 'de.codecentric:spring-boot-admin-starter-client:@VERSION@' +``` + +### Step 2: Configure the Admin Server URL + +Add the Admin Server URL to your `application.properties` or `application.yml`: + +```yaml title="application.yml" +spring: + boot: + admin: + client: + url: http://localhost:8080 # URL of your Admin Server + +management: + endpoints: + web: + exposure: + include: "*" # Expose all actuator endpoints + endpoint: + health: + show-details: ALWAYS + info: + env: + enabled: true # Enable the info endpoint +``` + +```properties title="application.properties" +spring.boot.admin.client.url=http://localhost:8080 +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=ALWAYS +management.info.env.enabled=true +``` + +### Step 3: Start Your Application + +When your application starts, it will automatically register with the Admin Server. You'll see your application appear +in the Admin Server's web interface. + +### Client Configuration Options + +#### Custom Instance Metadata + +Add custom metadata to your application registration: + +```yaml title="application.yml" +spring: + boot: + admin: + client: + instance: + metadata: + tags: + environment: production + region: us-east-1 + team: platform +``` + +#### Custom Service URL + +Override the service URL that the Admin Server uses to connect: + +```yaml title="application.yml" +spring: + boot: + admin: + client: + instance: + service-url: https://my-app.example.com + service-host-type: IP # or CANONICAL +``` + +#### Registration Interval + +Configure how often the client registers with the server: + +```yaml title="application.yml" +spring: + boot: + admin: + client: + period: 10000 # milliseconds (default: 10000) + auto-registration: true # Enable/disable auto-registration +``` + +## Using Spring Cloud Discovery + +If you're using Spring Cloud Discovery (Eureka, Consul, Zookeeper), you don't need the Spring Boot Admin Client. The +Admin Server can discover applications automatically. + +### Eureka Example + +#### Step 1: Add Eureka Client Dependency + +Add to your application: + +```xml title="pom.xml" + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +#### Step 2: Configure Eureka + +```yaml title="application.yml" +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + instance: + leaseRenewalIntervalInSeconds: 10 + health-check-url-path: /actuator/health + metadata-map: + startup: ${random.int} # Trigger info update after restart + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS +``` + +#### Step 3: Enable Discovery on Admin Server + +Add Eureka client to your Admin Server: + +```xml title="pom.xml" + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +Enable discovery in the Admin Server: + +```java title="SpringBootAdminApplication.java" +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@EnableDiscoveryClient +@EnableAdminServer +@SpringBootApplication +public class SpringBootAdminApplication { + static void main(String[] args) { + SpringApplication.run(SpringBootAdminApplication.class, args); + } +} +``` + +### Consul Example + +```yaml title="application.yml" +spring: + cloud: + consul: + discovery: + metadata: + user-name: ${spring.security.user.name} + user-password: ${spring.security.user.password} +``` + +:::warning +Consul does not allow dots (".") in metadata keys. Use dashes instead (e.g., `user-name` instead of `user.name`). +::: + +### Zookeeper Example + +For Zookeeper integration, see +the [spring-boot-admin-sample-zookeeper](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/) +example. + +## Static Configuration + +You can configure applications statically on the Admin Server using Spring Cloud's `SimpleDiscoveryClient`. + +```yaml title="application.yml (Admin Server)" +spring: + cloud: + discovery: + client: + simple: + instances: + my-application: + - uri: http://localhost:8081 + metadata: + management.context-path: /actuator +``` + +This approach is useful for: + +- Legacy applications that can't be modified +- Applications running in environments without service discovery +- Static infrastructure setups + +## Securing Client Registration + +When your Admin Server is secured, clients need credentials to register: + +```yaml title="application.yml (Client)" +spring: + boot: + admin: + client: + url: http://localhost:8080 + username: admin + password: secret +``` + +For more details, see [Security](../02-server/02-security.md). + +## Exposing Actuator Endpoints + +Spring Boot Admin requires access to actuator endpoints. Ensure they are properly exposed: + +```yaml title="application.yml" +management: + endpoints: + web: + exposure: + include: "*" # Expose all endpoints + # Or be more specific: + # include: health,info,metrics,env,loggers +``` + +:::warning +In production, carefully consider which endpoints to expose and implement proper security measures. +::: + +## Verifying Registration + +After configuring your client: + +1. Start your Admin Server +2. Start your client application +3. Navigate to the Admin Server UI (`http://localhost:8080`) +4. Your application should appear in the applications list + +Check the logs for registration confirmation: + +``` +INFO: Application registered itself as +``` + +## Troubleshooting + +### Application Not Appearing + +- Verify the Admin Server URL is correct +- Check network connectivity between client and server +- Ensure actuator endpoints are exposed +- Review client logs for registration errors +- Verify security credentials if server is secured + +### Registration Keeps Failing + +- Check if the Admin Server is running +- Verify firewall rules allow communication +- Ensure the management port is accessible +- Check for proxy or network configuration issues + +## Next Steps + +- [Client Features](../03-client/10-client-features.md) - Learn about version display, JMX, logs, and tags +- [Client Configuration](../03-client/80-configuration.md) - Explore advanced client configuration +- [Service Discovery](../03-client/40-service-discovery.md) - Deep dive into Spring Cloud integration + +## Example Projects + +- [spring-boot-admin-sample-servlet](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-servlet/) - + Direct client registration with security +- [spring-boot-admin-sample-eureka](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-eureka/) - + Eureka discovery example +- [spring-boot-admin-sample-consul](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-consul/) - + Consul discovery example diff --git a/spring-boot-admin-docs/src/site/docs/snapshots.md b/spring-boot-admin-docs/src/site/docs/01-getting-started/50-snapshots.md similarity index 52% rename from spring-boot-admin-docs/src/site/docs/snapshots.md rename to spring-boot-admin-docs/src/site/docs/01-getting-started/50-snapshots.md index 8656679ab23..ef7261fa1e2 100644 --- a/spring-boot-admin-docs/src/site/docs/snapshots.md +++ b/spring-boot-admin-docs/src/site/docs/01-getting-started/50-snapshots.md @@ -1,33 +1,42 @@ -# Use SNAPSHOT-Versions +--- +sidebar_custom_props: + icon: 'package' +--- -If you want to use a snapshot version of Spring Boot Admin Server you most likely need to include the spring and sonatype snapshot repositories: +# SNAPSHOT-Versions + +If you want to use a snapshot version of Spring Boot Admin Server you most likely need to include the spring and +sonatype snapshot repositories: ```xml title="pom.xml" + - spring-milestone + spring-boot-admin-snapshot + Spring Boot Admin Snapshots + https://maven.pkg.github.com/codecentric/spring-boot-admin - false + true - http://repo.spring.io/milestone + + false + + + - spring-snapshot + spring-milestone - true + false - http://repo.spring.io/snapshot + https://repo.spring.io/milestone - sonatype-nexus-snapshots - Sonatype Nexus Snapshots - https://oss.sonatype.org/content/repositories/snapshots/ + spring-snapshot true - - false - + http:s//repo.spring.io/snapshot ``` diff --git a/spring-boot-admin-docs/src/site/docs/01-getting-started/_category_.json b/spring-boot-admin-docs/src/site/docs/01-getting-started/_category_.json new file mode 100644 index 00000000000..3562d433d76 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/01-getting-started/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Getting Started", + "position": 1 +} diff --git a/spring-boot-admin-docs/src/site/docs/01-getting-started/index.md b/spring-boot-admin-docs/src/site/docs/01-getting-started/index.md new file mode 100644 index 00000000000..695b5b08d23 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/01-getting-started/index.md @@ -0,0 +1,63 @@ +--- +sidebar_position: 2 +sidebar_custom_props: + icon: 'rocket' +--- + +# Getting Started + +Spring Boot Admin follows a server-client architecture designed to provide centralized monitoring and management of +Spring Boot applications. This guide will help you quickly set up both the server and client components. + +## Architecture Overview + +Spring Boot Admin consists of two main components: + +- **Server**: A centralized monitoring hub that provides a web-based UI and aggregates data from multiple applications +- **Client**: Applications that register themselves with the server and expose management endpoints + +The server continuously polls the clients' Actuator endpoints to collect health status, metrics, and other management +information, making this data available through an intuitive dashboard. + +## Quick Start + +The fastest way to get started with Spring Boot Admin: + +1. **Set up the Admin Server** - Create a Spring Boot application with `@EnableAdminServer` +2. **Register your applications** - Add the Admin Client to your applications or use Spring Cloud Discovery +3. **Access the dashboard** - Navigate to your server URL to view and manage your applications + +## Prerequisites + +- Java 17 or higher +- Spring Boot 3.0 or higher +- Maven or Gradle build tool + +:::note +Spring Boot Admin 3.x requires Spring Boot 3.x. For Spring Boot 2.x applications, use Spring Boot Admin 2.x. +::: + +## What's Next? + +- [Server Setup](./10-server-setup.md) - Learn how to configure the Admin Server +- [Client Registration](./20-client-registration.md) - Discover different ways to register your applications + +## Motivation + +In modern microservices architecture, monitoring and managing distributed systems is complex and challenging. Spring +Boot Admin provides a powerful solution for visualizing, monitoring, and managing Spring Boot applications in real-time. + +By offering a web interface that aggregates the health and metrics of all attached services, Spring Boot Admin +simplifies the process of ensuring system stability and performance. Whether you need insights into application health, +memory usage, or log output, Spring Boot Admin offers a centralized tool that streamlines operational management. + +:::info +While Spring Boot Admin offers a user-friendly and centralized interface for monitoring Spring Boot applications, it is +not designed to replace sophisticated, full-scale monitoring and observability tools like Grafana, Datadog, or Instana. +These tools provide advanced capabilities such as real-time alerting, history data, complex metric analysis, distributed +tracing, and customizable dashboards across diverse environments. + +Spring Boot Admin excels at providing a lightweight, application-centric view with essential health checks, metrics, and +management endpoints. For production-grade observability in larger, more complex systems, integrating Spring Boot Admin +alongside these advanced platforms ensures comprehensive system monitoring and deep insights. +::: diff --git a/spring-boot-admin-docs/src/site/docs/01-index.md b/spring-boot-admin-docs/src/site/docs/01-index.md deleted file mode 100644 index 9c8f89c4284..00000000000 --- a/spring-boot-admin-docs/src/site/docs/01-index.md +++ /dev/null @@ -1,19 +0,0 @@ -import {Screenshot} from "../src/components/Screenshot"; - -# Overview - - - diff --git a/spring-boot-admin-docs/src/site/docs/server/01-server.mdx b/spring-boot-admin-docs/src/site/docs/02-server/01-server.mdx similarity index 74% rename from spring-boot-admin-docs/src/site/docs/server/01-server.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/01-server.mdx index 40321620942..4988620ba91 100644 --- a/spring-boot-admin-docs/src/site/docs/server/01-server.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/01-server.mdx @@ -2,11 +2,11 @@ sidebar_custom_props: icon: 'server' --- -import metadataServer from "../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import metadataServerCloud from "../../../../../spring-boot-admin-server-cloud/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../src/components/PropertyTable"; +import metadataServer from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import metadataServerCloud from "@sba/spring-boot-admin-server-cloud/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; -# Set up the Server +# Set up server ## Running Behind a Front-end Proxy Server @@ -45,7 +45,7 @@ spring: ### Other DiscoveryClients -Spring Boot Admin supports all other implementations of Spring Cloud’s `DiscoveryClient` ([Eureka](https://docs.spring.io/spring-cloud-netflix/docs/current/reference/html/#service-discovery-eureka-clients/), [Zookeeper](https://docs.spring.io/spring-cloud-zookeeper/docs/current/reference/html/#spring-cloud-zookeeper-discovery), [Consul](https://docs.spring.io/spring-cloud-consul/docs/current/reference/html/#spring-cloud-consul-discovery), [Kubernetes](https://docs.spring.io/spring-cloud-kubernetes/docs/current/reference/html/#discoveryclient-for-kubernetes), …​). You need to add it to the Spring Boot Admin Server and configure it properly. An [example setup using Eureka](../installation-and-setup/index.md#using-spring-cloud-discovery) is shown above. +Spring Boot Admin supports all other implementations of Spring Cloud's `DiscoveryClient` ([Eureka](https://docs.spring.io/spring-cloud-netflix/docs/current/reference/html/#service-discovery-eureka-clients/), [Zookeeper](https://docs.spring.io/spring-cloud-zookeeper/docs/current/reference/html/#spring-cloud-zookeeper-discovery), [Consul](https://docs.spring.io/spring-cloud-consul/docs/current/reference/html/#spring-cloud-consul-discovery), [Kubernetes](https://docs.spring.io/spring-cloud-kubernetes/docs/current/reference/html/#discoveryclient-for-kubernetes), …​). You need to add it to the Spring Boot Admin Server and configure it properly. See the [integration guides](../04-integration/) for detailed setup instructions. ### Converting ServiceInstances diff --git a/spring-boot-admin-docs/src/site/docs/02-server/02-security.md b/spring-boot-admin-docs/src/site/docs/02-server/02-security.md new file mode 100644 index 00000000000..db622c0617e --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/02-server/02-security.md @@ -0,0 +1,217 @@ +--- +sidebar_custom_props: + icon: 'shield' +--- + +# Foster Security + +Since there are several approaches on solving authentication and authorization in distributed web applications Spring +Boot Admin doesn't ship a default one. By default `spring-boot-admin-server-ui` provides a login page and a logout +button. + +A Spring Security configuration for your server could look like this: + +```java title="SecuritySecureConfig.java" + +@Configuration(proxyBeanMethods = false) +public class SecuritySecureConfig { + + private final AdminServerProperties adminServer; + + private final SecurityProperties security; + + public SecuritySecureConfig(AdminServerProperties adminServer, SecurityProperties security) { + this.adminServer = adminServer; + this.security = security; + } + + @Bean + protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); + successHandler.setTargetUrlParameter("redirectTo"); + successHandler.setDefaultTargetUrl(this.adminServer.path("/")); + + http.authorizeHttpRequests((authorizeRequests) -> authorizeRequests // + .requestMatchers(new AntPathRequestMatcher(this.adminServer.path("/assets/**"))) + .permitAll() // (1) + .requestMatchers(new AntPathRequestMatcher(this.adminServer.path("/actuator/info"))) + .permitAll() + .requestMatchers(new AntPathRequestMatcher(adminServer.path("/actuator/health"))) + .permitAll() + .requestMatchers(new AntPathRequestMatcher(this.adminServer.path("/login"))) + .permitAll() + .dispatcherTypeMatchers(DispatcherType.ASYNC) + .permitAll() // https://github.com/spring-projects/spring-security/issues/11027 + .anyRequest() + .authenticated()) // (2) + .formLogin( + (formLogin) -> formLogin.loginPage(this.adminServer.path("/login")).successHandler(successHandler)) // (3) + .logout((logout) -> logout.logoutUrl(this.adminServer.path("/logout"))) + .httpBasic(Customizer.withDefaults()); // (4) + + http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class) // (5) + .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) + .ignoringRequestMatchers( + new AntPathRequestMatcher(this.adminServer.path("/instances"), POST.toString()), // (6) + new AntPathRequestMatcher(this.adminServer.path("/instances/*"), DELETE.toString()), // (6) + new AntPathRequestMatcher(this.adminServer.path("/actuator/**")) // (7) + )); + + http.rememberMe((rememberMe) -> rememberMe.key(UUID.randomUUID().toString()).tokenValiditySeconds(1209600)); + + return http.build(); + + } + + // Required to provide UserDetailsService for "remember functionality" + @Bean + public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) { + UserDetails user = User.withUsername("user").password(passwordEncoder.encode("password")).roles("USER").build(); + return new InMemoryUserDetailsManager(user); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} +``` + +1. Grants public access to all static assets and the login page. +2. Every other request must be authenticated. +3. Configures login and logout. +4. Enables HTTP-Basic support. This is needed for the Spring Boot Admin Client to register. +5. Enables CSRF-Protection using Cookies +6. Disables CSRF-Protection for the endpoint the Spring Boot Admin Client uses to (de-)register. +7. Disables CSRF-Protection for the actuator endpoints. + +In case you use the Spring Boot Admin Client, it needs the credentials for accessing the server: + +```yaml title="application.yml" +spring.boot.admin.client: + username: sba-client + password: s3cret +``` + +For a complete sample look +at [spring-boot-admin-sample-servlet](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-servlet/). + +:::note +If you protect the /instances endpoint don't forget to configure the username and password on your SBA-Client using +spring.boot.admin.client.username and spring.boot.admin.client.password. +::: + +## Securing Client Actuator Endpoints + +When the actuator endpoints are secured using HTTP Basic authentication the SBA Server needs credentials to access them. +You can submit the credentials in the metadata when registering the application. The `BasicAuthHttpHeaderProvider` then +uses this metadata to add the `Authorization` header to access your application's actuator endpoints. You can provide +your own `HttpHeadersProvider` to alter the behaviour (e.g. add some decryption) or add extra headers. + +:::note +The SBA Server masks certain metadata in the HTTP interface to prevent leaking of sensitive information. +::: + +:::warning +You should configure HTTPS for your SBA Server or (service registry) when transferring credentials via the metadata. +::: + +:::warning +When using Spring Cloud Discovery, you must be aware that anybody who can query your service registry can obtain the +credentials. +::: + +:::tip +When using this approach the SBA Server decides whether the user can access the registered applications. There are more +complex solutions possible (using OAuth2) to let the clients decide if the user can access the endpoints. For that +please have a look at the samples +in [joshiste/spring-boot-admin-samples](https://github.com/joshiste/spring-boot-admin-samples). +::: + +### SBA Client + +```yaml title="application.yml" +spring.boot.admin.client: + url: http://localhost:8080 + instance: + metadata: + user.name: ${spring.security.user.name} + user.password: ${spring.security.user.password} +``` + +### SBA Server + +You can specify credentials via configuration properties in your admin server. + +:::tip +You can use this in conjunction +with [spring-cloud-kubernetes](https://cloud.spring.io/spring-cloud-kubernetes/1.1.x/reference/html/#secrets-propertysource) +to pull credentials from [secrets](https://kubernetes.io/docs/concepts/configuration/secret/). +::: + +To enable pulling credentials from properties the `spring.boot.admin.instance-auth.enabled` property must be `true` ( +default). + +:::note +If your clients provide credentials via metadata (i.e., via service annotations), that metadata will be used instead of +the properties. +::: + +You can provide a default username and password by setting `spring.boot.admin.instance-auth.default-user-name` and +`spring.boot.admin.instance-auth.default-user-password`. Optionally you can provide credentials for specific services ( +by name) using the `spring.boot.admin.instance-auth.service-map.*.user-name` pattern, replacing `*` with the service +name. + +```yaml title="application.yml" +spring.boot.admin: + instance-auth: + enabled: true + default-user-name: "${some.user.name.from.secret}" + default-password: "${some.user.password.from.secret}" + service-map: + my-first-service-to-monitor: + user-name: "${some.user.name.from.secret}" + user-password: "${some.user.password.from.secret}" + my-second-service-to-monitor: + user-name: "${some.user.name.from.secret}" + user-password: "${some.user.password.from.secret}" +``` + +### Eureka + +```yaml title="application.yml" +eureka: + instance: + metadata-map: + user.name: ${spring.security.user.name} + user.password: ${spring.security.user.password} +``` + +### Consul + +```yaml title="application.yml" +spring.cloud.consul: + discovery: + metadata: + user-name: ${spring.security.user.name} + user-password: ${spring.security.user.password} +``` + +:::warning +Consul does not allow dots (".") in metadata keys, use dashes instead. +::: + +## CSRF on Actuator Endpoints + +Some of the actuator endpoints (e.g. `/loggers`) support POST requests. When using Spring Security you need to ignore +the actuator endpoints for CSRF-Protection as the Spring Boot Admin Server currently lacks support. + +```java title="SecuritySecureConfig.java" + +@Bean +private SecurityFilterChain filterChain(HttpSecurity http) { + return http.csrf(c -> c.ignoringRequestMatchers("/actuator/**")).build(); +} +``` diff --git a/spring-boot-admin-docs/src/site/docs/server/10-Events.mdx b/spring-boot-admin-docs/src/site/docs/02-server/10-Events.mdx similarity index 100% rename from spring-boot-admin-docs/src/site/docs/server/10-Events.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/10-Events.mdx diff --git a/spring-boot-admin-docs/src/site/docs/server/20-Clustering.mdx b/spring-boot-admin-docs/src/site/docs/02-server/20-Clustering.mdx similarity index 92% rename from spring-boot-admin-docs/src/site/docs/server/20-Clustering.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/20-Clustering.mdx index 9ea034621ae..21622c6f0b4 100644 --- a/spring-boot-admin-docs/src/site/docs/server/20-Clustering.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/20-Clustering.mdx @@ -3,8 +3,8 @@ sidebar_custom_props: icon: 'server' --- import metadataServer - from "../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../src/components/PropertyTable"; + from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; # Clustering @@ -78,10 +78,3 @@ public Config hazelcastConfig() { return config; } ``` - - diff --git a/spring-boot-admin-docs/src/site/docs/02-server/30-persistence.md b/spring-boot-admin-docs/src/site/docs/02-server/30-persistence.md new file mode 100644 index 00000000000..956ae1e788d --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/02-server/30-persistence.md @@ -0,0 +1,274 @@ +--- +sidebar_position: 30 +sidebar_custom_props: + icon: 'database' +--- + +# Persistence and Event Store + +Spring Boot Admin uses an event-sourced architecture to track the state of registered applications. All changes to +application instances are stored as events in an `InstanceEventStore`, allowing the server to rebuild application state +and maintain a complete audit trail. + +## Event Store Architecture + +The `InstanceEventStore` is responsible for storing all instance-related events. Spring Boot Admin provides two built-in +implementations: + +### InMemoryEventStore + +The default implementation stores events in memory using a `ConcurrentHashMap`. This is suitable for single-instance +deployments and development environments. + +**Characteristics:** + +- Fast and lightweight +- Non-persistent (data lost on restart) +- Limited by available memory +- Configurable maximum log size per instance + +**Configuration:** + +```java +@Bean +public InstanceEventStore eventStore() { + return new InMemoryEventStore(100); // Max 100 events per instance +} +``` + +The default configuration creates an `InMemoryEventStore` with a maximum of 100 events per instance aggregate. Older +events are automatically removed when the limit is reached. + +### HazelcastEventStore + +For clustered deployments, the `HazelcastEventStore` provides distributed persistence using Hazelcast's `IMap`. + +**Characteristics:** + +- Distributed across cluster nodes +- Survives single-node failures +- Automatic synchronization between nodes +- Real-time event publishing across the cluster + +**Configuration:** + +First, add the Hazelcast dependency: + +```xml title="pom.xml" + + com.hazelcast + hazelcast + +``` + +Then configure the Hazelcast-backed event store: + +```java +import com.hazelcast.config.Config; +import com.hazelcast.config.MapConfig; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.map.IMap; +import de.codecentric.boot.admin.server.eventstore.HazelcastEventStore; + +@Configuration +public class HazelcastConfig { + + @Bean + public Config hazelcastConfig() { + MapConfig mapConfig = new MapConfig("spring-boot-admin-event-store") + .setBackupCount(1) + .setMergePolicyConfig(new MergePolicyConfig( + PutIfAbsentMergePolicy.class.getName(), 100)); + + Config config = new Config(); + config.addMapConfig(mapConfig); + return config; + } + + @Bean + public HazelcastInstance hazelcastInstance(Config hazelcastConfig) { + return Hazelcast.newHazelcastInstance(hazelcastConfig); + } + + @Bean + public InstanceEventStore eventStore(HazelcastInstance hazelcastInstance) { + IMap> map = + hazelcastInstance.getMap("spring-boot-admin-event-store"); + return new HazelcastEventStore(100, map); + } +} +``` + +**How it works:** + +The `HazelcastEventStore` listens to map entry updates and publishes new events to all cluster nodes: + +```java +eventLog.addEntryListener(new EntryAdapter>() { + @Override + public void entryUpdated(EntryEvent> event) { + long lastKnownVersion = getLastVersion(event.getOldValue()); + List newEvents = event.getValue() + .stream() + .filter((e) -> e.getVersion() > lastKnownVersion) + .toList(); + publish(newEvents); + } +}, true); +``` + +## Event Types + +The event store manages different types of instance events: + +- `InstanceRegisteredEvent` - Application registers with the server +- `InstanceDeregisteredEvent` - Application unregisters or is removed +- `InstanceStatusChangedEvent` - Health status changes +- `InstanceEndpointsDetectedEvent` - Actuator endpoints discovered +- `InstanceInfoChangedEvent` - Application info updated +- `InstanceRegistrationUpdatedEvent` - Registration details changed + +Each event contains: + +- Instance ID +- Timestamp +- Version (for optimistic locking) +- Event-specific data + +## InstanceEventStore Interface + +```java +public interface InstanceEventStore extends Publisher { + + Flux findAll(); + + Flux find(InstanceId id); + + Mono append(List events); +} +``` + +### Methods + +- **`findAll()`** - Returns all events for all instances +- **`find(InstanceId id)`** - Returns events for a specific instance +- **`append(List events)`** - Appends new events to the store + +The store also implements `Publisher`, allowing components to subscribe to new events in real-time. + +## Event Versioning and Optimistic Locking + +Events are versioned to prevent concurrent modification issues. Each event includes a version number that increments +with each change: + +```java +public abstract class InstanceEvent implements Serializable { + private final InstanceId instance; + private final long version; + private final long timestamp; + + // ... +} +``` + +When appending events, the event store checks that the version matches the expected sequence, throwing an +`OptimisticLockingException` if there's a conflict. + +## Event Publishing + +The event store publishes events to subscribers, enabling reactive processing: + +```java +eventStore.subscribe(event -> { + if (event instanceof InstanceStatusChangedEvent statusEvent) { + // React to status changes + System.out.println("Instance " + event.getInstance() + + " changed to " + statusEvent.getStatusInfo().getStatus()); + } +}); +``` + +## Configuring Event Store Size + +Control the maximum number of events stored per instance: + +```java +@Bean +public InstanceEventStore eventStore() { + return new InMemoryEventStore(500); // Store up to 500 events per instance +} +``` + +When the limit is reached, the oldest events are removed. This prevents unbounded memory growth while maintaining recent +history. + +## Custom Event Store Implementation + +You can implement your own event store for custom persistence requirements (e.g., database, external cache): + +```java +public class CustomEventStore implements InstanceEventStore { + + @Override + public Flux findAll() { + // Load all events from your storage + } + + @Override + public Flux find(InstanceId id) { + // Load events for specific instance + } + + @Override + public Mono append(List events) { + // Persist events and publish to subscribers + } + + @Override + public void subscribe(Subscriber subscriber) { + // Handle event subscriptions + } +} +``` + +Then register your custom implementation as a bean: + +```java +@Bean +public InstanceEventStore eventStore() { + return new CustomEventStore(); +} +``` + +## Best Practices + +1. **For Development**: Use `InMemoryEventStore` for simplicity +2. **For Single Instance Deployments**: Use `InMemoryEventStore` if restart data loss is acceptable +3. **For Clustered Deployments**: Use `HazelcastEventStore` for high availability +4. **For Large Deployments**: Tune the max log size to balance memory usage and history retention +5. **For Custom Requirements**: Implement your own event store with database or distributed cache backing + +## Monitoring Event Store + +Monitor event store health through actuator endpoints or by subscribing to events: + +```java +@Component +public class EventStoreMonitor { + + public EventStoreMonitor(InstanceEventStore eventStore) { + eventStore.subscribe(event -> { + // Log or metric collection + log.debug("Event: {} for instance {}", + event.getType(), event.getInstance()); + }); + } +} +``` + +## See Also + +- [Clustering](./20-Clustering.mdx) - Learn about clustering with Hazelcast +- [Events](./10-Events.mdx) - Understand the event system +- [Instance Registry](./40-instance-registry.md) - How instances are managed diff --git a/spring-boot-admin-docs/src/site/docs/02-server/40-instance-registry.md b/spring-boot-admin-docs/src/site/docs/02-server/40-instance-registry.md new file mode 100644 index 00000000000..e8cce2bd67f --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/02-server/40-instance-registry.md @@ -0,0 +1,369 @@ +--- +sidebar_position: 40 +sidebar_custom_props: + icon: 'apps' +--- + +# Instance Registry + +The Instance Registry is the core component responsible for managing registered applications in Spring Boot Admin. It +uses an event-sourced architecture to track application state through the `InstanceRepository` interface. + +## InstanceRepository + +The `InstanceRepository` is the primary interface for storing and retrieving application instances. It provides reactive +methods for managing instance lifecycle: + +```java +public interface InstanceRepository { + + Mono save(Instance app); + + Flux findAll(); + + Mono find(InstanceId id); + + Flux findByName(String name); + + Mono compute(InstanceId id, + BiFunction> remappingFunction); + + Mono computeIfPresent(InstanceId id, + BiFunction> remappingFunction); +} +``` + +## Event-Sourced Implementation + +Spring Boot Admin uses `EventsourcingInstanceRepository`, which rebuilds instance state from events stored in the +`InstanceEventStore`. + +### How It Works + +Instead of directly storing instance state, the repository stores events that represent state changes: + +1. **Registration**: When an application registers, an `InstanceRegisteredEvent` is created +2. **State Changes**: Each state change (health, info, endpoints) generates a new event +3. **Reconstruction**: The current instance state is rebuilt by replaying all events + +```java +public class EventsourcingInstanceRepository implements InstanceRepository { + + private final InstanceEventStore eventStore; + + @Override + public Mono save(Instance instance) { + return eventStore.append(instance.getUnsavedEvents()) + .then(Mono.just(instance.clearUnsavedEvents())); + } + + @Override + public Mono find(InstanceId id) { + return eventStore.find(id) + .collectList() + .filter(e -> !e.isEmpty()) + .map(events -> Instance.create(id).apply(events)); + } + + @Override + public Flux findAll() { + return eventStore.findAll() + .groupBy(InstanceEvent::getInstance) + .flatMap(f -> f.reduce(Instance.create(f.key()), + Instance::apply)); + } +} +``` + +### Benefits of Event Sourcing + +- **Complete Audit Trail**: Every change is recorded as an event +- **Temporal Queries**: Can reconstruct state at any point in time +- **Event Replay**: Can rebuild state from events after crashes +- **Debugging**: Full history of state changes for troubleshooting + +## Instance Lifecycle + +### 1. Registration + +When an application registers, a new instance is created: + +```java +InstanceId id = idGenerator.generateId(registration); +Instance newInstance = Instance.create(id).register(registration); +repository.save(newInstance); +``` + +This generates an `InstanceRegisteredEvent`. + +### 2. Endpoint Detection + +After registration, the server detects available actuator endpoints: + +```java +instance = instance.withEndpoints(detectedEndpoints); +repository.save(instance); +``` + +This generates an `InstanceEndpointsDetectedEvent`. + +### 3. Status Updates + +The server periodically polls health endpoints: + +```java +instance = instance.withStatusInfo(statusInfo); +repository.save(instance); +``` + +This generates an `InstanceStatusChangedEvent` when status changes. + +### 4. Info Updates + +Application info is periodically refreshed: + +```java +instance = instance.withInfo(info); +repository.save(instance); +``` + +This generates an `InstanceInfoChangedEvent` when info changes. + +### 5. Deregistration + +When an application shuts down or is removed: + +```java +instance = instance.deregister(); +repository.save(instance); +``` + +This generates an `InstanceDeregisteredEvent`. + +## Optimistic Locking + +The repository uses optimistic locking to handle concurrent updates: + +```java +private final Retry retryOptimisticLockException = Retry.max(10) + .doBeforeRetry(s -> log.debug("Retrying after OptimisticLockingException", + s.failure())) + .filter(OptimisticLockingException.class::isInstance); + +@Override +public Mono compute(InstanceId id, + BiFunction> remappingFunction) { + return find(id) + .flatMap(app -> remappingFunction.apply(id, app)) + .switchIfEmpty(Mono.defer(() -> remappingFunction.apply(id, null))) + .flatMap(this::save) + .retryWhen(retryOptimisticLockException); +} +``` + +If two updates conflict (based on event version numbers), the operation is automatically retried up to 10 times. + +## Querying Instances + +### Find All Instances + +```java +Flux instances = repository.findAll(); +instances.subscribe(instance -> { + System.out.println("Instance: " + instance.getRegistration().getName()); +}); +``` + +### Find by Instance ID + +```java +Mono instance = repository.find(instanceId); +instance.subscribe(inst -> { + System.out.println("Found: " + inst.getRegistration().getName()); +}); +``` + +### Find by Application Name + +```java +Flux instances = repository.findByName("my-application"); +instances.subscribe(instance -> { + System.out.println("Instance ID: " + instance.getId()); +}); +``` + +## Compute Operations + +The `compute` methods provide atomic read-modify-write operations: + +### compute() + +Updates an instance or creates it if it doesn't exist: + +```java +repository.compute(instanceId, (id, instance) -> { + if (instance == null) { + // Create new instance + return Mono.just(Instance.create(id).register(registration)); + } else { + // Update existing instance + return Mono.just(instance.withStatusInfo(newStatus)); + } +}).subscribe(); +``` + +### computeIfPresent() + +Updates only if the instance exists: + +```java +repository.computeIfPresent(instanceId, (id, instance) -> { + return Mono.just(instance.withInfo(updatedInfo)); +}).subscribe(); +``` + +## Instance State + +An `Instance` object contains: + +```java +public class Instance { + private final InstanceId id; + private final long version; + private final Registration registration; + private final boolean registered; + private final StatusInfo statusInfo; + private final Info info; + private final Endpoints endpoints; + private final BuildVersion buildVersion; + private final Tags tags; + private final List unsavedEvents; +} +``` + +### Key Properties + +- **`id`**: Unique identifier for the instance +- **`version`**: Event version for optimistic locking +- **`registration`**: Registration details (name, URL, metadata) +- **`registered`**: Whether the instance is currently registered +- **`statusInfo`**: Current health status +- **`info`**: Application info from `/actuator/info` +- **`endpoints`**: Discovered actuator endpoints +- **`buildVersion`**: Application version from build-info +- **`tags`**: Custom tags for classification +- **`unsavedEvents`**: Events pending persistence + +## Instance ID Generation + +Instance IDs are generated by `InstanceIdGenerator` implementations: + +### Default: HashingInstanceUrlIdGenerator + +Generates stable IDs based on the service URL: + +```java +public class HashingInstanceUrlIdGenerator implements InstanceIdGenerator { + @Override + public InstanceId generateId(Registration registration) { + String serviceUrl = registration.getServiceUrl(); + // Generate hash-based ID from URL + return InstanceId.of(hash(serviceUrl)); + } +} +``` + +### Cloud Foundry: CloudFoundryInstanceIdGenerator + +Uses Cloud Foundry's application instance ID: + +```java +public class CloudFoundryInstanceIdGenerator implements InstanceIdGenerator { + @Override + public InstanceId generateId(Registration registration) { + String cfInstanceId = registration.getMetadata() + .get("applicationId") + + ":" + registration.getMetadata().get("instanceId"); + return InstanceId.of(cfInstanceId); + } +} +``` + +### Custom ID Generator + +Implement your own ID generation strategy: + +```java +@Component +public class CustomInstanceIdGenerator implements InstanceIdGenerator { + + @Override + public InstanceId generateId(Registration registration) { + // Custom logic to generate instance ID + String customId = registration.getName() + + "-" + UUID.randomUUID().toString(); + return InstanceId.of(customId); + } +} +``` + +## Working with the Repository + +### Injecting the Repository + +```java +@Component +public class InstanceManager { + + private final InstanceRepository repository; + + public InstanceManager(InstanceRepository repository) { + this.repository = repository; + } + + public Flux getApplicationNames() { + return repository.findAll() + .filter(Instance::isRegistered) + .map(i -> i.getRegistration().getName()) + .distinct(); + } +} +``` + +### Reacting to Changes + +Subscribe to the event store to react to instance changes: + +```java +@Component +public class InstanceChangeListener { + + public InstanceChangeListener(InstanceEventStore eventStore, + InstanceRepository repository) { + eventStore.subscribe(event -> { + if (event instanceof InstanceStatusChangedEvent statusEvent) { + repository.find(event.getInstance()) + .subscribe(instance -> { + log.info("Instance {} status: {}", + instance.getRegistration().getName(), + instance.getStatusInfo().getStatus()); + }); + } + }); + } +} +``` + +## Best Practices + +1. **Use compute methods** for atomic updates to avoid race conditions +2. **Don't modify Instance objects directly** - use the builder-style methods (withXxx) +3. **Let the system retry** optimistic locking failures automatically +4. **Subscribe to events** for reactive processing instead of polling +5. **Use findByName** for multi-instance applications to find all instances of a service + +## See Also + +- [Persistence](./30-persistence.md) - Learn about event storage +- [Events](./10-Events.mdx) - Understand the event system +- [Clustering](./20-Clustering.mdx) - Distributed instance management diff --git a/spring-boot-admin-docs/src/site/docs/02-server/99-server-properties.mdx b/spring-boot-admin-docs/src/site/docs/02-server/99-server-properties.mdx new file mode 100644 index 00000000000..b6ad660d5cb --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/02-server/99-server-properties.mdx @@ -0,0 +1,16 @@ +--- +sidebar_custom_props: + icon: 'properties' +--- +# Properties + +import metadata from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; + + diff --git a/spring-boot-admin-docs/src/site/docs/server/_category_.json b/spring-boot-admin-docs/src/site/docs/02-server/_category_.json similarity index 58% rename from spring-boot-admin-docs/src/site/docs/server/_category_.json rename to spring-boot-admin-docs/src/site/docs/02-server/_category_.json index 4b3e3511724..8ced279bd89 100644 --- a/spring-boot-admin-docs/src/site/docs/server/_category_.json +++ b/spring-boot-admin-docs/src/site/docs/02-server/_category_.json @@ -1,4 +1,4 @@ { - "position": 1, + "position": 2, "label": "Server" } diff --git a/spring-boot-admin-docs/src/site/docs/server/hazelcast-component-diagram.png b/spring-boot-admin-docs/src/site/docs/02-server/hazelcast-component-diagram.png similarity index 100% rename from spring-boot-admin-docs/src/site/docs/server/hazelcast-component-diagram.png rename to spring-boot-admin-docs/src/site/docs/02-server/hazelcast-component-diagram.png diff --git a/spring-boot-admin-docs/src/site/docs/server/index.md b/spring-boot-admin-docs/src/site/docs/02-server/index.md similarity index 96% rename from spring-boot-admin-docs/src/site/docs/server/index.md rename to spring-boot-admin-docs/src/site/docs/02-server/index.md index 027b67bb2f2..d0ff88acbf1 100644 --- a/spring-boot-admin-docs/src/site/docs/server/index.md +++ b/spring-boot-admin-docs/src/site/docs/02-server/index.md @@ -1,3 +1,8 @@ +--- +sidebar_custom_props: + icon: 'server' +--- + import DocCardList from '@theme/DocCardList'; # Spring Boot Admin Server diff --git a/spring-boot-admin-docs/src/site/docs/02-server/notifications/90-custom-notifiers.md b/spring-boot-admin-docs/src/site/docs/02-server/notifications/90-custom-notifiers.md new file mode 100644 index 00000000000..21c84c58e78 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/90-custom-notifiers.md @@ -0,0 +1,422 @@ +--- +sidebar_position: 90 +sidebar_custom_props: + icon: 'bell' +--- + +# Creating Custom Notifiers + +Spring Boot Admin makes it easy to create custom notifiers to integrate with your preferred notification channels. You +can extend the built-in notifier base classes or implement the `Notifier` interface directly. + +## Overview + +Notifiers are Spring beans that implement the `Notifier` interface and react to instance events such as status changes, +registration, or deregistration. + +## Using AbstractEventNotifier + +The recommended approach is to extend `AbstractEventNotifier`, which provides built-in support for: + +- Filtering events +- Enabling/disabling notifications +- Accessing instance details +- Error handling + +### Basic Example + +```java +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.domain.events.InstanceEvent; +import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; +import de.codecentric.boot.admin.server.notify.AbstractEventNotifier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public class CustomNotifier extends AbstractEventNotifier { + + private static final Logger log = LoggerFactory.getLogger(CustomNotifier.class); + + public CustomNotifier(InstanceRepository repository) { + super(repository); + } + + @Override + protected Mono doNotify(InstanceEvent event, Instance instance) { + return Mono.fromRunnable(() -> { + if (event instanceof InstanceStatusChangedEvent statusEvent) { + log.info("Instance {} ({}) is {}", + instance.getRegistration().getName(), + event.getInstance(), + statusEvent.getStatusInfo().getStatus()); + } else { + log.info("Instance {} ({}) {}", + instance.getRegistration().getName(), + event.getInstance(), + event.getType()); + } + }); + } +} +``` + +### Registering the Notifier + +Register your custom notifier as a Spring bean: + +```java +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class NotifierConfiguration { + + @Bean + public CustomNotifier customNotifier(InstanceRepository repository) { + return new CustomNotifier(repository); + } +} +``` + +## Advanced Custom Notifier + +Here's a more advanced example that sends notifications to an external API: + +```java +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +public class WebhookNotifier extends AbstractEventNotifier { + + private static final Logger log = LoggerFactory.getLogger(WebhookNotifier.class); + + private final WebClient webClient; + private final String webhookUrl; + + public WebhookNotifier(InstanceRepository repository, + WebClient.Builder webClientBuilder, + String webhookUrl) { + super(repository); + this.webhookUrl = webhookUrl; + this.webClient = webClientBuilder.build(); + } + + @Override + protected Mono doNotify(InstanceEvent event, Instance instance) { + return Mono.fromSupplier(() -> createNotificationPayload(event, instance)) + .flatMap(this::sendWebhookNotification) + .doOnError(ex -> log.error("Failed to send webhook notification", ex)) + .then(); + } + + private NotificationPayload createNotificationPayload(InstanceEvent event, + Instance instance) { + return NotificationPayload.builder() + .instanceId(instance.getId().getValue()) + .instanceName(instance.getRegistration().getName()) + .eventType(event.getType()) + .status(instance.getStatusInfo().getStatus()) + .timestamp(event.getTimestamp()) + .serviceUrl(instance.getRegistration().getServiceUrl()) + .build(); + } + + private Mono sendWebhookNotification(NotificationPayload payload) { + return webClient.post() + .uri(webhookUrl) + .bodyValue(payload) + .retrieve() + .bodyToMono(Void.class) + .doOnSuccess(v -> log.info("Webhook notification sent successfully")) + .onErrorResume(ex -> { + log.error("Webhook call failed: {}", ex.getMessage()); + return Mono.empty(); + }); + } + + @lombok.Data + @lombok.Builder + private static class NotificationPayload { + private String instanceId; + private String instanceName; + private String eventType; + private String status; + private long timestamp; + private String serviceUrl; + } +} +``` + +### Configuration + +```java + +@Configuration +public class WebhookNotifierConfiguration { + + @Bean + public WebhookNotifier webhookNotifier(InstanceRepository repository, + WebClient.Builder webClientBuilder, + @Value("${webhook.url}") String webhookUrl) { + return new WebhookNotifier(repository, webClientBuilder, webhookUrl); + } +} +``` + +```yaml title="application.yml" +webhook: + url: https://your-webhook-endpoint.com/notifications +``` + +## Filtering Events + +You can override `shouldNotify` to filter which events trigger notifications: + +```java +public class FilteredNotifier extends AbstractEventNotifier { + + public FilteredNotifier(InstanceRepository repository) { + super(repository); + } + + @Override + protected boolean shouldNotify(InstanceEvent event, Instance instance) { + // Only notify for production instances + String environment = instance.getRegistration() + .getMetadata() + .get("environment"); + return "production".equals(environment); + } + + @Override + protected Mono doNotify(InstanceEvent event, Instance instance) { + // Send notification + return Mono.fromRunnable(() -> { + log.info("Production instance event: {}", event.getType()); + }); + } +} +``` + +## Using AbstractStatusChangeNotifier + +If you only care about status changes (UP/DOWN/OFFLINE), extend `AbstractStatusChangeNotifier`: + +```java +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.domain.values.StatusInfo; +import de.codecentric.boot.admin.server.notify.AbstractStatusChangeNotifier; + +import reactor.core.publisher.Mono; + +public class StatusChangeNotifier extends AbstractStatusChangeNotifier { + + public StatusChangeNotifier(InstanceRepository repository) { + super(repository); + } + + @Override + protected Mono doNotify(InstanceEvent event, Instance instance) { + StatusInfo statusInfo = instance.getStatusInfo(); + String status = statusInfo.getStatus(); + + return Mono.fromRunnable(() -> { + if ("DOWN".equals(status)) { + // Send critical alert + log.error("CRITICAL: Instance {} is DOWN!", + instance.getRegistration().getName()); + } else if ("UP".equals(status)) { + // Send recovery notification + log.info("Instance {} is back UP", + instance.getRegistration().getName()); + } + }); + } +} +``` + +## Implementing Notifier Interface Directly + +For full control, implement the `Notifier` interface: + +```java +import de.codecentric.boot.admin.server.notify.Notifier; + +import reactor.core.publisher.Mono; + +public class DirectNotifier implements Notifier { + + private final InstanceRepository repository; + private boolean enabled = true; + + public DirectNotifier(InstanceRepository repository) { + this.repository = repository; + } + + @Override + public Mono notify(InstanceEvent event) { + if (!enabled) { + return Mono.empty(); + } + + return repository.find(event.getInstance()) + .flatMap(instance -> processNotification(event, instance)) + .then(); + } + + private Mono processNotification(InstanceEvent event, Instance instance) { + // Custom notification logic + return Mono.fromRunnable(() -> { + // Send notification + }); + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} +``` + +## Configuration Properties + +Make your notifier configurable through application properties: + +```java +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "spring.boot.admin.notify.custom") +public class CustomNotifierProperties { + private final boolean enabled = true; + private String apiUrl; + private String apiKey; + private final int timeout = 5000; + + // Getters and setters +} +``` + +```java + +@Configuration +@EnableConfigurationProperties(CustomNotifierProperties.class) +public class CustomNotifierConfiguration { + + @Bean + @ConditionalOnProperty(prefix = "spring.boot.admin.notify.custom", + name = "enabled", + havingValue = "true", + matchIfMissing = true) + public CustomNotifier customNotifier(InstanceRepository repository, + CustomNotifierProperties properties) { + CustomNotifier notifier = new CustomNotifier(repository); + notifier.setApiUrl(properties.getApiUrl()); + notifier.setApiKey(properties.getApiKey()); + notifier.setTimeout(properties.getTimeout()); + return notifier; + } +} +``` + +```yaml title="application.yml" +spring: + boot: + admin: + notify: + custom: + enabled: true + api-url: https://api.example.com/notifications + api-key: ${NOTIFICATION_API_KEY} + timeout: 10000 +``` + +## Combining with FilteringNotifier + +Use `FilteringNotifier` to allow runtime control: + +```java + +@Configuration +public class NotifierConfig { + + @Bean + public FilteringNotifier filteringNotifier(InstanceRepository repository, + ObjectProvider> otherNotifiers) { + CompositeNotifier delegate = new CompositeNotifier( + otherNotifiers.getIfAvailable(Collections::emptyList)); + return new FilteringNotifier(delegate, repository); + } + + @Primary + @Bean(initMethod = "start", destroyMethod = "stop") + public RemindingNotifier remindingNotifier(FilteringNotifier filteringNotifier, + InstanceRepository repository) { + RemindingNotifier notifier = new RemindingNotifier( + filteringNotifier, repository); + notifier.setReminderPeriod(Duration.ofMinutes(10)); + notifier.setCheckReminderInverval(Duration.ofSeconds(10)); + return notifier; + } + + @Bean + public CustomNotifier customNotifier(InstanceRepository repository) { + return new CustomNotifier(repository); + } +} +``` + +## Testing Custom Notifiers + +```java +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.test.StepVerifier; + +public class CustomNotifierTest { + + @Test + public void testNotification() { + InstanceRepository repository = Mockito.mock(InstanceRepository.class); + CustomNotifier notifier = new CustomNotifier(repository); + + Instance instance = Instance.create(InstanceId.of("test-instance")) + .register(Registration.create("test-app", "http://localhost:8080") + .build()); + + InstanceEvent event = new InstanceStatusChangedEvent( + instance.getId(), + instance.getVersion(), + StatusInfo.ofUp() + ); + + Mockito.when(repository.find(instance.getId())) + .thenReturn(Mono.just(instance)); + + StepVerifier.create(notifier.notify(event)) + .verifyComplete(); + } +} +``` + +## Best Practices + +1. **Extend AbstractEventNotifier** for most use cases - it provides essential features +2. **Handle errors gracefully** - don't let notification failures affect the server +3. **Use reactive programming** - return `Mono` for async operations +4. **Make it configurable** - use `@ConfigurationProperties` for flexibility +5. **Filter appropriately** - override `shouldNotify` to reduce noise +6. **Log failures** - always log when notifications fail for debugging +7. **Use WebClient** for HTTP calls - it's reactive and efficient +8. **Consider rate limiting** - prevent notification storms +9. **Test thoroughly** - ensure your notifier handles all event types +10. **Document configuration** - provide clear examples for users + +## See Also + +- [Notifications Overview](./index.mdx) - Learn about the notification system +- [Filtering Notifications](./index.mdx#filtering-notifications) - Control which notifications are sent +- [Notification Reminders](./index.mdx#notification-reminder) - Set up reminder notifications +- [Events](../10-Events.mdx) - Understand instance events diff --git a/spring-boot-admin-docs/src/site/docs/02-server/notifications/_category_.json b/spring-boot-admin-docs/src/site/docs/02-server/notifications/_category_.json new file mode 100644 index 00000000000..c8f25e6ba09 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Notifications", + "description": "Configure notifications to alert teams about instance status changes via email, Slack, Teams, and other channels." +} diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/index.mdx b/spring-boot-admin-docs/src/site/docs/02-server/notifications/index.mdx similarity index 95% rename from spring-boot-admin-docs/src/site/docs/server/notifications/index.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/index.mdx index 8b1a62ef42e..00696495a1c 100644 --- a/spring-boot-admin-docs/src/site/docs/server/notifications/index.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/index.mdx @@ -1,12 +1,13 @@ --- sidebar_position: 80 sidebar_custom_props: - icon: 'notifications' + icon: 'bell-ring' --- + # Notifications -import metadata from "../../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../../src/components/PropertyTable"; +import metadata from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; import DocCardList from '@theme/DocCardList'; You can add your own Notifiers by adding Spring Beans which implement the `Notifier` interface, at best by extending`AbstractEventNotifier` or `AbstractStatusChangeNotifier`. @@ -44,7 +45,7 @@ All Notifiers which are using a `RestTemplate` can be configured to use a proxy. title="Notification Proxy configuration options" properties={metadata.properties} filter={['notify.proxy']} - exclusive={false} + includeOnly={true} /> ## Notification Reminder diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/mail-notification.png b/spring-boot-admin-docs/src/site/docs/02-server/notifications/mail-notification.png similarity index 100% rename from spring-boot-admin-docs/src/site/docs/server/notifications/mail-notification.png rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/mail-notification.png diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/notification-filter.png b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notification-filter.png similarity index 100% rename from spring-boot-admin-docs/src/site/docs/server/notifications/notification-filter.png rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/notification-filter.png diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-dingtalk.mdx b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-dingtalk.mdx similarity index 63% rename from spring-boot-admin-docs/src/site/docs/server/notifications/notifier-dingtalk.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-dingtalk.mdx index 41017a8a090..eb96deb3dc7 100644 --- a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-dingtalk.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-dingtalk.mdx @@ -2,8 +2,8 @@ sidebar_custom_props: icon: 'notifications' --- -import metadata from "../../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../../src/components/PropertyTable"; +import metadata from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; # DingTalk Notifications @@ -13,6 +13,6 @@ To enable [DingTalk](https://www.dingtalk.com/) notifications you need to create title="DingTalk notifications configuration options" properties={metadata.properties} filter={['notify.dingtalk']} - exclusive={false} + includeOnly={true} /> diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-discord.mdx b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-discord.mdx similarity index 58% rename from spring-boot-admin-docs/src/site/docs/server/notifications/notifier-discord.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-discord.mdx index 4fdf4f19ac2..0ef8b5b36e2 100644 --- a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-discord.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-discord.mdx @@ -2,8 +2,8 @@ sidebar_custom_props: icon: 'notifications' --- -import metadata from "../../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../../src/components/PropertyTable"; +import metadata from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; # Discord Notifications @@ -13,5 +13,5 @@ To enable Discord notifications you need to create a webhook and set the appropr title="Discord notifications configuration options" properties={metadata.properties} filter={['notify.discord']} - exclusive={false} + includeOnly={true} /> diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-hipchat.mdx b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-hipchat.mdx similarity index 62% rename from spring-boot-admin-docs/src/site/docs/server/notifications/notifier-hipchat.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-hipchat.mdx index 66a8c915995..c5c2f00d71e 100644 --- a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-hipchat.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-hipchat.mdx @@ -2,8 +2,8 @@ sidebar_custom_props: icon: 'notifications' --- -import metadata from "../../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../../src/components/PropertyTable"; +import metadata from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; # Hipchat Notifications @@ -13,5 +13,5 @@ To enable [Hipchat](https://www.hipchat.com/) notifications you need to create a title="Hipchat notifications configuration options" properties={metadata.properties} filter={['notify.hipchat']} - exclusive={false} + includeOnly={true} /> diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-lets-chat.mdx b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-lets-chat.mdx similarity index 62% rename from spring-boot-admin-docs/src/site/docs/server/notifications/notifier-lets-chat.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-lets-chat.mdx index 671d1e998e7..504f88a58ed 100644 --- a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-lets-chat.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-lets-chat.mdx @@ -2,8 +2,8 @@ sidebar_custom_props: icon: 'notifications' --- -import metadata from "../../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../../src/components/PropertyTable"; +import metadata from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; # Let’s Chat Notifications @@ -13,5 +13,5 @@ To enable [Let’s Chat](https://sdelements.github.io/lets-chat/) notifications title="Let’s Chat notifications configuration options" properties={metadata.properties} filter={['notify.letschat']} - exclusive={false} + includeOnly={true} /> diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-mail.mdx b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-mail.mdx similarity index 82% rename from spring-boot-admin-docs/src/site/docs/server/notifications/notifier-mail.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-mail.mdx index 54a23b7c727..372322c9e19 100644 --- a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-mail.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-mail.mdx @@ -2,8 +2,8 @@ sidebar_custom_props: icon: 'notifications' --- -import metadata from "../../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../../src/components/PropertyTable"; +import metadata from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; # Mail Notifications @@ -37,5 +37,5 @@ spring.boot.admin.notify.mail.to=admin@example.com title="Mail notifications configuration options" properties={metadata.properties} filter={['notify.mail']} -exclusive={false} +includeOnly={true} /> diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-mattermost.mdx b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-mattermost.mdx similarity index 62% rename from spring-boot-admin-docs/src/site/docs/server/notifications/notifier-mattermost.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-mattermost.mdx index 1ec9c9d8538..5684314b9ee 100644 --- a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-mattermost.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-mattermost.mdx @@ -2,8 +2,8 @@ sidebar_custom_props: icon: 'notifications' --- -import metadata from "../../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../../src/components/PropertyTable"; +import metadata from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; # Mattermost Notifications @@ -13,5 +13,5 @@ To enable [Mattermost](https://mattermost.com/) notifications you need to add a title="Mattermost notifications configuration options" properties={metadata.properties} filter={['notify.mattermost']} - exclusive={false} + includeOnly={true} /> diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-msteams.mdx b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-msteams.mdx similarity index 61% rename from spring-boot-admin-docs/src/site/docs/server/notifications/notifier-msteams.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-msteams.mdx index b388eb1633c..8a86dcf700a 100644 --- a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-msteams.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-msteams.mdx @@ -2,8 +2,8 @@ sidebar_custom_props: icon: 'notifications' --- -import metadata from "../../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../../src/components/PropertyTable"; +import metadata from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; # Microsoft Teams Notifications @@ -13,5 +13,5 @@ To enable Microsoft Teams notifications you need to set up a connector webhook u title="Microsoft Teams notifications configuration options" properties={metadata.properties} filter={['notify.ms-teams']} - exclusive={false} + includeOnly={true} /> diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-rocketchat.mdx b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-rocketchat.mdx similarity index 61% rename from spring-boot-admin-docs/src/site/docs/server/notifications/notifier-rocketchat.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-rocketchat.mdx index 4e5d939c5a4..21de85ef8d7 100644 --- a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-rocketchat.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-rocketchat.mdx @@ -2,8 +2,8 @@ sidebar_custom_props: icon: 'notifications' --- -import metadata from "../../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../../src/components/PropertyTable"; +import metadata from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; # RocketChat Notifications @@ -13,5 +13,5 @@ To enable [Rocket.Chat](https://www.rocket.chat/) notifications you need a perso title="RocketChat notifications configuration options" properties={metadata.properties} filter={['notify.rocketchat']} - exclusive={false} + includeOnly={true} /> diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-slack.mdx b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-slack.mdx similarity index 58% rename from spring-boot-admin-docs/src/site/docs/server/notifications/notifier-slack.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-slack.mdx index 9291e99b8d0..3f9ee2cbc92 100644 --- a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-slack.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-slack.mdx @@ -1,9 +1,9 @@ --- sidebar_custom_props: - icon: 'notifications' + icon: 'bell' --- -import metadata from "../../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../../src/components/PropertyTable"; +import metadata from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; # Slack Notifications @@ -13,5 +13,5 @@ To enable [Slack](https://slack.com/) notifications you need to add an incoming title="Slack notifications configuration options" properties={metadata.properties} filter={['notify.slack']} - exclusive={false} + includeOnly={true} /> diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-telegram.mdx b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-telegram.mdx similarity index 63% rename from spring-boot-admin-docs/src/site/docs/server/notifications/notifier-telegram.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-telegram.mdx index 5a4374ab49b..c285c563cf8 100644 --- a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-telegram.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-telegram.mdx @@ -2,8 +2,8 @@ sidebar_custom_props: icon: 'notifications' --- -import metadata from "../../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../../src/components/PropertyTable"; +import metadata from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; # Telegram Notifications @@ -13,5 +13,5 @@ To enable [Telegram](https://telegram.org/) notifications you need to create and title="Telegram notifications configuration options" properties={metadata.properties} filter={['notify.telegram']} - exclusive={false} + includeOnly={true} /> diff --git a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-webex.mdx b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-webex.mdx similarity index 60% rename from spring-boot-admin-docs/src/site/docs/server/notifications/notifier-webex.mdx rename to spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-webex.mdx index 12851d10a91..2ad110a4c4c 100644 --- a/spring-boot-admin-docs/src/site/docs/server/notifications/notifier-webex.mdx +++ b/spring-boot-admin-docs/src/site/docs/02-server/notifications/notifier-webex.mdx @@ -2,8 +2,8 @@ sidebar_custom_props: icon: 'notifications' --- -import metadata from "../../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../../src/components/PropertyTable"; +import metadata from "@sba/spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; # Webex Notifications @@ -13,5 +13,5 @@ To enable [Webex](https://www.webex.com/) notifications, you need to set the app title="Webex notifications configuration options" properties={metadata.properties} filter={['notify.webex']} - exclusive={false} + includeOnly={true} /> diff --git a/spring-boot-admin-docs/src/site/docs/client/10-client-features.md b/spring-boot-admin-docs/src/site/docs/03-client/10-client-features.md similarity index 63% rename from spring-boot-admin-docs/src/site/docs/client/10-client-features.md rename to spring-boot-admin-docs/src/site/docs/03-client/10-client-features.md index 2ab503b04d3..bfedb83ec6c 100644 --- a/spring-boot-admin-docs/src/site/docs/client/10-client-features.md +++ b/spring-boot-admin-docs/src/site/docs/03-client/10-client-features.md @@ -7,9 +7,12 @@ sidebar_custom_props: ## Show Version in Application List -For **Spring Boot** applications the easiest way to show the version, is to use the `build-info` goal from the `spring-boot-maven-plugin`, which generates the `META-INF/build-info.properties`. See also the [Spring Boot Reference Guide](http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#howto-build-info). +For **Spring Boot** applications the easiest way to show the version, is to use the `build-info` goal from the +`spring-boot-maven-plugin`, which generates the `META-INF/build-info.properties`. See also +the [Spring Boot Reference Guide](http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#howto-build-info). -For **non-Spring Boot** applications you can either add a `version` or `build.version` to the registration metadata and the version will show up in the application list. +For **non-Spring Boot** applications you can either add a `version` or `build.version` to the registration metadata and +the version will show up in the application list. ```xml title="pom.xml" @@ -44,6 +47,19 @@ expose it via the actuator endpoint. As Jolokia is servlet based there is no sup You might want to set spring.jmx.enabled=true if you want to expose Spring beans via JMX. +### Spring Boot 4 App + +Spring Boot 4 does not support Jolokia directly, you need a separate dependency for Spring Boot 4-based applications. +See https://jolokia.org/reference/html/manual/spring.html for more details. + +```xml title="pom.xml" + + org.jolokia + jolokia-support-springboot + 2.5.0 + +``` + ### Spring Boot 3 App Spring Boot 3 does not support Jolokia directly, you need a separate dependency for Spring Boot 3-based applications. @@ -52,8 +68,8 @@ See https://jolokia.org/reference/html/manual/spring.html for more details. ```xml title="pom.xml" org.jolokia - jolokia-support-spring - 2.1.0 + jolokia-support-springboot-3 + 2.5.0 ``` @@ -71,13 +87,17 @@ provided the actuator itself, so you only need the plain jolokia dependency. ## Logfile Viewer -By default, the logfile is not accessible via actuator endpoints and therefore not visible in Spring Boot Admin. In order to enable the logfile actuator endpoint you need to configure Spring Boot to write a logfile, either by setting`logging.file.path` or `logging.file.name`. +By default, the logfile is not accessible via actuator endpoints and therefore not visible in Spring Boot Admin. In +order to enable the logfile actuator endpoint you need to configure Spring Boot to write a logfile, either by setting +`logging.file.path` or `logging.file.name`. Spring Boot Admin will detect everything that looks like an URL and render it as hyperlink. -ANSI color-escapes are also supported. You need to set a custom file log pattern as Spring Boot’s default one doesn’t use colors. +ANSI color-escapes are also supported. You need to set a custom file log pattern as Spring Boot’s default one doesn’t +use colors. -To enforce the use of ANSI-colored output, set `spring.output.ansi.enabled=ALWAYS`. Otherwise Spring tries to detect if ANSI-colored output is available and might disable it. +To enforce the use of ANSI-colored output, set `spring.output.ansi.enabled=ALWAYS`. Otherwise Spring tries to detect if +ANSI-colored output is available and might disable it. ```properties title="application.properties" logging.file.name=/var/log/sample-boot-application.log (1) @@ -89,7 +109,9 @@ logging.pattern.file=%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${P ## Show Tags per Instance -`Tags` are a way to add visual markers per instance, they will appear in the application list as well as in the instance view. By default, no tags are added to instances, and it’s up to the client to specify the desired tags by adding the information to the metadata or info endpoint. +`Tags` are a way to add visual markers per instance, they will appear in the application list as well as in the instance +view. By default, no tags are added to instances, and it’s up to the client to specify the desired tags by adding the +information to the metadata or info endpoint. ```properties title="application.properties" #using the metadata @@ -101,8 +123,10 @@ info.tags.environment=test ## Spring Boot Admin Client -The Spring Boot Admin Client registers the application at the admin server. This is done by periodically doing an HTTP post request to the SBA Server providing information about the application. +The Spring Boot Admin Client registers the application at the admin server. This is done by periodically doing an HTTP +post request to the SBA Server providing information about the application. :::tip -There are plenty of properties to influence the way how the SBA Client registers your application. In case that doesn’t fit your needs, you can provide your own ApplicationFactory implementation. +There are plenty of properties to influence the way how the SBA Client registers your application. In case that doesn’t +fit your needs, you can provide your own ApplicationFactory implementation. ::: diff --git a/spring-boot-admin-docs/src/site/docs/03-client/20-registration.md b/spring-boot-admin-docs/src/site/docs/03-client/20-registration.md new file mode 100644 index 00000000000..9fbcebc08e2 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/03-client/20-registration.md @@ -0,0 +1,428 @@ +--- + +sidebar_position: 20 +sidebar_custom_props: + icon: 'link' +--- + +# Application Registration + +The Spring Boot Admin Client handles the registration of your application with the Admin Server through the +`ApplicationRegistrator` and `ApplicationFactory` interfaces. + +## ApplicationRegistrator + +The `ApplicationRegistrator` is responsible for managing the registration lifecycle: + +```java +public interface ApplicationRegistrator { + + /** + * Registers the client application at spring-boot-admin-server. + * @return true if successful registration on at least one admin server + */ + boolean register(); + + /** + * Tries to deregister currently registered application + */ + void deregister(); + + /** + * @return the id of this client as given by the admin server. + * Returns null if not registered yet. + */ + String getRegisteredId(); +} +``` + +### Default Implementation + +The `DefaultApplicationRegistrator` automatically handles: + +- Initial registration on application startup +- Periodic re-registration (heartbeat) +- Automatic deregistration on shutdown +- Retry logic for failed registrations + +### Configuration + +```yaml title="application.yml" +spring: + boot: + admin: + client: + url: http://localhost:8080 # Admin Server URL + period: 10000 # Registration interval in milliseconds + auto-registration: true # Enable auto-registration + auto-deregistration: true # Enable auto-deregistration on shutdown +``` + +### Registration Process + +1. **Application Startup**: The `RegistrationApplicationListener` triggers registration when `WebServerInitializedEvent` + is fired +2. **Create Application**: `ApplicationFactory` creates the registration payload +3. **HTTP POST**: Client sends POST request to `/instances` endpoint +4. **Receive ID**: Server responds with an instance ID +5. **Periodic Heartbeat**: Client re-registers at configured intervals +6. **Shutdown Hook**: Application deregisters on graceful shutdown + +## ApplicationFactory + +The `ApplicationFactory` is responsible for creating the `Application` object that contains all registration +information. + +```java +public interface ApplicationFactory { + Application createApplication(); +} +``` + +### Default Implementation: DefaultApplicationFactory + +The default factory gathers information from: + +- `InstanceProperties` - Client configuration +- `ServerProperties` - Web server configuration +- `ManagementServerProperties` - Actuator configuration +- `PathMappedEndpoints` - Actuator endpoint mappings +- `MetadataContributor` - Custom metadata + +```java +@Override +public Application createApplication() { + return Application.create(getName()) + .healthUrl(getHealthUrl()) + .managementUrl(getManagementUrl()) + .serviceUrl(getServiceUrl()) + .metadata(getMetadata()) + .build(); +} +``` + +### Application Properties + +#### Name + +```yaml +spring: + boot: + admin: + client: + instance: + name: ${spring.application.name} # Application name +``` + +#### Service URL + +The URL where your application can be accessed: + +```yaml +spring: + boot: + admin: + client: + instance: + service-url: https://my-app.example.com + # or let it auto-detect: + service-base-url: https://my-app.example.com + service-path: / +``` + +Auto-detection uses: + +1. Configured `service-url` (highest priority) +2. `service-base-url` + `service-path` +3. Auto-detected from server properties + +#### Management URL + +URL for actuator endpoints: + +```yaml +spring: + boot: + admin: + client: + instance: + management-url: https://my-app.example.com/actuator + # or + management-base-url: https://my-app.example.com +management: + endpoints: + web: + base-path: /actuator +``` + +#### Health URL + +Specific health endpoint URL: + +```yaml +spring: + boot: + admin: + client: + instance: + health-url: https://my-app.example.com/actuator/health +``` + +### Host Type + +Control how the service host is determined: + +```yaml +spring: + boot: + admin: + client: + instance: + service-host-type: IP # or CANONICAL +``` + +- **`IP`**: Use the IP address +- **`CANONICAL`**: Use the canonical hostname + +### Custom ApplicationFactory + +Create a custom factory for specialized registration logic: + +```java +@Component +public class CustomApplicationFactory implements ApplicationFactory { + + private final InstanceProperties instance; + private final Environment environment; + + public CustomApplicationFactory(InstanceProperties instance, + Environment environment) { + this.instance = instance; + this.environment = environment; + } + + @Override + public Application createApplication() { + Map metadata = new HashMap<>(); + metadata.put("environment", environment.getProperty("app.environment")); + metadata.put("version", environment.getProperty("app.version")); + metadata.put("region", environment.getProperty("cloud.region")); + + return Application.create(instance.getName()) + .healthUrl(buildHealthUrl()) + .managementUrl(buildManagementUrl()) + .serviceUrl(buildServiceUrl()) + .metadata(metadata) + .build(); + } + + private String buildHealthUrl() { + // Custom logic to build health URL + return "https://my-app.com/health"; + } + + private String buildManagementUrl() { + // Custom logic to build management URL + return "https://my-app.com/management"; + } + + private String buildServiceUrl() { + // Custom logic to build service URL + return "https://my-app.com"; + } +} +``` + +## Specialized ApplicationFactories + +### Servlet ApplicationFactory + +For servlet-based applications: + +```java +public class ServletApplicationFactory extends DefaultApplicationFactory { + // Detects servlet port and context path automatically +} +``` + +### Reactive ApplicationFactory + +For WebFlux applications: + +```java +public class ReactiveApplicationFactory extends DefaultApplicationFactory { + // Detects Netty port and context automatically +} +``` + +### Cloud Foundry ApplicationFactory + +For Cloud Foundry deployments: + +```java +public class CloudFoundryApplicationFactory implements ApplicationFactory { + // Uses CF-specific environment variables: + // - vcap.application.application_id + // - vcap.application.instance_id + // - vcap.application.uris +} +``` + +Automatically activated when Cloud Foundry is detected. + +## Application Class + +The `Application` class represents the registration payload: + +```java +public class Application { + private final String name; + private final String managementUrl; + private final String healthUrl; + private final String serviceUrl; + private final Map metadata; + + // Builder pattern + public static Builder create(String name) { + return new Builder(name); + } +} +``` + +### Building an Application + +```java +Application app = Application.create("my-application") + .healthUrl("http://localhost:8080/actuator/health") + .managementUrl("http://localhost:8080/actuator") + .serviceUrl("http://localhost:8080") + .metadata("version", "1.0.0") + .metadata("environment", "production") + .build(); +``` + +## Registration Lifecycle Events + +Spring Boot Admin Client fires application events during registration: + +```java +@Component +public class RegistrationEventListener { + + @EventListener + public void onRegistration(InstanceRegisteredEvent event) { + String instanceId = event.getRegistration().getInstanceId(); + log.info("Registered with instance ID: {}", instanceId); + } + + @EventListener + public void onDeregistration(InstanceDeregisteredEvent event) { + log.info("Deregistered instance"); + } +} +``` + +## Custom Registrator + +Implement custom registration logic: + +```java +@Component +public class CustomApplicationRegistrator implements ApplicationRegistrator { + + private final ApplicationFactory applicationFactory; + private final RestClient restClient; + private final String adminUrl; + private volatile String registeredId; + + @Override + public boolean register() { + Application application = applicationFactory.createApplication(); + + try { + Map response = restClient.post() + .uri(adminUrl + "/instances") + .body(application) + .retrieve() + .body(new ParameterizedTypeReference>() {}); + + this.registeredId = (String) response.get("id"); + log.info("Registered as: {}", registeredId); + return true; + } catch (Exception e) { + log.error("Registration failed", e); + return false; + } + } + + @Override + public void deregister() { + if (registeredId != null) { + try { + restClient.delete() + .uri(adminUrl + "/instances/" + registeredId) + .retrieve() + .toBodilessEntity(); + log.info("Deregistered successfully"); + } catch (Exception e) { + log.error("Deregistration failed", e); + } + } + } + + @Override + public String getRegisteredId() { + return registeredId; + } +} +``` + +## Troubleshooting + +### Registration Fails + +Check: + +- Admin Server URL is correct and accessible +- Network connectivity between client and server +- Firewall rules allow outbound connections +- Admin Server is running and healthy +- Credentials are correct if security is enabled + +### Instance Not Appearing + +Verify: + +- Registration returned successfully (check logs) +- Application name is configured +- Health endpoint is accessible from Admin Server +- Actuator endpoints are exposed + +### Repeated Re-registrations + +This is normal behavior - the client re-registers periodically as a heartbeat. Adjust the period if needed: + +```yaml +spring: + boot: + admin: + client: + period: 30000 # 30 seconds instead of default 10 +``` + +## Best Practices + +1. **Use environment-specific URLs** for service URL in different environments +2. **Configure appropriate metadata** to help identify instances +3. **Set reasonable registration periods** - too frequent causes unnecessary load +4. **Enable auto-deregistration** for clean shutdown +5. **Use service discovery** for dynamic environments instead of direct client registration +6. **Monitor registration logs** to ensure successful registration +7. **Configure health check paths** correctly for proper monitoring + +## See Also + +- [Metadata](./30-metadata.md) - Learn about custom metadata +- [Service Discovery](./40-service-discovery.md) - Alternative registration using Spring Cloud Discovery +- [Client Configuration](./80-configuration.md) - Complete configuration reference +- [Client Features](./10-client-features.md) - Additional client capabilities diff --git a/spring-boot-admin-docs/src/site/docs/03-client/30-metadata.md b/spring-boot-admin-docs/src/site/docs/03-client/30-metadata.md new file mode 100644 index 00000000000..f25eaf00a08 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/03-client/30-metadata.md @@ -0,0 +1,451 @@ +--- +sidebar_position: 30 +sidebar_custom_props: + icon: 'link' +--- + +# Metadata and Tags + +Metadata allows you to attach custom information to your application registration, which can be used for filtering, +grouping, and providing additional context in the Spring Boot Admin UI. + +## MetadataContributor + +The `MetadataContributor` interface enables you to programmatically add metadata to your application registration: + +```java +@FunctionalInterface +public interface MetadataContributor { + Map getMetadata(); +} +``` + +## Built-in Metadata Contributors + +### StartupDateMetadataContributor + +Automatically adds the application startup timestamp: + +```java +public class StartupDateMetadataContributor implements MetadataContributor { + + private final OffsetDateTime timestamp = OffsetDateTime.now(); + + @Override + public Map getMetadata() { + return singletonMap("startup", + timestamp.format(DateTimeFormatter.ISO_DATE_TIME)); + } +} +``` + +This metadata is automatically included and helps the Admin Server detect application restarts. + +### CloudFoundryMetadataContributor + +For Cloud Foundry deployments, adds CF-specific metadata: + +```java +public class CloudFoundryMetadataContributor implements MetadataContributor { + + @Override + public Map getMetadata() { + Map metadata = new HashMap<>(); + metadata.put("applicationId", vcapApplication.getApplicationId()); + metadata.put("instanceId", vcapApplication.getInstanceId()); + // Additional CF metadata + return metadata; + } +} +``` + +Automatically activated when running on Cloud Foundry. + +### CompositeMetadataContributor + +Combines multiple metadata contributors: + +```java +public class CompositeMetadataContributor implements MetadataContributor { + + private final List delegates; + + public CompositeMetadataContributor(List delegates) { + this.delegates = delegates; + } + + @Override + public Map getMetadata() { + Map metadata = new LinkedHashMap<>(); + delegates.forEach(delegate -> metadata.putAll(delegate.getMetadata())); + return metadata; + } +} +``` + +Spring Boot Admin automatically creates a composite contributor from all `MetadataContributor` beans. + +## Adding Metadata via Configuration + +### Static Metadata + +Add static metadata through properties: + +```yaml title="application.yml" +spring: + boot: + admin: + client: + instance: + metadata: + team: platform-team + environment: production + region: us-east-1 + version: 1.0.0 + support-email: platform@example.com +``` + +### Dynamic Metadata from Environment + +Use property placeholders to inject environment-specific values: + +```yaml title="application.yml" +spring: + boot: + admin: + client: + instance: + metadata: + environment: ${APP_ENV:development} + version: ${APP_VERSION:unknown} + hostname: ${HOSTNAME:localhost} + pod-name: ${POD_NAME:} + namespace: ${NAMESPACE:default} +``` + +## Custom MetadataContributor + +Create custom metadata contributors for dynamic or computed metadata: + +```java +import org.springframework.stereotype.Component; +import java.util.HashMap; +import java.util.Map; + +@Component +public class CustomMetadataContributor implements MetadataContributor { + + private final Environment environment; + private final BuildProperties buildProperties; + + public CustomMetadataContributor(Environment environment, + @Autowired(required = false) BuildProperties buildProperties) { + this.environment = environment; + this.buildProperties = buildProperties; + } + + @Override + public Map getMetadata() { + Map metadata = new HashMap<>(); + + // Add build information + if (buildProperties != null) { + metadata.put("build.version", buildProperties.getVersion()); + metadata.put("build.time", buildProperties.getTime().toString()); + metadata.put("build.artifact", buildProperties.getArtifact()); + } + + // Add environment information + metadata.put("spring.profiles", String.join(",", + environment.getActiveProfiles())); + + // Add JVM information + metadata.put("java.version", System.getProperty("java.version")); + metadata.put("java.vendor", System.getProperty("java.vendor")); + + // Add custom business metadata + metadata.put("feature-flags", + environment.getProperty("app.feature-flags", "")); + + return metadata; + } +} +``` + +### Kubernetes Metadata + +```java +@Component +@ConditionalOnProperty(name = "kubernetes.enabled", havingValue = "true") +public class KubernetesMetadataContributor implements MetadataContributor { + + @Override + public Map getMetadata() { + Map metadata = new HashMap<>(); + + // Read from environment variables set by Kubernetes + metadata.put("k8s.pod", System.getenv("HOSTNAME")); + metadata.put("k8s.namespace", System.getenv("POD_NAMESPACE")); + metadata.put("k8s.node", System.getenv("NODE_NAME")); + metadata.put("k8s.service-account", + System.getenv("SERVICE_ACCOUNT")); + + // Add labels as metadata + String labels = System.getenv("POD_LABELS"); + if (labels != null) { + metadata.put("k8s.labels", labels); + } + + return metadata; + } +} +``` + +## Tags + +Tags are a special type of metadata used for visual markers in the Admin UI. They appear as colored badges in the +application list and instance views. + +### Configuring Tags + +#### Via Metadata + +```yaml title="application.yml" +spring: + boot: + admin: + client: + instance: + metadata: + tags: + environment: production + region: us-west-2 + tier: backend +``` + +#### Via Info Endpoint + +```yaml title="application.yml" +info: + tags: + environment: production + region: us-west-2 + tier: backend +``` + +### Tag Display + +Tags appear as colored badges: + +- In the applications list view +- In the instance details header +- Can be used for filtering and grouping + +### Dynamic Tags + +Create tags dynamically based on runtime conditions: + +```java +@Component +public class DynamicTagMetadataContributor implements MetadataContributor { + + private final Environment environment; + + public DynamicTagMetadataContributor(Environment environment) { + this.environment = environment; + } + + @Override + public Map getMetadata() { + Map metadata = new HashMap<>(); + + // Environment tag + String env = environment.getProperty("spring.profiles.active", "default"); + metadata.put("tags.environment", env); + + // Deployment type + if (isKubernetes()) { + metadata.put("tags.platform", "kubernetes"); + } else if (isCloudFoundry()) { + metadata.put("tags.platform", "cloud-foundry"); + } else { + metadata.put("tags.platform", "standalone"); + } + + // Health-based tag + metadata.put("tags.monitoring", "enabled"); + + return metadata; + } + + private boolean isKubernetes() { + return System.getenv("KUBERNETES_SERVICE_HOST") != null; + } + + private boolean isCloudFoundry() { + return System.getenv("VCAP_APPLICATION") != null; + } +} +``` + +## Metadata Use Cases + +### 1. Security Credentials + +Pass credentials for actuator endpoint access: + +```yaml title="application.yml" +spring: + boot: + admin: + client: + instance: + metadata: + user.name: ${spring.security.user.name} + user.password: ${spring.security.user.password} +``` + +:::warning +Credentials in metadata are masked in the Admin UI but transmitted over the network. Always use HTTPS when transmitting +sensitive data. +::: + +### 2. Service Discovery Integration + +#### Eureka + +```yaml title="application.yml" +eureka: + instance: + metadata-map: + startup: ${random.int} # Trigger update on restart + user.name: ${spring.security.user.name} + user.password: ${spring.security.user.password} + tags.environment: ${APP_ENV} +``` + +#### Consul + +```yaml title="application.yml" +spring: + cloud: + consul: + discovery: + metadata: + user-name: ${spring.security.user.name} # Note: use dashes, not dots + user-password: ${spring.security.user.password} + environment: production +``` + +:::warning +Consul does not allow dots (`.`) in metadata keys. Use dashes (`-`) instead. +::: + +### 3. Grouping Applications + +```yaml title="application.yml" +spring: + boot: + admin: + client: + instance: + metadata: + group: Legacy Squad + squad: backend-team + cost-center: CC-1234 +``` + +The Admin UI can use the `group` metadata for visual grouping. + +### 4. Custom URLs and Paths + +```yaml title="application.yml" +spring: + boot: + admin: + client: + instance: + metadata: + management.context-path: /actuator + service-url: https://my-app.example.com + service-path: /api +``` + +### 5. Visibility Control + +```yaml title="application.yml" +spring: + boot: + admin: + client: + instance: + metadata: + hide-url: true # Hide service URL in UI +``` + +## Accessing Metadata in Admin Server + +Metadata is available through the instance object: + +```java +@Component +public class MetadataProcessor { + + public void processInstance(Instance instance) { + Map metadata = instance.getRegistration().getMetadata(); + + String environment = metadata.get("tags.environment"); + String team = metadata.get("team"); + String version = metadata.get("version"); + + // Process metadata + log.info("Instance {} - Environment: {}, Team: {}, Version: {}", + instance.getRegistration().getName(), + environment, team, version); + } +} +``` + +## Best Practices + +1. **Use Meaningful Keys**: Use descriptive, hierarchical keys (e.g., `tags.environment`, `k8s.namespace`) +2. **Avoid Sensitive Data**: Don't include secrets unless necessary; use secure transmission +3. **Keep It Lightweight**: Don't overload metadata with large values +4. **Use Tags for Visuals**: Leverage tags for important visual indicators +5. **Document Metadata**: Maintain documentation of your metadata schema +6. **Use Environment Variables**: Make metadata configurable per environment +7. **Consistent Naming**: Use consistent naming conventions across services +8. **Leverage Existing Info**: Use `/actuator/info` for build and git information + +## Metadata Security + +### Masked Metadata + +The Admin Server masks certain metadata keys by default: + +- `password` +- `secret` +- `key` +- `token` +- `credentials` + +These values are hidden in the UI but still transmitted to the server. + +### Secure Transmission + +Always use HTTPS when transmitting sensitive metadata: + +```yaml +spring: + boot: + admin: + client: + url: https://admin-server.example.com # Use HTTPS +``` + +## See Also + +- [Client Features](./10-client-features.md) - Other client capabilities +- [Registration](./20-registration.md) - Application registration process +- [Service Discovery](./40-service-discovery.md) - Metadata in service discovery +- [Security](../02-server/02-security.md) - Securing metadata transmission diff --git a/spring-boot-admin-docs/src/site/docs/03-client/40-service-discovery.md b/spring-boot-admin-docs/src/site/docs/03-client/40-service-discovery.md new file mode 100644 index 00000000000..591004c038f --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/03-client/40-service-discovery.md @@ -0,0 +1,490 @@ +--- +sidebar_position: 40 +sidebar_custom_props: + icon: 'cloud' +--- + +# Service Discovery Integration + +Spring Boot Admin integrates seamlessly with Spring Cloud Discovery services, allowing automatic registration without +the Spring Boot Admin Client library. + +## Overview + +When using service discovery, the Admin Server discovers applications automatically through the discovery client. This +eliminates the need for: + +- Spring Boot Admin Client dependency +- Explicit Admin Server URL configuration +- Manual registration code + +## Supported Discovery Services + +- **Eureka** (Netflix) +- **Consul** (HashiCorp) +- **Zookeeper** (Apache) +- **Kubernetes** (via Spring Cloud Kubernetes) + +## Eureka Integration + +### Server Setup + +Add Eureka Client to your Admin Server: + +```xml title="pom.xml" + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +Enable discovery in your Admin Server: + +```java title="SpringBootAdminApplication.java" +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import de.codecentric.boot.admin.server.config.EnableAdminServer; + +@EnableDiscoveryClient +@EnableAdminServer +@SpringBootApplication +public class SpringBootAdminApplication { + static void main(String[] args) { + SpringApplication.run(SpringBootAdminApplication.class, args); + } +} +``` + +Configure Eureka connection: + +```yaml title="application.yml" +spring: + application: + name: spring-boot-admin-server + +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + registryFetchIntervalSeconds: 5 + instance: + leaseRenewalIntervalInSeconds: 10 + health-check-url-path: /actuator/health + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS +``` + +### Client Setup + +Add Eureka Client to your application (no Admin Client needed): + +```xml title="pom.xml" + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +Enable discovery: + +```java +@EnableDiscoveryClient +@SpringBootApplication +public class MyApplication { + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } +} +``` + +Configure Eureka and expose endpoints: + +```yaml title="application.yml" +spring: + application: + name: my-application + +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + instance: + leaseRenewalIntervalInSeconds: 10 + health-check-url-path: /actuator/health + metadata-map: + startup: ${random.int} # Triggers info update on restart + user.name: ${spring.security.user.name} # For secured actuators + user.password: ${spring.security.user.password} + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS +``` + +### Eureka Metadata + +Add custom metadata through Eureka: + +```yaml title="application.yml" +eureka: + instance: + metadata-map: + startup: ${random.int} + tags.environment: production + tags.region: us-east-1 + team: platform + version: ${spring.application.version} +``` + +## Consul Integration + +### Server Setup + +```xml title="pom.xml" + + org.springframework.cloud + spring-cloud-starter-consul-discovery + +``` + +```java +@EnableDiscoveryClient +@EnableAdminServer +@SpringBootApplication +public class SpringBootAdminApplication { + static void main(String[] args) { + SpringApplication.run(SpringBootAdminApplication.class, args); + } +} +``` + +```yaml title="application.yml" +spring: + application: + name: spring-boot-admin-server + cloud: + consul: + host: localhost + port: 8500 + discovery: + prefer-ip-address: true + health-check-interval: 10s +``` + +### Client Setup + +```xml title="pom.xml" + + org.springframework.cloud + spring-cloud-starter-consul-discovery + +``` + +```yaml title="application.yml" +spring: + application: + name: my-application + cloud: + consul: + host: localhost + port: 8500 + discovery: + metadata: + user-name: ${spring.security.user.name} # Note: dashes not dots! + user-password: ${spring.security.user.password} + environment: production + management-context-path: ${management.server.base-path:/actuator} + +management: + endpoints: + web: + exposure: + include: "*" +``` + +:::warning +Consul does not allow dots (`.`) in metadata keys. Use dashes (`-`) or underscores (`_`) instead: + +- ✅ `user-name` or `user_name` +- ❌ `user.name` + ::: + +## Zookeeper Integration + +### Server Setup + +```xml title="pom.xml" + + org.springframework.cloud + spring-cloud-starter-zookeeper-discovery + +``` + +```yaml title="application.yml" +spring: + application: + name: spring-boot-admin-server + cloud: + zookeeper: + connect-string: localhost:2181 + discovery: + enabled: true +``` + +### Client Setup + +```xml title="pom.xml" + + org.springframework.cloud + spring-cloud-starter-zookeeper-discovery + +``` + +```yaml title="application.yml" +spring: + application: + name: my-application + cloud: + zookeeper: + connect-string: localhost:2181 + discovery: + metadata: + user.name: ${spring.security.user.name} + user.password: ${spring.security.user.password} + management.context-path: /actuator +``` + +## Filtering Discovered Services + +By default, the Admin Server monitors all discovered services. You can filter services using the `InstanceFilter` +interface. + +### Configuration-Based Filtering + +```yaml title="application.yml" +spring: + boot: + admin: + discovery: + ignored-services: consul,eureka,zookeeper # Don't monitor discovery services +``` + +### Custom InstanceFilter + +```java +import de.codecentric.boot.admin.server.domain.values.Registration; +import de.codecentric.boot.admin.server.services.InstanceFilter; +import org.springframework.stereotype.Component; + +@Component +public class CustomInstanceFilter implements InstanceFilter { + + @Override + public boolean test(Registration registration) { + String name = registration.getName(); + + // Ignore internal services + if (name.startsWith("internal-")) { + return false; + } + + // Only monitor services with specific metadata + String monitorable = registration.getMetadata().get("monitorable"); + if (!"true".equals(monitorable)) { + return false; + } + + return true; + } +} +``` + +## Instance Preference Strategy + +When multiple instances of the same service exist, configure which instance URL to use: + +```yaml title="application.yml" +spring: + boot: + admin: + discovery: + instance-prefer-ip: true # Use IP instead of hostname +``` + +## Management Context Path + +If your management endpoints are on a different port or path: + +```yaml title="application.yml (Client)" +management: + server: + port: 9090 # Management on different port + base-path: /management + +eureka: + instance: + metadata-map: + management.port: 9090 + management.context-path: /management +``` + +## Health Check Configuration + +Configure health check paths for discovery: + +```yaml title="application.yml" +eureka: + instance: + health-check-url-path: /actuator/health + health-check-url: http://my-app.example.com/actuator/health + status-page-url-path: /actuator/info + home-page-url: / +``` + +## Service URL vs Management URL + +Discovery services may return different URLs for the service and management endpoints: + +```yaml title="application.yml" +eureka: + instance: + metadata-map: + management.context-path: /actuator # Management endpoint path + service-url: https://my-app.example.com # Public service URL + management-url: http://internal-app:8080/actuator # Internal mgmt URL +``` + +## Securing Discovered Services + +Pass credentials through metadata: + +```yaml title="application.yml" +eureka: + instance: + metadata-map: + user.name: admin + user.password: ${ACTUATOR_PASSWORD} +``` + +Or configure on the Admin Server: + +```yaml title="application.yml (Admin Server)" +spring: + boot: + admin: + instance-auth: + enabled: true + default-user-name: admin + default-password: ${DEFAULT_PASSWORD} + service-map: + my-application: + user-name: app-admin + user-password: ${APP_PASSWORD} +``` + +## Advantages of Service Discovery + +1. **No Client Library Required**: Applications don't need Spring Boot Admin Client +2. **Automatic Discovery**: New instances automatically appear +3. **Centralized Configuration**: Manage discovery in one place +4. **Load Balancing**: Discovery services handle load balancing +5. **Health Checks**: Built-in health check integration +6. **Service Metadata**: Rich metadata support + +## Disadvantages + +1. **Additional Infrastructure**: Requires running discovery service +2. **Network Complexity**: Additional network hop +3. **Discovery Lag**: Slight delay in detecting new instances +4. **Metadata Limitations**: Some discovery services have metadata restrictions + +## Mixed Mode + +You can use both service discovery and direct client registration simultaneously: + +```yaml title="application.yml" +spring: + boot: + admin: + client: + url: http://localhost:8080 # Direct registration + auto-registration: true + +eureka: + client: + enabled: true # Also register with Eureka +``` + +This provides redundancy if one registration method fails. + +## Troubleshooting + +### Application Not Discovered + +1. **Check Discovery Registration**: + ```bash + # For Eureka + curl http://localhost:8761/eureka/apps + ``` + +2. **Verify Admin Server Discovery Client**: Ensure `@EnableDiscoveryClient` is present + +3. **Check Network Connectivity**: Admin Server must reach discovery service + +4. **Review Metadata**: Ensure management URLs are correct + +### Incorrect Management URL + +Set explicit management metadata: + +```yaml +eureka: + instance: + metadata-map: + management.port: ${management.server.port} + management.context-path: ${management.server.base-path} +``` + +### Health Check Failures + +Ensure health endpoint is accessible: + +```yaml +management: + endpoint: + health: + show-details: ALWAYS + endpoints: + web: + exposure: + include: health,info +``` + +## Best Practices + +1. **Use Metadata for Configuration**: Leverage metadata for flexible configuration +2. **Set Appropriate Intervals**: Balance between freshness and load +3. **Implement Filters**: Don't monitor unnecessary services +4. **Secure Metadata Transmission**: Use secure discovery service connections +5. **Monitor Discovery Health**: Ensure discovery service is healthy +6. **Document Metadata Schema**: Maintain clear metadata conventions +7. **Test Failover**: Verify behavior when discovery service is down + +## See Also + +- [Registration](./20-registration.md) - Direct client registration +- [Metadata](./30-metadata.md) - Working with metadata +- [Eureka Sample](../09-samples/30-sample-eureka.md) - Complete Eureka example +- [Consul Sample](../09-samples/40-sample-consul.md) - Complete Consul example +- [Security](../02-server/02-security.md) - Securing discovered services diff --git a/spring-boot-admin-docs/src/site/docs/client/80-configuration.md b/spring-boot-admin-docs/src/site/docs/03-client/80-configuration.md similarity index 100% rename from spring-boot-admin-docs/src/site/docs/client/80-configuration.md rename to spring-boot-admin-docs/src/site/docs/03-client/80-configuration.md diff --git a/spring-boot-admin-docs/src/site/docs/03-client/99-properties.mdx b/spring-boot-admin-docs/src/site/docs/03-client/99-properties.mdx new file mode 100644 index 00000000000..eacd2c9c0b2 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/03-client/99-properties.mdx @@ -0,0 +1,15 @@ +--- +sidebar_custom_props: + icon: 'server' +--- + +# Properties + +import metadata from "@sba/spring-boot-admin-client/target/classes/META-INF/spring-configuration-metadata.json"; +import { PropertyTable } from "@sba/spring-boot-admin-docs/src/site/src/components/PropertyTable"; + + diff --git a/spring-boot-admin-docs/src/site/docs/client/_category_.json b/spring-boot-admin-docs/src/site/docs/03-client/_category_.json similarity index 59% rename from spring-boot-admin-docs/src/site/docs/client/_category_.json rename to spring-boot-admin-docs/src/site/docs/03-client/_category_.json index af57753db34..1ba33429568 100644 --- a/spring-boot-admin-docs/src/site/docs/client/_category_.json +++ b/spring-boot-admin-docs/src/site/docs/03-client/_category_.json @@ -1,4 +1,4 @@ { - "position": 2, + "position": 3, "label": "Clients" } diff --git a/spring-boot-admin-docs/src/site/docs/client/index.md b/spring-boot-admin-docs/src/site/docs/03-client/index.md similarity index 57% rename from spring-boot-admin-docs/src/site/docs/client/index.md rename to spring-boot-admin-docs/src/site/docs/03-client/index.md index db05f0faa95..b0970db614f 100644 --- a/spring-boot-admin-docs/src/site/docs/client/index.md +++ b/spring-boot-admin-docs/src/site/docs/03-client/index.md @@ -1,16 +1,29 @@ +--- +sidebar_custom_props: + icon: 'link' +--- + import DocCardList from '@theme/DocCardList'; # Registering Clients -Spring Boot Admin is built on top of the mechanisms provided by Spring Cloud. This means that it can integrate seamlessly with any Spring Cloud–compliant service discovery tool. Common options include Eureka, Kubernetes, Nacos, and many others that implement the Spring Cloud Discovery interfaces. +Spring Boot Admin is built on top of the mechanisms provided by Spring Cloud. This means that it can integrate +seamlessly with any Spring Cloud–compliant service discovery tool. Common options include Eureka, Kubernetes, Nacos, and +many others that implement the Spring Cloud Discovery interfaces. When it comes to connecting services, Spring Boot Admin offers flexible configuration options. You can choose to configure the services explicitly, so that the admin server is aware of them without relying on any discovery mechanism. This approach is often useful in smaller environments or when the service landscape is relatively static. -Alternatively, you can leverage Spring Cloud’s service discovery features. In this mode, Spring Boot Admin automatically discovers and registers services that are available within the configured discovery system. This reduces manual configuration overhead and is particularly well-suited for dynamic, cloud-native environments where services may scale up and down frequently. +Alternatively, you can leverage Spring Cloud’s service discovery features. In this mode, Spring Boot Admin automatically +discovers and registers services that are available within the configured discovery system. This reduces manual +configuration overhead and is particularly well-suited for dynamic, cloud-native environments where services may scale +up and down frequently. -Importantly, Spring Boot Admin does not force you to choose one method exclusively. A hybrid setup is also possible, where some services are registered manually while others are discovered automatically through Spring Cloud. This allows you to tailor the setup to your specific infrastructure and operational needs, combining the stability of manual configuration with the flexibility of automated discovery. +Importantly, Spring Boot Admin does not force you to choose one method exclusively. A hybrid setup is also possible, +where some services are registered manually while others are discovered automatically through Spring Cloud. This allows +you to tailor the setup to your specific infrastructure and operational needs, combining the stability of manual +configuration with the flexibility of automated discovery. **Key Features:** diff --git a/spring-boot-admin-docs/src/site/docs/04-integration/10-eureka.md b/spring-boot-admin-docs/src/site/docs/04-integration/10-eureka.md new file mode 100644 index 00000000000..4d3636cd991 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/04-integration/10-eureka.md @@ -0,0 +1,554 @@ +--- +sidebar_position: 10 +sidebar_custom_props: + icon: 'cloud' +--- + +# Eureka Integration + +Netflix Eureka is a service discovery and registration solution that integrates seamlessly with Spring Boot Admin. This +guide shows how to set up automatic application discovery using Eureka. + +## Overview + +With Eureka integration: + +- Applications register with Eureka server +- Spring Boot Admin Server discovers applications automatically +- No Spring Boot Admin Client library needed +- Applications appear/disappear based on Eureka registration status + +## Architecture + +```mermaid +flowchart LR + Apps[Applications] + Eureka[Eureka Server
Service Registry] + Admin[Spring Boot Admin
Server] + + Apps -->|Register| Eureka + Admin -->|Discover| Eureka + Admin -.->|Monitor| Apps +``` + +Applications register with Eureka, and the Admin Server discovers them through Eureka's registry. + +## Setting Up Eureka Server + +First, you need a Eureka Server running. Here's a minimal setup: + +### Dependencies + +```xml title="pom.xml" + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + +``` + +### Configuration + +```java title="EurekaServerApplication.java" +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +@EnableEurekaServer +@SpringBootApplication +public class EurekaServerApplication { + static void main(String[] args) { + SpringApplication.run(EurekaServerApplication.class, args); + } +} +``` + +```yaml title="application.yml" +server: + port: 8761 + +eureka: + client: + registerWithEureka: false + fetchRegistry: false + server: + enableSelfPreservation: false +``` + +## Configuring Spring Boot Admin Server + +### Add Dependencies + +Add Eureka client to your Admin Server: + +```xml title="pom.xml" + + + + de.codecentric + spring-boot-admin-starter-server + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + +``` + +### Enable Discovery + +Enable both Admin Server and Eureka Discovery: + +```java title="SpringBootAdminEurekaApplication.java" +import de.codecentric.boot.admin.server.config.EnableAdminServer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@EnableDiscoveryClient +@EnableAdminServer +@SpringBootApplication +public class SpringBootAdminEurekaApplication { + public static void main(String[] args) { + SpringApplication.run(SpringBootAdminEurekaApplication.class, args); + } +} +``` + +### Configure Eureka Client + +```yaml title="application.yml" +spring: + application: + name: spring-boot-admin-server + +eureka: + instance: + leaseRenewalIntervalInSeconds: 10 + health-check-url-path: /actuator/health + metadata-map: + startup: ${random.int} # Trigger info update on restart + client: + registryFetchIntervalSeconds: 5 + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS +``` + +## Configuring Client Applications + +Applications only need Eureka Client - no Spring Boot Admin Client required! + +### Add Dependencies + +```xml title="pom.xml" + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + +``` + +### Enable Discovery + +```java title="Application.java" +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@EnableDiscoveryClient +@SpringBootApplication +public class Application { + static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +### Configure Application + +```yaml title="application.yml" +spring: + application: + name: my-application + +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + instance: + leaseRenewalIntervalInSeconds: 10 + health-check-url-path: /actuator/health + metadata-map: + startup: ${random.int} # Triggers info/endpoint update on restart + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS +``` + +## Metadata Configuration + +### Adding Custom Metadata + +Pass custom metadata through Eureka registration: + +```yaml title="application.yml" +eureka: + instance: + metadata-map: + startup: ${random.int} + tags.environment: production + tags.region: us-east-1 + team: platform + version: 1.0.0 +``` + +### Security Credentials + +For secured actuator endpoints, pass credentials via metadata: + +```yaml title="application.yml" +eureka: + instance: + metadata-map: + user.name: ${spring.security.user.name} + user.password: ${spring.security.user.password} +``` + +:::warning +Credentials in metadata are visible to anyone who can query Eureka. Use HTTPS and secure your Eureka server +appropriately. +::: + +### Management Port Configuration + +If management endpoints are on a different port: + +```yaml title="application.yml" +server: + port: 8080 + +management: + server: + port: 9090 + endpoints: + web: + base-path: /actuator + +eureka: + instance: + metadata-map: + management.port: 9090 + management.context-path: /actuator +``` + +## Service URL Configuration + +### Custom Service URL + +Override the service URL Spring Boot Admin uses: + +```yaml title="application.yml" +eureka: + instance: + metadata-map: + service-url: https://my-app.example.com + management-url: http://internal-app:9090/actuator +``` + +### Prefer IP Address + +Use IP address instead of hostname: + +```yaml title="application.yml" +eureka: + instance: + preferIpAddress: true +``` + +On the Admin Server: + +```yaml title="application.yml" +spring: + boot: + admin: + discovery: + instancePreferIp: true +``` + +## Filtering Services + +### Ignore Specific Services + +Don't monitor certain services: + +```yaml title="application.yml (Admin Server)" +spring: + boot: + admin: + discovery: + ignored-services: eureka,config-server,gateway +``` + +### Custom Instance Filter + +```java +import de.codecentric.boot.admin.server.domain.values.Registration; +import de.codecentric.boot.admin.server.services.InstanceFilter; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class InstanceFilterConfig { + + @Bean + public InstanceFilter customInstanceFilter() { + return registration -> { + String name = registration.getName(); + + // Don't monitor infrastructure services + if (name.startsWith("eureka") || + name.startsWith("config") || + name.startsWith("gateway")) { + return false; + } + + // Only monitor services with specific metadata + String monitorable = registration.getMetadata().get("monitor"); + return "true".equals(monitorable); + }; + } +} +``` + +## Health Check Configuration + +### Custom Health Check Path + +```yaml title="application.yml" +eureka: + instance: + health-check-url-path: /actuator/health + health-check-url: http://my-app.example.com/actuator/health +``` + +### Status Page URL + +```yaml title="application.yml" +eureka: + instance: + status-page-url-path: /actuator/info + status-page-url: http://my-app.example.com/actuator/info +``` + +## Securing Eureka Discovery + +### Basic Authentication + +Secure Eureka server with basic auth: + +```yaml title="application.yml (Admin Server)" +eureka: + client: + serviceUrl: + defaultZone: http://user:password@localhost:8761/eureka/ +``` + +### Mutual TLS + +Configure SSL for Eureka communication: + +```yaml title="application.yml" +eureka: + client: + serviceUrl: + defaultZone: https://localhost:8761/eureka/ + tls: + enabled: true + key-store: classpath:keystore.p12 + key-store-password: changeit + trust-store: classpath:truststore.jks + trust-store-password: changeit +``` + +## Docker Compose Example + +```yaml title="docker-compose.yml" +version: '3' + +services: + eureka: + image: springcloud/eureka + ports: + - "8761:8761" + environment: + - EUREKA_INSTANCE_HOSTNAME=eureka + + spring-boot-admin: + build: ./admin-server + ports: + - "8080:8080" + environment: + - EUREKA_SERVICE_URL=http://eureka:8761 + depends_on: + - eureka + + my-application: + build: ./my-app + ports: + - "8081:8081" + environment: + - EUREKA_SERVICE_URL=http://eureka:8761 + depends_on: + - eureka + - spring-boot-admin +``` + +## Troubleshooting + +### Application Not Appearing + +1. **Check Eureka Registration**: + ```bash + curl http://localhost:8761/eureka/apps + ``` + +2. **Verify Admin Server sees Eureka apps**: + Check Admin Server logs for discovery messages + +3. **Confirm metadata is correct**: + ```bash + curl http://localhost:8761/eureka/apps/MY-APPLICATION | grep metadata + ``` + +### Incorrect Management URL + +Ensure management metadata is set: + +```yaml +eureka: + instance: + metadata-map: + management.port: ${management.server.port} + management.context-path: ${management.server.base-path} +``` + +### Health Check Failures + +Verify health endpoint is accessible: + +```bash +curl http://localhost:8081/actuator/health +``` + +Ensure Eureka health check path matches: + +```yaml +eureka: + instance: + health-check-url-path: /actuator/health +``` + +### Stale Instances + +Eureka may keep instances in registry after shutdown. Configure self-preservation: + +```yaml title="application.yml (Eureka Server)" +eureka: + server: + enableSelfPreservation: false # Disable for development + evictionIntervalTimerInMs: 5000 +``` + +## Best Practices + +1. **Set Appropriate Intervals**: Balance between freshness and load + ```yaml + eureka: + instance: + leaseRenewalIntervalInSeconds: 10 + client: + registryFetchIntervalSeconds: 5 + ``` + +2. **Use Startup Metadata**: Trigger updates on restart + ```yaml + eureka: + instance: + metadata-map: + startup: ${random.int} + ``` + +3. **Expose Necessary Endpoints**: Only expose what's needed + ```yaml + management: + endpoints: + web: + exposure: + include: health,info,metrics + ``` + +4. **Secure Metadata**: Use HTTPS for sensitive data + ```yaml + eureka: + client: + serviceUrl: + defaultZone: https://eureka:8761/eureka/ + ``` + +5. **Monitor Eureka Health**: Ensure Eureka is healthy + ```yaml + management: + health: + eureka: + enabled: true + ``` + +6. **Use Instance Filters**: Don't monitor everything + ```java + @Bean + public InstanceFilter filter() { + return registration -> !registration.getName().startsWith("internal-"); + } + ``` + +7. **Configure Timeouts**: Prevent hanging requests + ```yaml + eureka: + client: + eureka-server-connect-timeout-seconds: 5 + eureka-server-read-timeout-seconds: 8 + ``` + +## Complete Example + +See +the [spring-boot-admin-sample-eureka](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-eureka/) +project for a complete working example. + +## See Also + +- [Service Discovery](../03-client/40-service-discovery.md) - Service discovery overview +- [Eureka Sample](../09-samples/30-sample-eureka.md) - Detailed sample walkthrough +- [Security](../02-server/02-security.md) - Securing discovered services +- [Metadata](../03-client/30-metadata.md) - Working with metadata diff --git a/spring-boot-admin-docs/src/site/docs/04-integration/20-consul.md b/spring-boot-admin-docs/src/site/docs/04-integration/20-consul.md new file mode 100644 index 00000000000..70519a870fb --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/04-integration/20-consul.md @@ -0,0 +1,590 @@ +--- +sidebar_position: 20 +sidebar_custom_props: + icon: 'cloud' +--- + +# Consul Integration + +HashiCorp Consul is a service mesh solution that provides service discovery, health checking, and key-value storage. +This guide shows how to integrate Spring Boot Admin with Consul. + +## Overview + +With Consul integration: + +- Applications register with Consul +- Spring Boot Admin Server discovers applications via Consul +- Built-in health checks +- No Spring Boot Admin Client library required + +## Architecture + +```mermaid +flowchart LR + Apps[Applications] + Agent[Consul Agent] + Server[Consul Server
Service Registry] + Admin[Spring Boot Admin
Server] + + Apps -->|Register| Agent + Agent -->|Sync| Server + Admin -->|Discover| Server + Admin -.->|Monitor| Apps +``` + +## Setting Up Consul + +### Install Consul + +```bash +# macOS +brew install consul + +# Linux +wget https://releases.hashicorp.com/consul/1.17.0/consul_1.17.0_linux_amd64.zip +unzip consul_1.17.0_linux_amd64.zip +sudo mv consul /usr/local/bin/ + +# Docker +docker run -d --name=consul -p 8500:8500 consul:latest +``` + +### Start Consul + +```bash +# Development mode +consul agent -dev + +# Production mode +consul agent -server -bootstrap-expect=1 -data-dir=/tmp/consul +``` + +Access Consul UI at: `http://localhost:8500` + +## Configuring Spring Boot Admin Server + +### Add Dependencies + +```xml title="pom.xml" + + + de.codecentric + spring-boot-admin-starter-server + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.cloud + spring-cloud-starter-consul-discovery + + +``` + +### Enable Discovery + +```java title="SpringBootAdminConsulApplication.java" +import de.codecentric.boot.admin.server.config.EnableAdminServer; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@EnableDiscoveryClient +@EnableAdminServer +@SpringBootApplication +public class SpringBootAdminConsulApplication { + public static void main(String[] args) { + SpringApplication.run(SpringBootAdminConsulApplication.class, args); + } +} +``` + +### Configure Consul Client + +```yaml title="application.yml" +spring: + application: + name: spring-boot-admin-server + cloud: + consul: + host: localhost + port: 8500 + discovery: + preferIpAddress: true + health-check-interval: 10s + health-check-path: /actuator/health + instance-id: ${spring.application.name}:${random.value} + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS +``` + +### Ignore Consul Service + +Don't monitor Consul itself: + +```yaml title="application.yml" +spring: + boot: + admin: + discovery: + ignored-services: consul +``` + +## Configuring Client Applications + +### Add Dependencies + +```xml title="pom.xml" + + org.springframework.cloud + spring-cloud-starter-consul-discovery + +``` + +### Enable Discovery + +```java +@EnableDiscoveryClient +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +### Configure Application + +```yaml title="application.yml" +spring: + application: + name: my-application + cloud: + consul: + host: localhost + port: 8500 + discovery: + metadata: + management-context-path: ${management.server.base-path:/actuator} + health-path: ${management.endpoints.web.path-mapping.health:health} + +management: + endpoints: + web: + exposure: + include: "*" + base-path: /actuator + endpoint: + health: + show-details: ALWAYS +``` + +## Metadata in Consul + +### Important: No Dots in Keys + +:::warning +Consul **does not allow dots (`.`)** in metadata keys. Use dashes (`-`) or underscores (`_`) instead. +::: + +**Wrong**: + +```yaml +metadata: + user.name: admin # ❌ Won't work + user.password: secret # ❌ Won't work +``` + +**Correct**: + +```yaml +metadata: + user-name: admin # ✅ Works + user-password: secret # ✅ Works +``` + +### Adding Custom Metadata + +```yaml title="application.yml" +spring: + cloud: + consul: + discovery: + metadata: + management-context-path: /actuator + health-path: /ping + user-name: ${spring.security.user.name} + user-password: ${spring.security.user.password} + tags-environment: production + tags-region: us-east-1 + team: platform +``` + +### Tags + +Consul supports tags for grouping: + +```yaml title="application.yml" +spring: + cloud: + consul: + discovery: + tags: + - production + - us-east-1 + - platform-team +``` + +## Custom Management Configuration + +### Different Management Port + +```yaml title="application.yml" +server: + port: 8080 + +management: + server: + port: 9090 + endpoints: + web: + base-path: /management + +spring: + cloud: + consul: + discovery: + metadata: + management-port: 9090 + management-context-path: /management +``` + +### Custom Health Check Path + +```yaml title="application.yml" +management: + endpoints: + web: + path-mapping: + health: /ping + base-path: /actuator + +spring: + cloud: + consul: + discovery: + health-check-path: /actuator/ping + metadata: + health-path: /ping +``` + +## Health Checks + +### Default Health Check + +Consul automatically creates HTTP health check: + +```yaml +spring: + cloud: + consul: + discovery: + health-check-interval: 10s + health-check-timeout: 5s + health-check-path: /actuator/health +``` + +### Custom Health Check + +```yaml +spring: + cloud: + consul: + discovery: + health-check-url: https://my-app.example.com/actuator/health + health-check-interval: 15s + health-check-critical-timeout: 30s +``` + +### TTL Health Check + +Use TTL-based health check instead of HTTP: + +```yaml +spring: + cloud: + consul: + discovery: + health-check-interval: 10s + heartbeat: + enabled: true + ttl-value: 15 + ttl-unit: s +``` + +## Service Filtering + +### By Service Name + +```yaml title="application.yml (Admin Server)" +spring: + boot: + admin: + discovery: + ignored-services: consul,config-server +``` + +### By Metadata + +```java +@Bean +public InstanceFilter consulInstanceFilter() { + return registration -> { + // Only monitor services with 'monitor' tag + Map metadata = registration.getMetadata(); + return "true".equals(metadata.get("monitor")); + }; +} +``` + +### By Tags + +```java +@Bean +public InstanceFilter tagBasedFilter() { + return registration -> { + String tags = registration.getMetadata().get("tags"); + return tags != null && tags.contains("production"); + }; +} +``` + +## Securing Consul + +### ACL Token + +```yaml title="application.yml" +spring: + cloud: + consul: + host: localhost + port: 8500 + discovery: + acl-token: ${CONSUL_ACL_TOKEN} +``` + +### TLS/SSL + +```yaml title="application.yml" +spring: + cloud: + consul: + host: localhost + port: 8501 + scheme: https + tls: + enabled: true + cert-path: /path/to/cert.pem + key-path: /path/to/key.pem + ca-cert-path: /path/to/ca.pem +``` + +## Docker Compose Example + +```yaml title="docker-compose.yml" +version: '3' + +services: + consul: + image: consul:latest + ports: + - "8500:8500" + - "8600:8600/udp" + command: agent -server -ui -bootstrap-expect=1 -client=0.0.0.0 + + spring-boot-admin: + build: ./admin-server + ports: + - "8080:8080" + environment: + - SPRING_CLOUD_CONSUL_HOST=consul + depends_on: + - consul + + my-application: + build: ./my-app + ports: + - "8081:8081" + environment: + - SPRING_CLOUD_CONSUL_HOST=consul + depends_on: + - consul +``` + +## Kubernetes Integration + +For Kubernetes, use Consul Connect: + +```yaml title="application.yml" +spring: + cloud: + consul: + host: consul.service.consul + port: 8500 + discovery: + preferIpAddress: false + hostname: ${HOSTNAME}.my-app.default.svc.cluster.local + metadata: + k8s-namespace: ${POD_NAMESPACE:default} + k8s-pod: ${HOSTNAME} +``` + +## Troubleshooting + +### Service Not Appearing + +1. **Check Consul registration**: + ```bash + curl http://localhost:8500/v1/catalog/services + curl http://localhost:8500/v1/health/service/my-application + ``` + +2. **Verify health check**: + ```bash + consul catalog services + consul catalog nodes -service=my-application + ``` + +3. **Check metadata**: + ```bash + curl http://localhost:8500/v1/catalog/service/my-application | jq + ``` + +### Metadata Not Working + +Ensure no dots in keys: + +```yaml +# Wrong +metadata: + user.name: admin + +# Correct +metadata: + user-name: admin +``` + +### Health Check Failing + +Verify endpoint is accessible: + +```bash +curl http://localhost:8081/actuator/health +``` + +Check Consul health status: + +```bash +consul catalog nodes -service=my-application -detailed +``` + +### Deregistration Issues + +Configure critical timeout: + +```yaml +spring: + cloud: + consul: + discovery: + health-check-critical-timeout: 30s +``` + +## Best Practices + +1. **Use Instance IDs**: Ensure unique instance identifiers + ```yaml + spring: + cloud: + consul: + discovery: + instance-id: ${spring.application.name}:${random.value} + ``` + +2. **Configure Health Checks**: Set appropriate intervals + ```yaml + spring: + cloud: + consul: + discovery: + health-check-interval: 10s + health-check-critical-timeout: 1m + ``` + +3. **Use Metadata for Credentials**: Avoid hardcoding + ```yaml + spring: + cloud: + consul: + discovery: + metadata: + user-name: ${ACTUATOR_USER} + user-password: ${ACTUATOR_PASSWORD} + ``` + +4. **Prefer IP Address**: For container environments + ```yaml + spring: + cloud: + consul: + discovery: + preferIpAddress: true + ``` + +5. **Use Tags**: For service categorization + ```yaml + spring: + cloud: + consul: + discovery: + tags: + - production + - microservice + ``` + +6. **Enable Deregistration**: Clean up on shutdown + ```yaml + spring: + cloud: + consul: + discovery: + deregister: true + ``` + +7. **Monitor Consul Health**: Ensure Consul is operational + ```bash + consul members + consul info + ``` + +## Complete Example + +See +the [spring-boot-admin-sample-consul](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-consul/) +project for a complete working example. + +## See Also + +- [Service Discovery](../03-client/40-service-discovery.md) - Service discovery overview +- [Consul Sample](../09-samples/40-sample-consul.md) - Detailed sample walkthrough +- [Metadata](../03-client/30-metadata.md) - Working with metadata +- [Security](../02-server/02-security.md) - Securing discovered services diff --git a/spring-boot-admin-docs/src/site/docs/04-integration/30-zookeeper.md b/spring-boot-admin-docs/src/site/docs/04-integration/30-zookeeper.md new file mode 100644 index 00000000000..b6678d16f9a --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/04-integration/30-zookeeper.md @@ -0,0 +1,398 @@ +--- +sidebar_position: 30 +sidebar_custom_props: + icon: 'cloud' +--- + +# Zookeeper Integration + +Apache Zookeeper is a centralized coordination service that can be used for service discovery with Spring Cloud +Zookeeper. This guide shows how to integrate Spring Boot Admin with Zookeeper. + +## Overview + +With Zookeeper integration: + +- Applications register with Zookeeper +- Spring Boot Admin Server discovers applications via Zookeeper +- Automatic ephemeral node management +- No Spring Boot Admin Client library required + +## Setting Up Zookeeper + +### Install Zookeeper + +```bash +# macOS +brew install zookeeper + +# Linux +wget https://downloads.apache.org/zookeeper/zookeeper-3.8.3/apache-zookeeper-3.8.3-bin.tar.gz +tar -xzf apache-zookeeper-3.8.3-bin.tar.gz +cd apache-zookeeper-3.8.3-bin + +# Docker +docker run -d --name zookeeper -p 2181:2181 zookeeper:latest +``` + +### Start Zookeeper + +```bash +# Direct +zkServer start + +# Docker +docker start zookeeper +``` + +Verify Zookeeper is running: + +```bash +echo ruok | nc localhost 2181 +# Should respond with: imok +``` + +## Configuring Spring Boot Admin Server + +### Add Dependencies + +```xml title="pom.xml" + + + de.codecentric + spring-boot-admin-starter-server + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.cloud + spring-cloud-starter-zookeeper-discovery + + +``` + +### Enable Discovery + +```java title="SpringBootAdminZookeeperApplication.java" +import de.codecentric.boot.admin.server.config.EnableAdminServer; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@EnableDiscoveryClient +@EnableAdminServer +@SpringBootApplication +public class SpringBootAdminZookeeperApplication { + public static void main(String[] args) { + SpringApplication.run(SpringBootAdminZookeeperApplication.class, args); + } +} +``` + +### Configure Zookeeper Client + +```yaml title="application.yml" +spring: + application: + name: spring-boot-admin-server + cloud: + zookeeper: + connect-string: localhost:2181 + discovery: + enabled: true + register: true + root: /services + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS +``` + +## Configuring Client Applications + +### Add Dependencies + +```xml title="pom.xml" + + org.springframework.cloud + spring-cloud-starter-zookeeper-discovery + +``` + +### Enable Discovery + +```java +@EnableDiscoveryClient +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +### Configure Application + +```yaml title="application.yml" +spring: + application: + name: my-application + cloud: + zookeeper: + connect-string: localhost:2181 + discovery: + enabled: true + register: true + metadata: + management.context-path: /actuator + user.name: ${spring.security.user.name} + user.password: ${spring.security.user.password} + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS +``` + +## Metadata Configuration + +### Adding Custom Metadata + +```yaml title="application.yml" +spring: + cloud: + zookeeper: + discovery: + metadata: + management.context-path: /actuator + user.name: admin + user.password: secret + tags.environment: production + tags.region: us-east-1 + team: platform + version: 1.0.0 +``` + +### Management Port + +```yaml title="application.yml" +server: + port: 8080 + +management: + server: + port: 9090 + endpoints: + web: + base-path: /actuator + +spring: + cloud: + zookeeper: + discovery: + metadata: + management.port: 9090 + management.context-path: /actuator +``` + +## Instance Configuration + +### Instance ID + +```yaml +spring: + cloud: + zookeeper: + discovery: + instance-id: ${spring.application.name}:${random.value} +``` + +### Prefer IP Address + +```yaml +spring: + cloud: + zookeeper: + discovery: + preferIpAddress: true +``` + +### Custom Service Name + +```yaml +spring: + cloud: + zookeeper: + discovery: + serviceName: custom-service-name +``` + +## Connection Configuration + +### Connection Timeout + +```yaml +spring: + cloud: + zookeeper: + connect-string: localhost:2181 + max-retries: 10 + max-sleep-ms: 500 + connection-timeout: 15000 + session-timeout: 60000 +``` + +### Multiple Zookeeper Servers + +```yaml +spring: + cloud: + zookeeper: + connect-string: zk1:2181,zk2:2181,zk3:2181 +``` + +### Zookeeper Path + +```yaml +spring: + cloud: + zookeeper: + discovery: + root: /services + uriSpec: '{scheme}://{address}:{port}' +``` + +## Docker Compose Example + +```yaml title="docker-compose.yml" +version: '3' + +services: + zookeeper: + image: zookeeper:3.8 + ports: + - "2181:2181" + environment: + - ZOO_MY_ID=1 + + spring-boot-admin: + build: ./admin-server + ports: + - "8080:8080" + environment: + - SPRING_CLOUD_ZOOKEEPER_CONNECT_STRING=zookeeper:2181 + depends_on: + - zookeeper + + my-application: + build: ./my-app + ports: + - "8081:8081" + environment: + - SPRING_CLOUD_ZOOKEEPER_CONNECT_STRING=zookeeper:2181 + depends_on: + - zookeeper +``` + +## Troubleshooting + +### Connection Failures + +Check Zookeeper is running: + +```bash +echo ruok | nc localhost 2181 +``` + +Verify connection: + +```bash +zkCli.sh -server localhost:2181 +ls /services +``` + +### Service Not Appearing + +List registered services: + +```bash +zkCli.sh -server localhost:2181 +ls /services +get /services/my-application +``` + +### Session Timeout + +Increase session timeout: + +```yaml +spring: + cloud: + zookeeper: + session-timeout: 120000 # 2 minutes +``` + +## Best Practices + +1. **Configure Retries**: + ```yaml + spring: + cloud: + zookeeper: + max-retries: 10 + max-sleep-ms: 500 + ``` + +2. **Use Ensemble**: + ```yaml + spring: + cloud: + zookeeper: + connect-string: zk1:2181,zk2:2181,zk3:2181 + ``` + +3. **Set Appropriate Timeouts**: + ```yaml + spring: + cloud: + zookeeper: + connection-timeout: 15000 + session-timeout: 60000 + ``` + +4. **Use Instance IDs**: + ```yaml + spring: + cloud: + zookeeper: + discovery: + instance-id: ${spring.application.name}:${random.value} + ``` + +5. **Monitor Zookeeper Health**: + ```bash + echo mntr | nc localhost 2181 + ``` + +## Complete Example + +See +the [spring-boot-admin-sample-zookeeper](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/) +project for a complete working example. + +## See Also + +- [Service Discovery](../03-client/40-service-discovery.md) - Service discovery overview +- [Zookeeper Sample](../09-samples/50-sample-zookeeper.md) - Detailed sample walkthrough +- [Metadata](../03-client/30-metadata.md) - Working with metadata diff --git a/spring-boot-admin-docs/src/site/docs/04-integration/40-hazelcast.md b/spring-boot-admin-docs/src/site/docs/04-integration/40-hazelcast.md new file mode 100644 index 00000000000..acfd89befcf --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/04-integration/40-hazelcast.md @@ -0,0 +1,485 @@ +--- +sidebar_position: 40 +sidebar_custom_props: + icon: 'server' +--- + +# Hazelcast Clustering + +Hazelcast provides distributed data structures for clustering multiple Spring Boot Admin Server instances. This enables +high availability and shared state across servers. + +## Overview + +With Hazelcast clustering: + +- Multiple Admin Server instances share event store +- No single point of failure +- Automatic synchronization across nodes +- Distributed notifications + +## Architecture + +```mermaid +flowchart TB + Apps[Applications] + + subgraph Cluster["Hazelcast Cluster"] + HZ[(Hazelcast
Distributed Data)] + end + + subgraph Server1["Admin Server 1"] + ES1[Event Store] + end + + subgraph Server2["Admin Server 2"] + ES2[Event Store] + end + + Apps -->|Register| Server1 + Apps -->|Register| Server2 + + Server1 <-->|Sync| HZ + Server2 <-->|Sync| HZ + + ES1 -.->|Shared State| HZ + ES2 -.->|Shared State| HZ +``` + +## Why Hazelcast? + +- **High Availability**: No single point of failure +- **Scalability**: Add more Admin Server instances +- **Shared State**: All servers see the same application state +- **Distributed Events**: Events propagated across cluster +- **Simple Setup**: Minimal configuration required + +## Setting Up Hazelcast + +### Add Dependencies + +```xml title="pom.xml" + + + de.codecentric + spring-boot-admin-starter-server + + + org.springframework.boot + spring-boot-starter-webflux + + + com.hazelcast + hazelcast + + +``` + +### Configure Hazelcast + +```java title="HazelcastConfig.java" +import com.hazelcast.config.Config; +import com.hazelcast.config.MapConfig; +import com.hazelcast.config.MergePolicyConfig; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.map.IMap; +import com.hazelcast.spi.merge.PutIfAbsentMergePolicy; +import de.codecentric.boot.admin.server.domain.events.InstanceEvent; +import de.codecentric.boot.admin.server.domain.values.InstanceId; +import de.codecentric.boot.admin.server.eventstore.HazelcastEventStore; +import de.codecentric.boot.admin.server.eventstore.InstanceEventStore; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class HazelcastConfig { + + @Bean + public Config hazelcastConfig() { + MapConfig mapConfig = new MapConfig("spring-boot-admin-event-store") + .setBackupCount(1) + .setMergePolicyConfig(new MergePolicyConfig( + PutIfAbsentMergePolicy.class.getName(), 100)); + + Config config = new Config(); + config.addMapConfig(mapConfig); + config.setProperty("hazelcast.jmx", "true"); + + // Network configuration + config.getNetworkConfig() + .setPort(5701) + .setPortAutoIncrement(true) + .getJoin() + .getMulticastConfig() + .setEnabled(true); + + return config; + } + + @Bean + public HazelcastInstance hazelcastInstance(Config hazelcastConfig) { + return Hazelcast.newHazelcastInstance(hazelcastConfig); + } + + @Bean + public InstanceEventStore eventStore(HazelcastInstance hazelcastInstance) { + IMap> map = + hazelcastInstance.getMap("spring-boot-admin-event-store"); + return new HazelcastEventStore(100, map); + } +} +``` + +### Basic Configuration + +```yaml title="application.yml" +spring: + application: + name: spring-boot-admin-server + +hazelcast: + network: + port: 5701 + port-auto-increment: true + join: + multicast: + enabled: true + tcp-ip: + enabled: false +``` + +## Network Configuration + +### Multicast (Development) + +Automatic discovery using multicast: + +```java +config.getNetworkConfig() + .getJoin() + .getMulticastConfig() + .setEnabled(true) + .setMulticastGroup("224.2.2.3") + .setMulticastPort(54327); +``` + +```yaml +hazelcast: + network: + join: + multicast: + enabled: true + multicast-group: 224.2.2.3 + multicast-port: 54327 +``` + +### TCP/IP (Production) + +Explicit member list for production: + +```java +config.getNetworkConfig() + .getJoin() + .getMulticastConfig() + .setEnabled(false); + +config.getNetworkConfig() + .getJoin() + .getTcpIpConfig() + .setEnabled(true) + .addMember("192.168.1.100") + .addMember("192.168.1.101") + .addMember("192.168.1.102"); +``` + +```yaml +hazelcast: + network: + join: + multicast: + enabled: false + tcp-ip: + enabled: true + members: + - 192.168.1.100 + - 192.168.1.101 + - 192.168.1.102 +``` + +### Kubernetes + +For Kubernetes deployments: + +```xml + + com.hazelcast + hazelcast-kubernetes + +``` + +```java +config.getNetworkConfig() + .getJoin() + .getMulticastConfig() + .setEnabled(false); + +config.getNetworkConfig() + .getJoin() + .getKubernetesConfig() + .setEnabled(true) + .setProperty("namespace", "default") + .setProperty("service-name", "spring-boot-admin"); +``` + +## Event Store Configuration + +### Map Configuration + +```java +MapConfig mapConfig = new MapConfig("spring-boot-admin-event-store") + .setBackupCount(1) // Number of backup copies + .setAsyncBackupCount(0) // Async backups + .setTimeToLiveSeconds(0) // No expiration + .setMaxIdleSeconds(0) // No idle timeout + .setMergePolicyConfig(new MergePolicyConfig( + PutIfAbsentMergePolicy.class.getName(), 100)); +``` + +### Event Store Size + +Limit events per instance: + +```java +@Bean +public InstanceEventStore eventStore(HazelcastInstance hazelcastInstance) { + IMap> map = + hazelcastInstance.getMap("spring-boot-admin-event-store"); + return new HazelcastEventStore(500, map); // Max 500 events per instance +} +``` + +## High Availability Setup + +### Load Balancer Configuration + +```nginx +upstream spring_boot_admin { + least_conn; + server admin1:8080; + server admin2:8080; + server admin3:8080; +} + +server { + listen 80; + server_name admin.example.com; + + location / { + proxy_pass http://spring_boot_admin; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### Session Persistence + +Use sticky sessions for the UI: + +```nginx +upstream spring_boot_admin { + ip_hash; # Sticky sessions + server admin1:8080; + server admin2:8080; +} +``` + +Or use Spring Session: + +```xml + + org.springframework.session + spring-session-hazelcast + +``` + +```java +@EnableHazelcastHttpSession +@Configuration +public class SessionConfig { + // Hazelcast will be used for session storage +} +``` + +## Docker Compose Example + +```yaml title="docker-compose.yml" +version: '3' + +services: + admin1: + build: ./admin-server + ports: + - "8080:8080" + environment: + - HAZELCAST_MEMBERS=admin1,admin2,admin3 + - SERVER_PORT=8080 + + admin2: + build: ./admin-server + ports: + - "8081:8080" + environment: + - HAZELCAST_MEMBERS=admin1,admin2,admin3 + - SERVER_PORT=8080 + + admin3: + build: ./admin-server + ports: + - "8082:8080" + environment: + - HAZELCAST_MEMBERS=admin1,admin2,admin3 + - SERVER_PORT=8080 + + nginx: + image: nginx:alpine + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + depends_on: + - admin1 + - admin2 + - admin3 +``` + +## Monitoring Hazelcast + +### Management Center + +Use Hazelcast Management Center: + +```yaml +hazelcast: + management-center: + enabled: true + url: http://localhost:8083/mancenter +``` + +### JMX Monitoring + +Enable JMX: + +```java +config.setProperty("hazelcast.jmx", "true"); +``` + +### Health Checks + +Check cluster health: + +```java +@Component +public class HazelcastHealthCheck { + + private final HazelcastInstance hazelcastInstance; + + public HazelcastHealthCheck(HazelcastInstance hazelcastInstance) { + this.hazelcastInstance = hazelcastInstance; + } + + public boolean isHealthy() { + return hazelcastInstance.getCluster().getMembers().size() > 0; + } + + public int getClusterSize() { + return hazelcastInstance.getCluster().getMembers().size(); + } +} +``` + +## Troubleshooting + +### Split Brain + +Configure merge policy: + +```java +mapConfig.setMergePolicyConfig(new MergePolicyConfig( + PutIfAbsentMergePolicy.class.getName(), 100)); +``` + +### Members Not Joining + +1. **Check network connectivity**: + ```bash + telnet admin1 5701 + ``` + +2. **Verify multicast**: + ```bash + # Check if multicast is enabled + ip maddr show + ``` + +3. **Check logs**: + ``` + Hazelcast logs will show connection attempts + ``` + +### Performance Issues + +1. **Increase backup count**: + ```java + mapConfig.setBackupCount(2); + ``` + +2. **Use async backups**: + ```java + mapConfig.setAsyncBackupCount(1); + ``` + +3. **Monitor map size**: + ```java + IMap map = hazelcastInstance.getMap("spring-boot-admin-event-store"); + log.info("Map size: {}", map.size()); + ``` + +## Best Practices + +1. **Use TCP/IP in Production**: Multicast may not work in cloud environments + +2. **Configure Appropriate Backups**: + ```java + mapConfig.setBackupCount(1); // At least 1 backup + ``` + +3. **Set Event Store Limits**: + ```java + new HazelcastEventStore(500, map); // Reasonable limit + ``` + +4. **Monitor Cluster Health**: Use Management Center or JMX + +5. **Use Load Balancer**: Distribute traffic across servers + +6. **Enable Session Persistence**: For seamless failover + +7. **Configure Network Properly**: Especially in Kubernetes/Docker + +## Complete Example + +See +the [spring-boot-admin-sample-hazelcast](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-hazelcast/) +project for a complete working example. + +## See Also + +- [Clustering](../02-server/20-Clustering.mdx) - Clustering overview +- [Persistence](../02-server/30-persistence.md) - Event store details +- [Hazelcast Sample](../09-samples/60-sample-hazelcast.md) - Detailed sample walkthrough diff --git a/spring-boot-admin-docs/src/site/docs/04-integration/_category_.json b/spring-boot-admin-docs/src/site/docs/04-integration/_category_.json new file mode 100644 index 00000000000..85f9953e92b --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/04-integration/_category_.json @@ -0,0 +1,4 @@ +{ + "position": 4, + "label": "Integration", +} diff --git a/spring-boot-admin-docs/src/site/docs/04-integration/index.md b/spring-boot-admin-docs/src/site/docs/04-integration/index.md new file mode 100644 index 00000000000..267dd55ab8b --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/04-integration/index.md @@ -0,0 +1,115 @@ +--- +sidebar_position: 40 +sidebar_custom_props: + icon: 'puzzle' +--- + +# Integration + +Spring Boot Admin integrates seamlessly with various service discovery solutions and clustering technologies. This +section covers how to set up and configure these integrations. + +## Service Discovery + +Instead of using the Spring Boot Admin Client library, you can leverage Spring Cloud Discovery services to automatically +register applications: + +- **[Eureka](./10-eureka.md)** - Netflix Eureka service discovery +- **[Consul](./20-consul.md)** - HashiCorp Consul service mesh +- **[Zookeeper](./30-zookeeper.md)** - Apache Zookeeper coordination service + +### Benefits + +- No client library dependency required +- Automatic discovery of new instances +- Built-in health checking +- Service metadata support +- Load balancing integration + +## Clustering + +For high-availability deployments, Spring Boot Admin supports clustering: + +- **[Hazelcast](./40-hazelcast.md)** - Distributed event store and coordination + +### Benefits + +- Shared event store across cluster nodes +- No single point of failure +- Automatic synchronization +- Distributed notifications + +## Choosing an Integration + +### Use Service Discovery When: + +- You already have a service discovery infrastructure +- Running in a microservices environment +- Need automatic service registration +- Want to leverage existing service mesh features + +### Use Direct Client Registration When: + +- Simple deployment with few applications +- No service discovery infrastructure +- Need full control over registration +- Running in traditional environments + +### Use Clustering When: + +- Require high availability +- Multiple Admin Server instances +- Need shared state across servers +- Running in production with SLAs + +## Integration Patterns + +### Pattern 1: Service Discovery Only + +``` +Applications → Service Discovery (Eureka/Consul) ← Admin Server +``` + +Applications register with service discovery, Admin Server discovers them automatically. + +### Pattern 2: Direct Registration with Clustering + +``` +Applications → Admin Server 1 ←→ Hazelcast ←→ Admin Server 2 ← Applications +``` + +Applications use client library, Admin Servers share state via Hazelcast. + +### Pattern 3: Service Discovery with Clustering + +``` +Applications → Service Discovery ← Admin Server 1 ←→ Hazelcast ←→ Admin Server 2 +``` + +Combines automatic discovery with high availability. + +## Quick Comparison + +| Feature | Eureka | Consul | Zookeeper | Hazelcast | +|----------------------|-----------|----------------|--------------|------------| +| Type | Discovery | Discovery + KV | Coordination | Clustering | +| Setup Complexity | Medium | Medium | High | Low | +| Spring Cloud Support | Excellent | Excellent | Good | N/A | +| Health Checks | Built-in | Built-in | Custom | N/A | +| Metadata Support | Yes | Limited | Yes | N/A | +| HA | Yes | Yes | Yes | Yes | +| Persistence | In-memory | Persistent | Persistent | In-memory | + +## Getting Started + +1. Choose your integration based on your infrastructure +2. Follow the specific guide for setup instructions +3. Configure your applications appropriately +4. Test the integration in development +5. Deploy to production with monitoring + +## See Also + +- [Service Discovery](../03-client/40-service-discovery.md) - Client-side discovery configuration +- [Clustering](../02-server/20-Clustering.mdx) - Admin Server clustering details +- [Samples](../09-samples/) - Working example projects diff --git a/spring-boot-admin-docs/src/site/docs/05-security/10-server-authentication.md b/spring-boot-admin-docs/src/site/docs/05-security/10-server-authentication.md new file mode 100644 index 00000000000..1f1dc51e79f --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/05-security/10-server-authentication.md @@ -0,0 +1,903 @@ +--- +sidebar_position: 10 +sidebar_custom_props: + icon: 'shield' +--- + +# Server Authentication + +Secure your Spring Boot Admin Server using Spring Security to protect the UI and API endpoints. + +## Overview + +A secured Admin Server requires: + +1. **Spring Security dependency** +2. **SecurityFilterChain configuration** +3. **User credentials** (in-memory, database, LDAP, OAuth2, etc.) +4. **CSRF protection** with exemptions for client registration + +--- + +## Quick Start + +### 1. Add Spring Security Dependency + +**Maven**: + +```xml + + org.springframework.boot + spring-boot-starter-security + +``` + +**Gradle**: + +```gradle +implementation 'org.springframework.boot:spring-boot-starter-security' +``` + +### 2. Basic Configuration + +**Minimal security** with default Spring Boot user: + +```yaml +spring: + security: + user: + name: admin + password: ${ADMIN_PASSWORD} +``` + +This provides: + +- Form login at `/login` +- HTTP Basic authentication for API +- Single user with username `admin` + +### 3. Access the UI + +Navigate to `http://localhost:8080`, and you'll be redirected to the login page. + +--- + +## Complete Security Configuration + +For more control, use a custom `SecurityFilterChain`: + +```java +package com.example.admin; + +import java.util.UUID; + +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; + +import de.codecentric.boot.admin.server.config.AdminServerProperties; + +import static org.springframework.http.HttpMethod.DELETE; +import static org.springframework.http.HttpMethod.POST; + +@Configuration +public class SecurityConfig { + + private final AdminServerProperties adminServer; + + public SecurityConfig(AdminServerProperties adminServer) { + this.adminServer = adminServer; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // Redirect to login after successful authentication + SavedRequestAwareAuthenticationSuccessHandler successHandler = + new SavedRequestAwareAuthenticationSuccessHandler(); + successHandler.setTargetUrlParameter("redirectTo"); + successHandler.setDefaultTargetUrl(adminServer.path("/")); + + http + .authorizeHttpRequests(auth -> auth + // Permit access to static resources + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/assets/**"))) + .permitAll() + // Permit access to login page + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/login"))) + .permitAll() + // Permit Admin Server's own actuator endpoints + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/actuator/info"))) + .permitAll() + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/actuator/health"))) + .permitAll() + // Require authentication for all other requests + .anyRequest().authenticated() + ) + // Form login for UI + .formLogin(formLogin -> formLogin + .loginPage(adminServer.path("/login")) + .successHandler(successHandler) + ) + // Logout configuration + .logout(logout -> logout + .logoutUrl(adminServer.path("/logout")) + ) + // HTTP Basic for API clients + .httpBasic(Customizer.withDefaults()); + + // CSRF configuration (see CSRF Protection section) + http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class) + .csrf(csrf -> csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) + .ignoringRequestMatchers( + // Exempt client registration endpoints + PathPatternRequestMatcher.withDefaults() + .matcher(POST, adminServer.path("/instances")), + PathPatternRequestMatcher.withDefaults() + .matcher(DELETE, adminServer.path("/instances/*")), + // Exempt Admin Server's own actuator + PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/actuator/**")) + ) + ); + + // Remember-me functionality + http.rememberMe(rememberMe -> rememberMe + .key(UUID.randomUUID().toString()) + .tokenValiditySeconds(1209600) // 2 weeks + ); + + return http.build(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) { + UserDetails user = User.builder() + .username("admin") + .password(passwordEncoder.encode(System.getenv("ADMIN_PASSWORD"))) + .roles("ADMIN") + .build(); + + return new InMemoryUserDetailsManager(user); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} +``` + +### Custom CSRF Filter + +Required to make CSRF token available to JavaScript: + +```java +package com.example.admin; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.WebUtils; + +public class CustomCsrfFilter extends OncePerRequestFilter { + + public static final String CSRF_COOKIE_NAME = "XSRF-TOKEN"; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + + if (csrf != null) { + Cookie cookie = WebUtils.getCookie(request, CSRF_COOKIE_NAME); + String token = csrf.getToken(); + + if (cookie == null || token != null && !token.equals(cookie.getValue())) { + cookie = new Cookie(CSRF_COOKIE_NAME, token); + cookie.setPath("/"); + response.addCookie(cookie); + } + } + + filterChain.doFilter(request, response); + } +} +``` + +--- + +## Configuration Options + +### Context Path + +If your Admin Server uses a custom context path: + +```yaml +spring: + boot: + admin: + context-path: /admin +``` + +Adjust security matchers: + +```java +.requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/assets/**"))) +.permitAll() +``` + +The `adminServer.path()` method handles context path automatically. + +### Remember-Me + +Enable persistent sessions: + +```java +http.rememberMe(rememberMe -> rememberMe + .key(UUID.randomUUID().toString()) // Unique key + .tokenValiditySeconds(1209600) // 2 weeks + .rememberMeParameter("remember-me") // Form parameter name +) +``` + +**Note**: Remember-me requires a `UserDetailsService` bean. + +### Session Management + +Configure session behavior: + +```java +http.sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + .maximumSessions(1) // Max 1 session per user + .maxSessionsPreventsLogin(false) // Invalidate old session +) +``` + +--- + +## User Management + +### In-Memory Users + +Simple for development or small deployments: + +```java +@Bean +public InMemoryUserDetailsManager userDetailsService(PasswordEncoder encoder) { + UserDetails admin = User.builder() + .username("admin") + .password(encoder.encode(System.getenv("ADMIN_PASSWORD"))) + .roles("ADMIN") + .build(); + + UserDetails viewer = User.builder() + .username("viewer") + .password(encoder.encode(System.getenv("VIEWER_PASSWORD"))) + .roles("USER") + .build(); + + return new InMemoryUserDetailsManager(admin, viewer); +} +``` + +### Database Users + +Use `JdbcUserDetailsManager` for database-backed users: + +```java +@Bean +public JdbcUserDetailsManager userDetailsService(DataSource dataSource, + PasswordEncoder encoder) { + JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource); + + // Create default admin if not exists + if (!manager.userExists("admin")) { + UserDetails admin = User.builder() + .username("admin") + .password(encoder.encode(System.getenv("ADMIN_PASSWORD"))) + .roles("ADMIN") + .build(); + manager.createUser(admin); + } + + return manager; +} +``` + +**Database Schema**: + +```sql +CREATE TABLE users ( + username VARCHAR(50) NOT NULL PRIMARY KEY, + password VARCHAR(100) NOT NULL, + enabled BOOLEAN NOT NULL +); + +CREATE TABLE authorities ( + username VARCHAR(50) NOT NULL, + authority VARCHAR(50) NOT NULL, + FOREIGN KEY (username) REFERENCES users(username) +); + +CREATE UNIQUE INDEX ix_auth_username ON authorities (username, authority); +``` + +### LDAP Authentication + +Authenticate against an LDAP server: + +```java +@Bean +public SecurityFilterChain filterChain(HttpSecurity http, + AdminServerProperties adminServer) throws Exception { + http + .authorizeHttpRequests(/* ... */) + .formLogin(/* ... */) + .logout(/* ... */) + .httpBasic(Customizer.withDefaults()); + + return http.build(); +} + +@Configuration +public static class LdapConfig { + + @Bean + public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { + EmbeddedLdapServerContextSourceFactoryBean factory = + new EmbeddedLdapServerContextSourceFactoryBean(); + factory.setPort(8389); + return factory; + } + + @Bean + public AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) { + LdapBindAuthenticationManagerFactory factory = + new LdapBindAuthenticationManagerFactory(contextSource); + factory.setUserDnPatterns("uid={0},ou=people"); + factory.setUserDetailsContextMapper(new PersonContextMapper()); + return factory.createAuthenticationManager(); + } +} +``` + +**Configuration**: + +```yaml +spring: + ldap: + urls: ldap://ldap.company.com:389 + base: dc=company,dc=com + username: cn=admin,dc=company,dc=com + password: ${LDAP_PASSWORD} +``` + +### OAuth2 / OIDC + +Use OAuth2 for Single Sign-On (SSO): + +**Dependencies**: + +```xml + + org.springframework.boot + spring-boot-starter-oauth2-client + +``` + +**Configuration**: + +```yaml +spring: + security: + oauth2: + client: + registration: + keycloak: + client-id: spring-boot-admin + client-secret: ${OAUTH2_CLIENT_SECRET} + scope: openid,profile,email + authorization-grant-type: authorization_code + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + provider: + keycloak: + issuer-uri: https://keycloak.company.com/realms/main +``` + +**Security Configuration**: + +```java +@Bean +public SecurityFilterChain filterChain(HttpSecurity http, + AdminServerProperties adminServer) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/assets/**"))) + .permitAll() + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/login"))) + .permitAll() + .anyRequest().authenticated() + ) + .oauth2Login(oauth2 -> oauth2 + .loginPage(adminServer.path("/login")) + ) + .logout(logout -> logout + .logoutUrl(adminServer.path("/logout")) + .logoutSuccessUrl(adminServer.path("/")) + ); + + // CSRF and other configurations... + + return http.build(); +} +``` + +--- + +## Role-Based Access Control + +Restrict access by roles: + +```java +@Bean +public SecurityFilterChain filterChain(HttpSecurity http, + AdminServerProperties adminServer) throws Exception { + http.authorizeHttpRequests(auth -> auth + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/assets/**"))) + .permitAll() + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/login"))) + .permitAll() + // Only ADMIN can delete instances + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(DELETE, adminServer.path("/instances/*"))) + .hasRole("ADMIN") + // Only ADMIN can access logfile endpoint + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/instances/*/actuator/logfile"))) + .hasRole("ADMIN") + // USER and ADMIN can view everything else + .anyRequest().hasAnyRole("USER", "ADMIN") + ) + .formLogin(formLogin -> formLogin.loginPage(adminServer.path("/login"))) + .httpBasic(Customizer.withDefaults()); + + return http.build(); +} +``` + +**Create users with different roles**: + +```java +@Bean +public InMemoryUserDetailsManager userDetailsService(PasswordEncoder encoder) { + UserDetails admin = User.builder() + .username("admin") + .password(encoder.encode(System.getenv("ADMIN_PASSWORD"))) + .roles("ADMIN") + .build(); + + UserDetails viewer = User.builder() + .username("viewer") + .password(encoder.encode(System.getenv("VIEWER_PASSWORD"))) + .roles("USER") + .build(); + + return new InMemoryUserDetailsManager(admin, viewer); +} +``` + +--- + +## HTTP vs HTTPS + +### Local Development (HTTP) + +For local development, HTTP is acceptable: + +```yaml +server: + port: 8080 +``` + +### HTTPS Configuration + +Enable HTTPS for secure communication: + +```yaml +server: + port: 8443 + ssl: + enabled: true + key-store: classpath:keystore.p12 + key-store-password: ${KEYSTORE_PASSWORD} + key-store-type: PKCS12 + key-alias: spring-boot-admin +``` + +**Generate keystore**: + +```bash +keytool -genkeypair -alias spring-boot-admin \ + -keyalg RSA -keysize 2048 \ + -storetype PKCS12 \ + -keystore keystore.p12 \ + -validity 3650 \ + -storepass changeit +``` + +**Update Admin Client configuration**: + +```yaml +spring: + boot: + admin: + client: + url: https://admin-server:8443 +``` + +--- + +## Reverse Proxy Setup + +### Behind Nginx + +**Nginx Configuration**: + +```nginx +server { + listen 80; + server_name admin.company.com; + + location / { + proxy_pass http://localhost:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +**Admin Server Configuration**: + +```yaml +server: + forward-headers-strategy: native + +spring: + boot: + admin: + ui: + public-url: https://admin.company.com +``` + +### Behind Apache + +**Apache Configuration**: + +```apache + + ServerName admin.company.com + + ProxyPreserveHost On + ProxyPass / http://localhost:8080/ + ProxyPassReverse / http://localhost:8080/ + + RequestHeader set X-Forwarded-Proto "https" + RequestHeader set X-Forwarded-Port "443" + +``` + +--- + +## Security Headers + +Add security headers to protect against common attacks: + +```java +@Bean +public SecurityFilterChain filterChain(HttpSecurity http, + AdminServerProperties adminServer) throws Exception { + http + .headers(headers -> headers + // Content Security Policy + .contentSecurityPolicy(csp -> csp + .policyDirectives("default-src 'self'; " + + "script-src 'self' 'unsafe-inline'; " + + "style-src 'self' 'unsafe-inline'; " + + "img-src 'self' data:; " + + "font-src 'self' data:") + ) + // Frame options + .frameOptions(frame -> frame.sameOrigin()) + // XSS protection + .xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)) + // HSTS + .httpStrictTransportSecurity(hsts -> hsts + .includeSubDomains(true) + .maxAgeInSeconds(31536000) + ) + ); + + // Other configurations... + + return http.build(); +} +``` + +--- + +## Multiple Authentication Methods + +Support both form login and HTTP Basic: + +```java +@Bean +public SecurityFilterChain filterChain(HttpSecurity http, + AdminServerProperties adminServer) throws Exception { + http + .authorizeHttpRequests(/* ... */) + .formLogin(formLogin -> formLogin + .loginPage(adminServer.path("/login")) + ) + .httpBasic(Customizer.withDefaults()) + .logout(logout -> logout + .logoutUrl(adminServer.path("/logout")) + ); + + return http.build(); +} +``` + +- **Form login**: For browser-based UI access +- **HTTP Basic**: For API clients, scripts, monitoring tools + +--- + +## Troubleshooting + +### Issue: Login page not loading + +**Cause**: Assets blocked by security configuration. + +**Solution**: Permit `/assets/**`: + +```java +.requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/assets/**"))) +.permitAll() +``` + +### Issue: Infinite redirect loop + +**Cause**: Login page requires authentication. + +**Solution**: Permit `/login`: + +```java +.requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/login"))) +.permitAll() +``` + +### Issue: Clients cannot register + +**Cause**: CSRF protection blocking `/instances` endpoint. + +**Solution**: Exempt client registration endpoints: + +```java +.csrf(csrf -> csrf + .ignoringRequestMatchers( + PathPatternRequestMatcher.withDefaults() + .matcher(POST, adminServer.path("/instances")), + PathPatternRequestMatcher.withDefaults() + .matcher(DELETE, adminServer.path("/instances/*")) + ) +) +``` + +### Issue: Remember-me not working + +**Cause**: No `UserDetailsService` configured. + +**Solution**: Add `UserDetailsService` bean: + +```java +@Bean +public InMemoryUserDetailsManager userDetailsService(PasswordEncoder encoder) { + // ... +} +``` + +### Issue: 401 on API requests + +**Cause**: API client not providing credentials. + +**Solution**: Use HTTP Basic authentication: + +```bash +curl -u admin:password http://localhost:8080/instances +``` + +--- + +## Complete Example + +```java +package com.example.admin; + +import java.util.UUID; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; + +import de.codecentric.boot.admin.server.config.AdminServerProperties; +import de.codecentric.boot.admin.server.config.EnableAdminServer; + +import static org.springframework.http.HttpMethod.DELETE; +import static org.springframework.http.HttpMethod.POST; + +@EnableAdminServer +@Configuration +public class AdminServerConfig { + + private final AdminServerProperties adminServer; + + public AdminServerConfig(AdminServerProperties adminServer) { + this.adminServer = adminServer; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + SavedRequestAwareAuthenticationSuccessHandler successHandler = + new SavedRequestAwareAuthenticationSuccessHandler(); + successHandler.setTargetUrlParameter("redirectTo"); + successHandler.setDefaultTargetUrl(adminServer.path("/")); + + http + .authorizeHttpRequests(auth -> auth + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/assets/**"))) + .permitAll() + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/login"))) + .permitAll() + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/actuator/info"))) + .permitAll() + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/actuator/health"))) + .permitAll() + .anyRequest().authenticated() + ) + .formLogin(formLogin -> formLogin + .loginPage(adminServer.path("/login")) + .successHandler(successHandler) + ) + .logout(logout -> logout + .logoutUrl(adminServer.path("/logout")) + ) + .httpBasic(Customizer.withDefaults()); + + http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class) + .csrf(csrf -> csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) + .ignoringRequestMatchers( + PathPatternRequestMatcher.withDefaults() + .matcher(POST, adminServer.path("/instances")), + PathPatternRequestMatcher.withDefaults() + .matcher(DELETE, adminServer.path("/instances/*")), + PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/actuator/**")) + ) + ); + + http.rememberMe(rememberMe -> rememberMe + .key(UUID.randomUUID().toString()) + .tokenValiditySeconds(1209600) + ); + + return http.build(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) { + UserDetails admin = User.builder() + .username("admin") + .password(passwordEncoder.encode(System.getenv("ADMIN_PASSWORD"))) + .roles("ADMIN") + .build(); + + UserDetails viewer = User.builder() + .username("viewer") + .password(passwordEncoder.encode(System.getenv("VIEWER_PASSWORD"))) + .roles("USER") + .build(); + + return new InMemoryUserDetailsManager(admin, viewer); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} +``` + +**application.yml**: + +```yaml +spring: + application: + name: spring-boot-admin-server + + boot: + admin: + context-path: /admin + ui: + title: "Production Monitor" + +server: + port: 8443 + ssl: + enabled: true + key-store: classpath:keystore.p12 + key-store-password: ${KEYSTORE_PASSWORD} + key-store-type: PKCS12 +``` + +--- + +## See Also + +- [Actuator Security](./20-actuator-security.md) - Secure client actuator endpoints +- [CSRF Protection](./30-csrf-protection.md) - Detailed CSRF configuration diff --git a/spring-boot-admin-docs/src/site/docs/05-security/20-actuator-security.md b/spring-boot-admin-docs/src/site/docs/05-security/20-actuator-security.md new file mode 100644 index 00000000000..b6bf485443d --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/05-security/20-actuator-security.md @@ -0,0 +1,923 @@ +--- +sidebar_position: 20 +sidebar_custom_props: + icon: 'shield' +--- + +# Actuator Security + +Secure your client application's actuator endpoints and configure Spring Boot Admin Server to access them. + +## Overview + +When client applications expose actuator endpoints, they should be secured. The Admin Server needs credentials to access +these secured endpoints. + +```mermaid +graph TD + A["Spring Boot Admin Server
Needs credentials to access
secured actuator endpoints"] -->|HTTP Basic Auth
user.name + user.password| B["Client Application
Secured actuator endpoints:
/actuator/health
/actuator/metrics
/actuator/env"] +``` + +--- + +## Quick Start + +### 1. Client: Secure Actuator Endpoints + +Add Spring Security to your client application: + +**Maven**: + +```xml + + org.springframework.boot + spring-boot-starter-security + +``` + +**Gradle**: + +```gradle +implementation 'org.springframework.boot:spring-boot-starter-security' +``` + +**application.yml**: + +```yaml +spring: + security: + user: + name: actuator + password: ${ACTUATOR_PASSWORD} + +management: + endpoints: + web: + exposure: + include: "*" +``` + +### 2. Client: Share Credentials with Admin Server + +Pass credentials via metadata: + +```yaml +spring: + boot: + admin: + client: + url: http://admin-server:8080 + instance: + metadata: + user.name: actuator + user.password: ${ACTUATOR_PASSWORD} +``` + +### 3. Server: Enable Instance Authentication + +**application.yml**: + +```yaml +spring: + boot: + admin: + instance-auth: + enabled: true +``` + +The Admin Server will automatically use credentials from instance metadata to access actuator endpoints. + +--- + +## Client Configuration + +### Basic Actuator Security + +Simplest approach using default Spring Security user: + +```yaml +spring: + application: + name: my-service + + security: + user: + name: actuator + password: ${ACTUATOR_PASSWORD} + + boot: + admin: + client: + url: http://admin-server:8080 + instance: + metadata: + user.name: ${spring.security.user.name} + user.password: ${spring.security.user.password} + +management: + endpoints: + web: + exposure: + include: health,info,metrics,env,loggers +``` + +### Custom Security Configuration + +For more control, create a custom `SecurityFilterChain`: + +```java +package com.example.myservice; + +import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class ActuatorSecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + // Permit health endpoint for load balancers + .requestMatchers(EndpointRequest.to("health")).permitAll() + // Secure all other actuator endpoints + .requestMatchers(EndpointRequest.toAnyEndpoint()).authenticated() + // Allow application endpoints + .anyRequest().permitAll() + ) + // Use HTTP Basic for actuator + .httpBasic(Customizer.withDefaults()) + // Disable CSRF for stateless API + .csrf(csrf -> csrf.disable()); + + return http.build(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService(PasswordEncoder encoder) { + UserDetails actuator = User.builder() + .username("actuator") + .password(encoder.encode(System.getenv("ACTUATOR_PASSWORD"))) + .roles("ACTUATOR") + .build(); + + return new InMemoryUserDetailsManager(actuator); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} +``` + +### Different Actuator and Application Security + +Separate security for actuator and application: + +```java +@Configuration +@Order(1) // Higher precedence +public class ActuatorSecurityConfig { + + @Bean + @Order(1) + public SecurityFilterChain actuatorFilterChain(HttpSecurity http) throws Exception { + http + .securityMatcher(EndpointRequest.toAnyEndpoint()) + .authorizeHttpRequests(auth -> auth + .requestMatchers(EndpointRequest.to("health")).permitAll() + .anyRequest().hasRole("ACTUATOR") + ) + .httpBasic(Customizer.withDefaults()) + .csrf(csrf -> csrf.disable()); + + return http.build(); + } + + @Bean + public InMemoryUserDetailsManager actuatorUserDetailsService(PasswordEncoder encoder) { + UserDetails actuator = User.builder() + .username("actuator") + .password(encoder.encode(System.getenv("ACTUATOR_PASSWORD"))) + .roles("ACTUATOR") + .build(); + + return new InMemoryUserDetailsManager(actuator); + } +} + +@Configuration +@Order(2) // Lower precedence +public class ApplicationSecurityConfig { + + @Bean + @Order(2) + public SecurityFilterChain applicationFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/public/**").permitAll() + .anyRequest().authenticated() + ) + .oauth2Login(Customizer.withDefaults()); + + return http.build(); + } +} +``` + +### Actuator on Separate Port + +Run actuator on a different port for isolation: + +```yaml +management: + server: + port: 8081 # Separate management port + endpoints: + web: + exposure: + include: "*" + +spring: + security: + user: + name: actuator + password: ${ACTUATOR_PASSWORD} + + boot: + admin: + client: + instance: + # Admin Server will auto-detect management port + # Or specify explicitly: + management-base-url: http://localhost:8081 + metadata: + user.name: actuator + user.password: ${ACTUATOR_PASSWORD} +``` + +**Security configuration**: + +```java +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + // No actuator endpoints on main port + .anyRequest().permitAll() + ) + .csrf(csrf -> csrf.disable()); + + return http.build(); + } +} +``` + +On port 8081, actuator endpoints are secured with Spring Security's default security. + +--- + +## Server Configuration + +### Enable Instance Authentication + +```yaml +spring: + boot: + admin: + instance-auth: + enabled: true +``` + +Admin Server will: + +1. Check instance metadata for `user.name` and `user.password` +2. Use these credentials to access actuator endpoints via HTTP Basic + +### Default Credentials + +Set default credentials for all instances: + +```yaml +spring: + boot: + admin: + instance-auth: + enabled: true + default-user-name: actuator + default-password: ${DEFAULT_ACTUATOR_PASSWORD} +``` + +Instances can override via metadata. + +### Per-Service Credentials + +Configure different credentials for each service: + +```yaml +spring: + boot: + admin: + instance-auth: + enabled: true + service-map: + # Service name from spring.application.name + my-service: + user-name: my-service-actuator + user-password: ${MY_SERVICE_PASSWORD} + another-service: + user-name: another-actuator + user-password: ${ANOTHER_SERVICE_PASSWORD} + + # Fallback for services not in service-map + default-user-name: default-actuator + default-password: ${DEFAULT_PASSWORD} +``` + +**Client (my-service)**: + +```yaml +spring: + application: + name: my-service + + security: + user: + name: my-service-actuator + password: ${MY_SERVICE_PASSWORD} +``` + +--- + +## Credential Strategies + +### Strategy 1: Metadata (Recommended) + +**Client passes credentials in metadata**: + +```yaml +spring: + boot: + admin: + client: + instance: + metadata: + user.name: actuator + user.password: ${ACTUATOR_PASSWORD} +``` + +**Server uses metadata automatically**: + +```yaml +spring: + boot: + admin: + instance-auth: + enabled: true +``` + +**Pros**: + +- Client controls its own credentials +- Each instance can have unique credentials +- Server automatically picks up credentials + +**Cons**: + +- Credentials visible in instance metadata (sanitized by default) +- Requires client configuration + +### Strategy 2: Server-Side Configuration + +**Server has all credentials**: + +```yaml +spring: + boot: + admin: + instance-auth: + enabled: true + service-map: + service-a: + user-name: service-a-user + user-password: ${SERVICE_A_PASSWORD} +``` + +**Client just secures actuator**: + +```yaml +spring: + security: + user: + name: service-a-user + password: ${SERVICE_A_PASSWORD} +``` + +**Pros**: + +- Centralized credential management +- Client configuration simpler + +**Cons**: + +- Server must know all client credentials +- Harder to scale with many services + +### Strategy 3: Default Credentials + +**All clients use same credentials**: + +**Server**: + +```yaml +spring: + boot: + admin: + instance-auth: + enabled: true + default-user-name: actuator + default-password: ${ACTUATOR_PASSWORD} +``` + +**All Clients**: + +```yaml +spring: + security: + user: + name: actuator + password: ${ACTUATOR_PASSWORD} +``` + +**Pros**: + +- Simplest to configure +- Uniform across all services + +**Cons**: + +- Single credentials compromise affects all services +- Less secure + +--- + +## Limiting Exposed Endpoints + +Only expose necessary endpoints: + +```yaml +management: + endpoints: + web: + exposure: + include: health,info,metrics,env,loggers +``` + +Or exclude specific endpoints: + +```yaml +management: + endpoints: + web: + exposure: + include: "*" + exclude: heapdump,threaddump +``` + +**Health endpoint details**: + +```yaml +management: + endpoint: + health: + show-details: when-authorized + roles: ACTUATOR +``` + +--- + +## Metadata Sanitization + +By default, credentials in metadata are sanitized: + +```yaml +spring: + boot: + admin: + metadata-keys-to-sanitize: + - ".*password$" + - ".*secret$" + - ".*key$" + - ".*token$" + - ".*credentials.*" +``` + +Metadata `user.password` will appear as `******` in responses, but the server still uses it internally. + +--- + +## Service Discovery + +When using service discovery (Eureka, Consul, etc.), credentials can be set via metadata: + +**Eureka**: + +```yaml +eureka: + instance: + metadata-map: + user.name: actuator + user.password: ${ACTUATOR_PASSWORD} +``` + +**Consul**: + +```yaml +spring: + cloud: + consul: + discovery: + metadata: + user.name: actuator + user.password: ${ACTUATOR_PASSWORD} +``` + +**Kubernetes ConfigMap**: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-service-config +data: + application.yml: | + spring: + boot: + admin: + client: + instance: + metadata: + user.name: actuator + user.password: ${ACTUATOR_PASSWORD} +``` + +--- + +## TLS/SSL for Actuator + +Use HTTPS for actuator endpoints: + +```yaml +management: + server: + port: 8443 + ssl: + enabled: true + key-store: classpath:actuator-keystore.p12 + key-store-password: ${KEYSTORE_PASSWORD} + key-store-type: PKCS12 + +spring: + boot: + admin: + client: + instance: + management-base-url: https://localhost:8443 + metadata: + user.name: actuator + user.password: ${ACTUATOR_PASSWORD} +``` + +**Generate keystore**: + +```bash +keytool -genkeypair -alias actuator \ + -keyalg RSA -keysize 2048 \ + -storetype PKCS12 \ + -keystore actuator-keystore.p12 \ + -validity 3650 \ + -storepass changeit +``` + +--- + +## Examples + +### Example 1: Development (No Security) + +**Client**: + +```yaml +management: + endpoints: + web: + exposure: + include: "*" + +spring: + boot: + admin: + client: + url: http://localhost:8080 +``` + +No Spring Security dependency, all endpoints open. + +### Example 2: Full Security Setup + +**Client**: + +```yaml +spring: + application: + name: payment-service + + security: + user: + name: ${ACTUATOR_USER} + password: ${ACTUATOR_PASSWORD} + + boot: + admin: + client: + url: https://admin.company.com + username: ${ADMIN_CLIENT_USER} + password: ${ADMIN_CLIENT_PASSWORD} + instance: + service-base-url: https://payment.company.com + management-base-url: https://payment.company.com:8443 + metadata: + user.name: ${ACTUATOR_USER} + user.password: ${ACTUATOR_PASSWORD} + tags: + environment: production + +management: + server: + port: 8443 + ssl: + enabled: true + key-store: classpath:keystore.p12 + key-store-password: ${KEYSTORE_PASSWORD} + + endpoints: + web: + exposure: + include: health,info,metrics,env,loggers + + endpoint: + health: + show-details: when-authorized +``` + +**Server**: + +```yaml +spring: + boot: + admin: + instance-auth: + enabled: true + # Uses credentials from instance metadata +``` + +### Example 3: Kubernetes with Secrets + +**Secret**: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: actuator-credentials +type: Opaque +stringData: + username: actuator + password: secure-password-123 +``` + +**Deployment**: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-service +spec: + template: + spec: + containers: + - name: my-service + image: my-service:latest + env: + - name: ACTUATOR_USER + valueFrom: + secretKeyRef: + name: actuator-credentials + key: username + - name: ACTUATOR_PASSWORD + valueFrom: + secretKeyRef: + name: actuator-credentials + key: password + ports: + - containerPort: 8080 + - containerPort: 8081 # Actuator +``` + +**application.yml** (in ConfigMap): + +```yaml +spring: + security: + user: + name: ${ACTUATOR_USER} + password: ${ACTUATOR_PASSWORD} + + boot: + admin: + client: + instance: + metadata: + user.name: ${ACTUATOR_USER} + user.password: ${ACTUATOR_PASSWORD} + +management: + server: + port: 8081 +``` + +### Example 4: Multiple Environments + +**application.yml** (common): + +```yaml +spring: + boot: + admin: + client: + instance: + metadata: + user.name: ${ACTUATOR_USER:actuator} + user.password: ${ACTUATOR_PASSWORD} + +management: + endpoints: + web: + exposure: + include: "*" +``` + +**application-dev.yml**: + +```yaml +spring: + security: + user: + name: actuator + password: dev-password + + boot: + admin: + client: + url: http://localhost:8080 +``` + +**application-prod.yml**: + +```yaml +spring: + security: + user: + name: ${ACTUATOR_USER} + password: ${ACTUATOR_PASSWORD} + + boot: + admin: + client: + url: https://admin.company.com + username: ${ADMIN_CLIENT_USER} + password: ${ADMIN_CLIENT_PASSWORD} + +management: + endpoints: + web: + exposure: + include: health,info,metrics,env,loggers + endpoint: + health: + show-details: when-authorized +``` + +--- + +## Troubleshooting + +### Issue: 401 Unauthorized on actuator endpoints + +**Cause**: Admin Server doesn't have valid credentials. + +**Check**: + +1. Instance metadata contains credentials: + + ```bash + curl http://admin-server:8080/instances/{id} | jq '.metadata' + ``` + +2. Credentials match actuator configuration: + + ```bash + curl -u actuator:password http://client:8080/actuator/health + ``` + +**Solution**: Add credentials to metadata: + +```yaml +spring: + boot: + admin: + client: + instance: + metadata: + user.name: actuator + user.password: ${ACTUATOR_PASSWORD} +``` + +### Issue: Admin Server shows "Unavailable" + +**Cause**: Cannot access health endpoint. + +**Check**: + +```bash +curl -u actuator:password http://client:8080/actuator/health +``` + +**Solution**: Verify: + +1. Health endpoint is exposed +2. Credentials are correct +3. Health URL is accessible from Admin Server + +### Issue: Some endpoints work, others return 401 + +**Cause**: Different security rules for different endpoints. + +**Solution**: Ensure all actuator endpoints have same security: + +```java +http.authorizeHttpRequests(auth -> auth + .requestMatchers(EndpointRequest.toAnyEndpoint()).authenticated() +) +``` + +### Issue: Metadata shows credentials in plain text + +**Expected**: Credentials should be sanitized as `******`. + +**Check sanitization patterns**: + +```yaml +spring: + boot: + admin: + metadata-keys-to-sanitize: + - ".*password$" +``` + +This is working correctly if API responses show `******` for `user.password`, even though the server uses the real value +internally. + +--- + +## Best Practices + +1. **Use Strong Passwords**: Generate secure random passwords +2. **Environment Variables**: Never hardcode credentials +3. **Limit Exposure**: Only expose necessary actuator endpoints +4. **Use HTTPS**: Encrypt actuator traffic with TLS +5. **Separate Port**: Consider separate management port for isolation +6. **Role-Based Access**: Use roles for fine-grained control +7. **Monitor Access**: Log actuator access attempts +8. **Rotate Credentials**: Regularly update actuator passwords + +--- + +## See Also + +- [Server Authentication](./10-server-authentication.md) - Secure Admin Server +- [CSRF Protection](./30-csrf-protection.md) - Configure CSRF tokens diff --git a/spring-boot-admin-docs/src/site/docs/05-security/30-csrf-protection.md b/spring-boot-admin-docs/src/site/docs/05-security/30-csrf-protection.md new file mode 100644 index 00000000000..47483fbe9da --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/05-security/30-csrf-protection.md @@ -0,0 +1,761 @@ +--- +sidebar_position: 30 +sidebar_custom_props: + icon: 'shield' +--- + +# CSRF Protection + +Configure Cross-Site Request Forgery (CSRF) protection for Spring Boot Admin while allowing client registration. + +## Overview + +Spring Boot Admin Server needs CSRF protection for the web UI, but must exempt certain endpoints: + +- `/instances` - Client registration endpoint (POST) +- `/instances/*` - Client deregistration endpoint (DELETE) +- `/actuator/**` - Admin Server's own actuator endpoints + +```mermaid +graph TB + A["**Browser Admin UI**
• Sends CSRF token in requests
• Token stored in XSRF-TOKEN cookie
• Angular/React reads cookie and sends X-XSRF-TOKEN header"] --> B["**Spring Boot Admin Server**
• Validates CSRF token for UI requests
• Exempts /instances endpoint client registration
• Exempts /actuator/** health checks"] + C["**Client Application**
• Registers without CSRF token
• Uses HTTP POST to /instances"] --> B +``` + +--- + +## Why CSRF Protection? + +CSRF attacks trick authenticated users into performing unwanted actions: + +1. User logs into Admin Server in their browser +2. User visits malicious website +3. Malicious website sends request to Admin Server using user's session +4. Without CSRF protection, the request succeeds + +**CSRF tokens prevent this**: + +- Each request requires a unique token +- Tokens are tied to the user's session +- Malicious websites cannot obtain valid tokens + +--- + +## CSRF Configuration + +### Complete Example + +```java +package com.example.admin; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; + +import de.codecentric.boot.admin.server.config.AdminServerProperties; + +import static org.springframework.http.HttpMethod.DELETE; +import static org.springframework.http.HttpMethod.POST; + +@Configuration +public class SecurityConfig { + + private final AdminServerProperties adminServer; + + public SecurityConfig(AdminServerProperties adminServer) { + this.adminServer = adminServer; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/assets/**"))) + .permitAll() + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/login"))) + .permitAll() + .anyRequest().authenticated() + ) + .formLogin(formLogin -> formLogin + .loginPage(adminServer.path("/login")) + ) + .httpBasic(Customizer.withDefaults()); + + // Custom CSRF filter to expose token to JavaScript + http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class); + + // CSRF configuration + http.csrf(csrf -> csrf + // Use cookie-based token repository (accessible to JavaScript) + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + // Use attribute-based token handler + .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) + // Exempt specific endpoints + .ignoringRequestMatchers( + // Client registration + PathPatternRequestMatcher.withDefaults() + .matcher(POST, adminServer.path("/instances")), + // Client deregistration + PathPatternRequestMatcher.withDefaults() + .matcher(DELETE, adminServer.path("/instances/*")), + // Notification endpoints + PathPatternRequestMatcher.withDefaults() + .matcher(POST, adminServer.path("/notifications/**")), + PathPatternRequestMatcher.withDefaults() + .matcher(DELETE, adminServer.path("/notifications/**")), + // Admin Server's own actuator + PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/actuator/**")) + ) + ); + + return http.build(); + } +} +``` + +--- + +## Custom CSRF Filter + +The Admin UI (JavaScript) needs access to the CSRF token. Create a filter to expose it: + +```java +package com.example.admin; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.WebUtils; + +public class CustomCsrfFilter extends OncePerRequestFilter { + + public static final String CSRF_COOKIE_NAME = "XSRF-TOKEN"; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + // Get CSRF token from request attributes + CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + + if (csrf != null) { + Cookie cookie = WebUtils.getCookie(request, CSRF_COOKIE_NAME); + String token = csrf.getToken(); + + // Set cookie if not present or token changed + if (cookie == null || token != null && !token.equals(cookie.getValue())) { + cookie = new Cookie(CSRF_COOKIE_NAME, token); + cookie.setPath("/"); + response.addCookie(cookie); + } + } + + filterChain.doFilter(request, response); + } +} +``` + +**What this does**: + +1. Extracts CSRF token from Spring Security +2. Stores it in a cookie named `XSRF-TOKEN` +3. Cookie is **not** HTTP-only (JavaScript can read it) +4. Admin UI reads cookie and includes token in requests + +--- + +## How CSRF Works in Admin Server + +### 1. User Opens Admin UI + +``` +GET / HTTP/1.1 +``` + +**Response**: + +``` +HTTP/1.1 200 OK +Set-Cookie: XSRF-TOKEN=abc123; Path=/ +Set-Cookie: JSESSIONID=xyz789; Path=/; HttpOnly + + +... +``` + +### 2. JavaScript Makes Request + +Admin UI JavaScript reads `XSRF-TOKEN` cookie and sends it in header: + +```javascript +fetch('/instances/123/actuator/info', { + method: 'GET', + headers: { + 'X-XSRF-TOKEN': 'abc123' // From cookie + }, + credentials: 'same-origin' +}) +``` + +**HTTP Request**: + +``` +GET /instances/123/actuator/info HTTP/1.1 +X-XSRF-TOKEN: abc123 +Cookie: XSRF-TOKEN=abc123; JSESSIONID=xyz789 +``` + +### 3. Spring Security Validates Token + +Spring Security compares: + +- Token from `X-XSRF-TOKEN` header +- Token from `XSRF-TOKEN` cookie + +If they match, request is allowed. + +### 4. Client Registration (Exempted) + +Client applications register **without** CSRF token: + +``` +POST /instances HTTP/1.1 +Content-Type: application/json + +{ + "name": "my-service", + "healthUrl": "http://localhost:8081/actuator/health" +} +``` + +This works because `/instances` is in `ignoringRequestMatchers`. + +--- + +## Cookie vs Session Token Repository + +### Cookie-Based (Recommended for SPA) + +```java +.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) +``` + +**Pros**: + +- JavaScript can read token from cookie +- Works with Single Page Applications (SPA) +- Stateless (no server-side session storage) + +**Cons**: + +- Cookie not HTTP-only (accessible to JavaScript) +- Requires custom filter to set cookie + +### Session-Based (Default) + +```java +.csrfTokenRepository(new HttpSessionCsrfTokenRepository()) +``` + +**Pros**: + +- More secure (token not exposed to JavaScript) +- Simpler configuration + +**Cons**: + +- Requires server-side session +- Harder to use with SPA frameworks + +**Spring Boot Admin requires cookie-based** because the UI is a JavaScript SPA. + +--- + +## Exempted Endpoints + +### Client Registration + +```java +.ignoringRequestMatchers( + PathPatternRequestMatcher.withDefaults() + .matcher(POST, adminServer.path("/instances")), + PathPatternRequestMatcher.withDefaults() + .matcher(DELETE, adminServer.path("/instances/*")) +) +``` + +**Why?** + +- Client applications don't have CSRF tokens +- They register/deregister via simple HTTP POST/DELETE +- Not vulnerable to CSRF (no browser session involved) + +### Actuator Endpoints + +```java +.ignoringRequestMatchers( + PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/actuator/**")) +) +``` + +**Why?** + +- Admin Server's own health checks +- Load balancers, monitoring tools access these +- No CSRF risk (stateless, no session) + +### Notification Endpoints + +```java +.ignoringRequestMatchers( + PathPatternRequestMatcher.withDefaults() + .matcher(POST, adminServer.path("/notifications/**")), + PathPatternRequestMatcher.withDefaults() + .matcher(DELETE, adminServer.path("/notifications/**")) +) +``` + +**Why?** + +- Webhook endpoints from external services (Slack, Teams, etc.) +- Cannot provide CSRF tokens +- Authenticated via other means (webhook secrets) + +--- + +## Context Path Support + +If using a custom context path: + +```yaml +spring: + boot: + admin: + context-path: /admin +``` + +Use `adminServer.path()` to include context path automatically: + +```java +PathPatternRequestMatcher.withDefaults() + .matcher(POST, adminServer.path("/instances")) +``` + +This becomes `/admin/instances` automatically. + +--- + +## Testing CSRF Protection + +### Test UI Request (Requires Token) + +**Without token**: + +```bash +curl -X POST http://localhost:8080/applications/my-app/restart \ + -H "Cookie: JSESSIONID=abc123" +``` + +**Response**: `403 Forbidden` + +**With token**: + +```bash +curl -X POST http://localhost:8080/applications/my-app/restart \ + -H "Cookie: JSESSIONID=abc123; XSRF-TOKEN=def456" \ + -H "X-XSRF-TOKEN: def456" +``` + +**Response**: `200 OK` + +### Test Client Registration (Exempted) + +```bash +curl -X POST http://localhost:8080/instances \ + -H "Content-Type: application/json" \ + -d '{ + "name": "test-service", + "healthUrl": "http://localhost:8081/actuator/health" + }' +``` + +**Response**: `201 Created` (no CSRF token needed) + +### Test Actuator (Exempted) + +```bash +curl http://localhost:8080/actuator/health +``` + +**Response**: `200 OK` (no CSRF token needed) + +--- + +## Disable CSRF (Not Recommended) + +For development/testing only: + +```java +@Bean +@Profile("dev") +public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(/* ... */) + .csrf(csrf -> csrf.disable()); // Disable CSRF + + return http.build(); +} +``` + +**Only disable CSRF for development and testing.** + +--- + +## SameSite Cookie Attribute + +Enhance CSRF protection with SameSite cookies: + +```yaml +server: + servlet: + session: + cookie: + same-site: strict +``` + +**Options**: + +- `strict`: Cookie only sent for same-site requests (most secure) +- `lax`: Cookie sent for top-level navigation (default) +- `none`: Cookie sent for all requests (requires `secure=true`) + +**Recommendation**: Use `lax` for Admin Server (allows direct navigation). + +--- + +## Troubleshooting + +### Issue: 403 Forbidden on all requests + +**Cause**: CSRF token missing or invalid. + +**Check**: + +1. Cookie is set: + + ```bash + curl -i http://localhost:8080/ + ``` + + Should see `Set-Cookie: XSRF-TOKEN=...` + +2. Custom CSRF filter is registered: + + ```java + http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class) + ``` + +3. Token repository is cookie-based: + + ```java + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + ``` + +### Issue: Client registration fails with 403 + +**Cause**: `/instances` endpoint not exempted from CSRF. + +**Solution**: Add to `ignoringRequestMatchers`: + +```java +.csrf(csrf -> csrf + .ignoringRequestMatchers( + PathPatternRequestMatcher.withDefaults() + .matcher(POST, adminServer.path("/instances")), + PathPatternRequestMatcher.withDefaults() + .matcher(DELETE, adminServer.path("/instances/*")) + ) +) +``` + +### Issue: Token cookie not accessible to JavaScript + +**Cause**: Cookie is HTTP-only. + +**Solution**: Use `withHttpOnlyFalse()`: + +```java +.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) +``` + +### Issue: Token changes on every request + +**Expected behavior**. Spring Security generates new tokens regularly for security. + +**If problematic**: Use `CookieCsrfTokenRepository` with custom settings: + +```java +CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse(); +repository.setCookieName("XSRF-TOKEN"); +repository.setHeaderName("X-XSRF-TOKEN"); +``` + +### Issue: CSRF protection not working with context path + +**Cause**: Matchers don't include context path. + +**Solution**: Use `adminServer.path()`: + +```java +PathPatternRequestMatcher.withDefaults() + .matcher(POST, adminServer.path("/instances")) +``` + +Not: + +```java +new AntPathRequestMatcher("/instances", POST.name()) +``` + +--- + +## Advanced Configuration + +### Custom Token Header/Cookie Names + +```java +CookieCsrfTokenRepository repository = new CookieCsrfTokenRepository(); +repository.setCookieName("MY-CSRF-TOKEN"); +repository.setHeaderName("X-MY-CSRF-TOKEN"); +repository.setParameterName("_csrf"); +repository.setCookieHttpOnly(false); + +http.csrf(csrf -> csrf + .csrfTokenRepository(repository) +) +``` + +Update `CustomCsrfFilter` accordingly: + +```java +public static final String CSRF_COOKIE_NAME = "MY-CSRF-TOKEN"; +``` + +### Conditional CSRF Protection + +Enable CSRF only for browser requests: + +```java +http.csrf(csrf -> csrf + .requireCsrfProtectionMatcher(request -> { + // Require CSRF for browser requests (non-API) + String method = request.getMethod(); + if ("GET".equals(method) || "HEAD".equals(method) || + "TRACE".equals(method) || "OPTIONS".equals(method)) { + return false; // Safe methods + } + + String header = request.getHeader("X-Requested-With"); + if ("XMLHttpRequest".equals(header)) { + return true; // AJAX requests + } + + String accept = request.getHeader("Accept"); + if (accept != null && accept.contains("application/json")) { + return false; // API clients + } + + return true; // Browser requests + }) +) +``` + +### Multiple Security Filter Chains + +Separate CSRF rules for UI and API: + +```java +@Configuration +public class SecurityConfig { + + @Bean + @Order(1) + public SecurityFilterChain apiFilterChain(HttpSecurity http, + AdminServerProperties adminServer) throws Exception { + http + .securityMatcher(adminServer.path("/api/**")) + .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) + .httpBasic(Customizer.withDefaults()) + .csrf(csrf -> csrf.disable()); // No CSRF for API + + return http.build(); + } + + @Bean + @Order(2) + public SecurityFilterChain uiFilterChain(HttpSecurity http, + AdminServerProperties adminServer) throws Exception { + http + .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) + .formLogin(/* ... */) + .csrf(csrf -> csrf // CSRF for UI + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + ); + + return http.build(); + } +} +``` + +--- + +## Best Practices + +1. **Always enable CSRF** when deploying +2. **Use cookie-based repository** for JavaScript SPAs +3. **Exempt only necessary endpoints** (client registration, actuator) +4. **Use SameSite cookies** for additional protection +5. **Test CSRF protection** before deploying +6. **Use HTTPS** to prevent token theft +7. **Rotate session IDs** after login +8. **Monitor for CSRF attacks** in logs + +--- + +## Complete Working Example + +**SecurityConfig.java**: + +```java +package com.example.admin; + +import java.util.UUID; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; + +import de.codecentric.boot.admin.server.config.AdminServerProperties; + +import static org.springframework.http.HttpMethod.DELETE; +import static org.springframework.http.HttpMethod.POST; + +@Configuration +public class SecurityConfig { + + private final AdminServerProperties adminServer; + + public SecurityConfig(AdminServerProperties adminServer) { + this.adminServer = adminServer; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + SavedRequestAwareAuthenticationSuccessHandler successHandler = + new SavedRequestAwareAuthenticationSuccessHandler(); + successHandler.setTargetUrlParameter("redirectTo"); + successHandler.setDefaultTargetUrl(adminServer.path("/")); + + http + .authorizeHttpRequests(auth -> auth + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/assets/**"))) + .permitAll() + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/login"))) + .permitAll() + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/actuator/info"))) + .permitAll() + .requestMatchers(PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/actuator/health"))) + .permitAll() + .anyRequest().authenticated() + ) + .formLogin(formLogin -> formLogin + .loginPage(adminServer.path("/login")) + .successHandler(successHandler) + ) + .logout(logout -> logout + .logoutUrl(adminServer.path("/logout")) + ) + .httpBasic(Customizer.withDefaults()); + + http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class) + .csrf(csrf -> csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) + .ignoringRequestMatchers( + PathPatternRequestMatcher.withDefaults() + .matcher(POST, adminServer.path("/instances")), + PathPatternRequestMatcher.withDefaults() + .matcher(DELETE, adminServer.path("/instances/*")), + PathPatternRequestMatcher.withDefaults() + .matcher(adminServer.path("/actuator/**")) + ) + ); + + http.rememberMe(rememberMe -> rememberMe + .key(UUID.randomUUID().toString()) + .tokenValiditySeconds(1209600) + ); + + return http.build(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) { + UserDetails user = User.builder() + .username("admin") + .password(passwordEncoder.encode(System.getenv("ADMIN_PASSWORD"))) + .roles("ADMIN") + .build(); + + return new InMemoryUserDetailsManager(user); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} +``` + +**CustomCsrfFilter.java** (same as shown earlier). + +--- + +## See Also + +- [Server Authentication](./10-server-authentication.md) - Configure Spring Security +- [Actuator Security](./20-actuator-security.md) - Secure client endpoints +- [Spring Security CSRF Documentation](https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html) diff --git a/spring-boot-admin-docs/src/site/docs/05-security/_category_.json b/spring-boot-admin-docs/src/site/docs/05-security/_category_.json new file mode 100644 index 00000000000..3b4938cffd8 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/05-security/_category_.json @@ -0,0 +1,4 @@ +{ + "position": 5, + "label": "Security" +} diff --git a/spring-boot-admin-docs/src/site/docs/05-security/index.md b/spring-boot-admin-docs/src/site/docs/05-security/index.md new file mode 100644 index 00000000000..eafb0d6bf38 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/05-security/index.md @@ -0,0 +1,486 @@ +--- +sidebar_position: 1 +sidebar_custom_props: + icon: 'shield' +--- + +# Security + +Spring Boot Admin Server and Client can be secured using Spring Security. This section covers all aspects of securing +your Spring Boot Admin deployment. + +## Security Overview + +A complete Spring Boot Admin deployment has multiple security concerns: + +```mermaid +graph TD + A[User Browser] -->|1 Login to Admin UI| B["Spring Boot Admin Server
• Form login + HTTP Basic authentication
• CSRF protection for UI
• Remember-me functionality"] + B -->|2 Access actuator endpoints| C["Client Application Instance
• Secured actuator endpoints
• Client provides credentials via metadata
• Server authenticates using instance-auth"] +``` + +## Security Layers + +### 1. Admin Server Security + +Protect the Admin UI and API endpoints: + +- **Authentication**: Form login for UI, HTTP Basic for API clients +- **Authorization**: Role-based access control +- **CSRF Protection**: Protect against Cross-Site Request Forgery +- **Session Management**: Remember-me functionality + +**See**: [Server Authentication](./10-server-authentication.md) + +### 2. Actuator Endpoint Security + +Secure the client application's actuator endpoints: + +- **Spring Security**: Protect actuator with authentication +- **Credentials Sharing**: Pass credentials to Admin Server via metadata +- **Per-Service Auth**: Different credentials per service + +**See**: [Actuator Security](./20-actuator-security.md) + +### 3. CSRF Protection + +Configure CSRF tokens for Admin UI while allowing client registration: + +- **Cookie-based CSRF**: JavaScript-friendly token repository +- **Exempted Endpoints**: Allow `/instances` registration without CSRF +- **Custom CSRF Filter**: Make token available to JavaScript + +**See**: [CSRF Protection](./30-csrf-protection.md) + +### 4. Mutual TLS (Optional) + +Enhanced security with client certificates: + +- **mTLS Between Server and Clients**: Mutual authentication +- **Certificate Validation**: Trust only specific clients +- **SSL Configuration**: Keystore and truststore setup + +--- + +## Quick Start Examples + +### Minimal Secured Server + +```yaml +spring: + security: + user: + name: admin + password: ${ADMIN_PASSWORD} +``` + +```java +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(auth -> auth + .requestMatchers("/assets/**").permitAll() + .requestMatchers("/login").permitAll() + .anyRequest().authenticated() + ) + .formLogin(formLogin -> formLogin.loginPage("/login")) + .httpBasic(Customizer.withDefaults()); + + return http.build(); + } +} +``` + +### Client with Secured Actuator + +**Client Configuration**: + +```yaml +spring: + boot: + admin: + client: + url: http://admin-server:8080 + instance: + metadata: + user.name: actuator + user.password: ${ACTUATOR_PASSWORD} + + security: + user: + name: actuator + password: ${ACTUATOR_PASSWORD} + +management: + endpoints: + web: + exposure: + include: "*" +``` + +**Server Configuration**: + +```yaml +spring: + boot: + admin: + instance-auth: + enabled: true + # Credentials from instance metadata +``` + +--- + +## Security Checklist + +Use this checklist to ensure your deployment is secure: + +### Admin Server + +- [ ] Enable Spring Security +- [ ] Use strong passwords (externalize via environment variables) +- [ ] Configure form login for UI access +- [ ] Enable HTTP Basic for API/programmatic access +- [ ] Configure CSRF protection with exemptions for `/instances` +- [ ] Set up remember-me with secure random key +- [ ] Use HTTPS for deployments +- [ ] Restrict access by IP (if applicable) +- [ ] Configure session timeout +- [ ] Audit authentication attempts + +### Client Applications + +- [ ] Secure actuator endpoints with Spring Security +- [ ] Pass actuator credentials via metadata (`user.name`, `user.password`) +- [ ] Use strong actuator passwords +- [ ] Limit exposed actuator endpoints to necessary ones +- [ ] Use HTTPS for actuator if possible +- [ ] Verify Admin Server certificate (if using HTTPS) +- [ ] Consider mutual TLS for high-security environments + +### Network Security + +- [ ] Use HTTPS for all communication +- [ ] Configure firewalls to restrict Admin Server access +- [ ] Use VPN or private networks when possible +- [ ] Enable mutual TLS if required +- [ ] Monitor for suspicious access patterns + +--- + +## Common Security Scenarios + +### Scenario 1: Development Environment + +**Goal**: Simple security for local development. + +```yaml +# Admin Server +spring: + security: + user: + name: user + password: password +``` + +No actuator security needed in development. + +### Scenario 2: Production with Role-Based Access + +**Goal**: Different roles for read-only vs admin users. + +```java +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(auth -> auth + .requestMatchers("/assets/**", "/login").permitAll() + .requestMatchers("/instances/**").hasRole("ADMIN") + .anyRequest().hasAnyRole("ADMIN", "USER") + ) + .formLogin(formLogin -> formLogin.loginPage("/login")) + .httpBasic(Customizer.withDefaults()); + + return http.build(); + } + + @Bean + public UserDetailsService userDetailsService(PasswordEncoder encoder) { + UserDetails admin = User.builder() + .username("admin") + .password(encoder.encode(System.getenv("ADMIN_PASSWORD"))) + .roles("ADMIN") + .build(); + + UserDetails user = User.builder() + .username("viewer") + .password(encoder.encode(System.getenv("VIEWER_PASSWORD"))) + .roles("USER") + .build(); + + return new InMemoryUserDetailsManager(admin, user); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} +``` + +### Scenario 3: Kubernetes with Service Accounts + +**Goal**: Use Kubernetes service accounts for authentication. + +```yaml +# Admin Server +spring: + boot: + admin: + discovery: + enabled: true + +# Spring Security with OAuth2 +spring: + security: + oauth2: + client: + registration: + keycloak: + client-id: spring-boot-admin + client-secret: ${OAUTH2_CLIENT_SECRET} + provider: + keycloak: + issuer-uri: https://keycloak.company.com/realms/main +``` + +### Scenario 4: Different Credentials per Service + +**Goal**: Use unique credentials for each client service. + +**Admin Server**: + +```yaml +spring: + boot: + admin: + instance-auth: + enabled: true + service-map: + service-a: + user-name: service-a-actuator + user-password: ${SERVICE_A_PASSWORD} + service-b: + user-name: service-b-actuator + user-password: ${SERVICE_B_PASSWORD} + default-user-name: default-actuator + default-password: ${DEFAULT_PASSWORD} +``` + +**Client (Service A)**: + +```yaml +spring: + application: + name: service-a + + security: + user: + name: service-a-actuator + password: ${SERVICE_A_PASSWORD} +``` + +--- + +## Best Practices + +### 1. Externalize Secrets + +Never hardcode passwords. Use environment variables or secret management: + +```yaml +spring: + security: + user: + name: ${ADMIN_USER:admin} + password: ${ADMIN_PASSWORD} +``` + +**Docker**: + +```bash +docker run -e ADMIN_PASSWORD=secret123 my-admin-server +``` + +**Kubernetes Secret**: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: admin-credentials +type: Opaque +data: + password: c2VjcmV0MTIz # base64 encoded +``` + +### 2. Use Strong Passwords + +- Minimum 16 characters +- Mix of uppercase, lowercase, numbers, symbols +- Use password generators +- Rotate regularly + +### 3. Limit Actuator Exposure + +Only expose necessary endpoints: + +```yaml +management: + endpoints: + web: + exposure: + include: health,info,metrics,loggers +``` + +### 4. Enable HTTPS + +Use TLS for all communication: + +```yaml +server: + port: 8443 + ssl: + enabled: true + key-store: classpath:keystore.p12 + key-store-password: ${KEYSTORE_PASSWORD} + key-store-type: PKCS12 +``` + +### 5. Monitor Security Events + +Log authentication attempts and failures: + +```yaml +logging: + level: + org.springframework.security: DEBUG + de.codecentric.boot.admin: DEBUG +``` + +--- + +## Security Headers + +Configure security headers for the Admin UI: + +```java +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .headers(headers -> headers + .contentSecurityPolicy(csp -> csp + .policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'") + ) + .frameOptions(frame -> frame.sameOrigin()) + .xssProtection(xss -> xss.block(true)) + .httpStrictTransportSecurity(hsts -> hsts + .includeSubDomains(true) + .maxAgeInSeconds(31536000) + ) + ); + + return http.build(); + } +} +``` + +--- + +## Troubleshooting + +### Issue: 401 Unauthorized when accessing instances + +**Cause**: Admin Server doesn't have credentials to access actuator endpoints. + +**Solution**: Add credentials to instance metadata: + +```yaml +spring: + boot: + admin: + client: + instance: + metadata: + user.name: actuator + user.password: ${ACTUATOR_PASSWORD} +``` + +### Issue: CSRF token errors on client registration + +**Cause**: CSRF protection blocking `/instances` endpoint. + +**Solution**: Exempt registration endpoints from CSRF: + +```java +.csrf(csrf -> csrf + .ignoringRequestMatchers( + new AntPathRequestMatcher("/instances", POST.name()), + new AntPathRequestMatcher("/instances/*", DELETE.name()) + ) +) +``` + +### Issue: Login page not loading + +**Cause**: Login page assets blocked by security. + +**Solution**: Permit access to assets and login: + +```java +.authorizeHttpRequests(auth -> auth + .requestMatchers("/assets/**", "/login").permitAll() + .anyRequest().authenticated() +) +``` + +### Issue: Remember-me not working + +**Cause**: No `UserDetailsService` configured. + +**Solution**: Add `UserDetailsService` bean: + +```java +@Bean +public InMemoryUserDetailsManager userDetailsService(PasswordEncoder encoder) { + UserDetails user = User.builder() + .username("admin") + .password(encoder.encode("password")) + .roles("ADMIN") + .build(); + return new InMemoryUserDetailsManager(user); +} +``` + +--- + +## Next Steps + +- [Server Authentication](./10-server-authentication.md) - Secure Admin Server with Spring Security +- [Actuator Security](./20-actuator-security.md) - Secure client actuator endpoints +- [CSRF Protection](./30-csrf-protection.md) - Configure CSRF for UI and API + +--- + +## See Also + +- [Server Configuration](../02-server/01-server.mdx) +- [Client Configuration](../03-client/10-client-features.md) +- [Spring Security Documentation](https://docs.spring.io/spring-security/reference/index.html) diff --git a/spring-boot-admin-docs/src/site/docs/06-customization/_category_.json b/spring-boot-admin-docs/src/site/docs/06-customization/_category_.json new file mode 100644 index 00000000000..d600c8bce13 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/06-customization/_category_.json @@ -0,0 +1,4 @@ +{ + "position": 6, + "label": "Customization" +} diff --git a/spring-boot-admin-docs/src/site/docs/third-party/index.md b/spring-boot-admin-docs/src/site/docs/06-customization/index.md similarity index 70% rename from spring-boot-admin-docs/src/site/docs/third-party/index.md rename to spring-boot-admin-docs/src/site/docs/06-customization/index.md index 670f6784091..e0a71bdd4b7 100644 --- a/spring-boot-admin-docs/src/site/docs/third-party/index.md +++ b/spring-boot-admin-docs/src/site/docs/06-customization/index.md @@ -1,5 +1,5 @@ import DocCardList from '@theme/DocCardList'; -# Third Party Integrations +# Customization diff --git a/spring-boot-admin-docs/src/site/docs/06-customization/monitoring/01-instance-filters.md b/spring-boot-admin-docs/src/site/docs/06-customization/monitoring/01-instance-filters.md new file mode 100644 index 00000000000..ce7adf95926 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/06-customization/monitoring/01-instance-filters.md @@ -0,0 +1,798 @@ +--- + +sidebar_position: 1 +sidebar_custom_props: + icon: 'wrench' +--- + +# Instance Filters + +Filter which instances are visible and managed by Spring Boot Admin Server. + +## Overview + +`InstanceFilter` allows you to selectively include or exclude instances from being displayed and monitored by the Admin +Server. + +**Use Cases**: + +- Hide test/development instances in production Admin Server +- Filter instances by environment, region, or tags +- Exclude specific services from monitoring +- Show only instances matching certain criteria + +```mermaid +graph TD + A["Instances Registered
• service-a (env=prod)
• service-b (env=dev)
• service-c (env=prod)
• service-d (env=test)"] --> B["InstanceFilter
filter(instance) {
return 'prod'.equals(
instance.getMetadata().get('env')
);
}"] + B --> C["Visible Instances
• service-a (env=prod)
• service-c (env=prod)"] +``` + +--- + +## Default Behavior + +By default, **all instances are visible**: + +```java +@Bean +@ConditionalOnMissingBean +public InstanceFilter instanceFilter() { + return instance -> true; // Accept all instances +} +``` + +--- + +## InstanceFilter Interface + +```java +package de.codecentric.boot.admin.server.services; + +import de.codecentric.boot.admin.server.domain.entities.Instance; + +@FunctionalInterface +public interface InstanceFilter { + + /** + * Test if instance should be visible + * @param instance the instance to filter + * @return true if instance should be included, false to exclude + */ + boolean filter(Instance instance); + +} +``` + +--- + +## How It Works + +`InstanceFilter` is applied by `InstanceRegistry`: + +```java +public Flux getInstances() { + return repository.findAll().filter(filter::filter); +} + +public Mono getInstance(InstanceId id) { + return repository.find(id).filter(filter::filter); +} +``` + +**Important**: Instances are **still stored** in the repository, but filtered from queries. This means: + +- Filtered instances continue to be monitored +- Events are still generated for filtered instances +- Filtering only affects visibility in the UI and API + +--- + +## Filter by Environment + +Show only production instances: + +```java +package com.example.admin; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.services.InstanceFilter; + +@Configuration +public class InstanceFilterConfig { + + @Bean + public InstanceFilter instanceFilter() { + return instance -> { + String env = instance.getRegistration() + .getMetadata() + .get("environment"); + + return "production".equals(env); + }; + } +} +``` + +**Client Configuration**: + +```yaml +spring: + boot: + admin: + client: + instance: + metadata: + environment: production +``` + +--- + +## Filter by Tags + +Show only instances with specific tags: + +```java +package com.example.admin; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.services.InstanceFilter; + +@Configuration +public class InstanceFilterConfig { + + @Bean + public InstanceFilter instanceFilter() { + return instance -> { + String tags = instance.getRegistration() + .getMetadata() + .get("tags"); + + if (tags == null) { + return false; // Exclude instances without tags + } + + // Show instances with "production" or "critical" tag + return tags.contains("production") || tags.contains("critical"); + }; + } +} +``` + +**Client Configuration**: + +```yaml +spring: + boot: + admin: + client: + instance: + metadata: + tags: production,critical,payment +``` + +--- + +## Filter by Service Name + +Exclude specific services: + +```java +package com.example.admin; + +import java.util.Set; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.services.InstanceFilter; + +@Configuration +public class InstanceFilterConfig { + + @Bean + public InstanceFilter instanceFilter() { + Set excludedServices = Set.of( + "test-service", + "dev-helper", + "mock-service" + ); + + return instance -> { + String serviceName = instance.getRegistration().getName(); + return !excludedServices.contains(serviceName); + }; + } +} +``` + +--- + +## Filter by Status + +Show only healthy instances: + +```java +package com.example.admin; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.domain.values.StatusInfo; +import de.codecentric.boot.admin.server.services.InstanceFilter; + +@Configuration +public class InstanceFilterConfig { + + @Bean + public InstanceFilter instanceFilter() { + return instance -> { + StatusInfo status = instance.getStatusInfo(); + + // Show only UP or UNKNOWN instances + return status.isUp() || status.isUnknown(); + }; + } +} +``` + +**Status values**: + +- `isUp()` - Instance is healthy +- `isDown()` - Instance is unhealthy +- `isOffline()` - Instance is unreachable +- `isUnknown()` - Status not yet determined + +--- + +## Filter by URL Pattern + +Show only instances from specific hosts: + +```java +package com.example.admin; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.services.InstanceFilter; + +@Configuration +public class InstanceFilterConfig { + + @Bean + public InstanceFilter instanceFilter() { + return instance -> { + String serviceUrl = instance.getRegistration().getServiceUrl(); + + // Show only instances on production domain + return serviceUrl != null && + serviceUrl.contains(".prod.company.com"); + }; + } +} +``` + +--- + +## Configurable Filter + +Filter based on application properties: + +**application.yml**: + +```yaml +admin: + filter: + enabled: true + environment: production + tags: + - critical + - production +``` + +**Configuration**: + +```java +package com.example.admin; + +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import de.codecentric.boot.admin.server.services.InstanceFilter; + +@Configuration +@EnableConfigurationProperties(FilterProperties.class) +public class InstanceFilterConfig { + + @Bean + public InstanceFilter instanceFilter(FilterProperties filterProperties) { + if (!filterProperties.isEnabled()) { + return instance -> true; // No filtering + } + + return instance -> { + String env = instance.getRegistration() + .getMetadata() + .get("environment"); + + String tags = instance.getRegistration() + .getMetadata() + .get("tags"); + + // Check environment + if (filterProperties.getEnvironment() != null) { + if (!filterProperties.getEnvironment().equals(env)) { + return false; + } + } + + // Check tags + if (filterProperties.getTags() != null && !filterProperties.getTags().isEmpty()) { + if (tags == null) { + return false; + } + + for (String requiredTag : filterProperties.getTags()) { + if (tags.contains(requiredTag)) { + return true; + } + } + return false; + } + + return true; + }; + } +} + +@Component +@ConfigurationProperties(prefix = "admin.filter") +class FilterProperties { + private boolean enabled = false; + private String environment; + private List tags; + + // Getters and setters + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getEnvironment() { + return environment; + } + + public void setEnvironment(String environment) { + this.environment = environment; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } +} +``` + +--- + +## Multiple Conditions + +Combine multiple filter conditions: + +```java +package com.example.admin; + +import java.util.Set; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.services.InstanceFilter; + +@Configuration +public class InstanceFilterConfig { + + @Bean + public InstanceFilter instanceFilter() { + Set allowedEnvironments = Set.of("production", "staging"); + Set excludedServices = Set.of("test-service", "dev-tool"); + + return instance -> { + String env = instance.getRegistration() + .getMetadata() + .get("environment"); + + String serviceName = instance.getRegistration().getName(); + + // Include if: + // 1. Environment is allowed AND + // 2. Service is not excluded + return allowedEnvironments.contains(env) && + !excludedServices.contains(serviceName); + }; + } +} +``` + +--- + +## Advanced Filtering + +### Filter by Registration Time + +Show only recently registered instances: + +```java +package com.example.admin; + +import java.time.Duration; +import java.time.Instant; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.services.InstanceFilter; + +@Configuration +public class InstanceFilterConfig { + + @Bean + public InstanceFilter instanceFilter() { + return instance -> { + Instant registrationTime = instance.getRegistration().getTimestamp(); + if (registrationTime == null) { + return true; + } + + // Show instances registered within last 7 days + Duration age = Duration.between(registrationTime, Instant.now()); + return age.compareTo(Duration.ofDays(7)) < 0; + }; + } +} +``` + +### Filter by Build Info + +Show only instances with specific versions: + +```java +package com.example.admin; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.services.InstanceFilter; + +@Configuration +public class InstanceFilterConfig { + + @Bean + public InstanceFilter instanceFilter() { + return instance -> { + if (instance.getInfo() == null || instance.getInfo().getValues() == null) { + return true; // No build info available + } + + Object buildInfo = instance.getInfo().getValues().get("build"); + if (buildInfo instanceof Map) { + Map build = (Map) buildInfo; + String version = (String) build.get("version"); + + // Only show version 2.x and above + return version != null && !version.startsWith("1."); + } + + return true; + }; + } +} +``` + +### Database-Driven Filter + +Load filter rules from database: + +```java +package com.example.admin; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.services.InstanceFilter; + +@Configuration +public class InstanceFilterConfig { + + @Bean + public InstanceFilter instanceFilter(InstanceFilterRuleRepository repository) { + return instance -> { + String serviceName = instance.getRegistration().getName(); + String env = instance.getRegistration() + .getMetadata() + .get("environment"); + + // Query database for filter rules + return repository.shouldShowInstance(serviceName, env); + }; + } +} + +interface InstanceFilterRuleRepository { + boolean shouldShowInstance(String serviceName, String environment); +} +``` + +--- + +## Composite Filters + +Combine multiple filters with AND/OR logic: + +```java +package com.example.admin; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.services.InstanceFilter; + +@Configuration +public class InstanceFilterConfig { + + @Bean + public InstanceFilter instanceFilter() { + InstanceFilter envFilter = instance -> { + String env = instance.getRegistration() + .getMetadata() + .get("environment"); + return "production".equals(env); + }; + + InstanceFilter statusFilter = instance -> { + return instance.getStatusInfo().isUp(); + }; + + InstanceFilter tagsFilter = instance -> { + String tags = instance.getRegistration() + .getMetadata() + .get("tags"); + return tags != null && tags.contains("monitored"); + }; + + // Combine with AND logic + return and(envFilter, statusFilter, tagsFilter); + } + + private InstanceFilter and(InstanceFilter... filters) { + return instance -> { + for (InstanceFilter filter : filters) { + if (!filter.filter(instance)) { + return false; + } + } + return true; + }; + } + + private InstanceFilter or(InstanceFilter... filters) { + return instance -> { + for (InstanceFilter filter : filters) { + if (filter.filter(instance)) { + return true; + } + } + return false; + }; + } +} +``` + +--- + +## Testing Filters + +### Unit Test + +```java +package com.example.admin; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.values.InstanceId; +import de.codecentric.boot.admin.server.domain.values.Registration; +import de.codecentric.boot.admin.server.services.InstanceFilter; + +import static org.assertj.core.api.Assertions.assertThat; + +class InstanceFilterTest { + + @Test + void shouldFilterByEnvironment() { + InstanceFilter filter = instance -> { + String env = instance.getRegistration() + .getMetadata() + .get("environment"); + return "production".equals(env); + }; + + Instance prodInstance = createInstance("prod-service", + Map.of("environment", "production")); + Instance devInstance = createInstance("dev-service", + Map.of("environment", "development")); + + assertThat(filter.filter(prodInstance)).isTrue(); + assertThat(filter.filter(devInstance)).isFalse(); + } + + private Instance createInstance(String name, Map metadata) { + Registration registration = Registration.builder() + .name(name) + .healthUrl("http://localhost:8080/actuator/health") + .metadata(metadata) + .build(); + + return Instance.create(InstanceId.of("test-id")) + .register(registration); + } +} +``` + +--- + +## Troubleshooting + +### Issue: Instances not appearing in UI + +**Cause**: Filter is excluding them. + +**Debug**: + +```java +@Bean +public InstanceFilter instanceFilter() { + return instance -> { + boolean result = /* your filter logic */; + + // Log for debugging + if (!result) { + System.out.println("Filtered out: " + + instance.getRegistration().getName()); + } + + return result; + }; +} +``` + +### Issue: Filter not applied + +**Cause**: Multiple `InstanceFilter` beans defined. + +**Solution**: Only define one `InstanceFilter` bean. Spring Boot Admin uses the first one it finds. + +### Issue: Metadata not available + +**Cause**: Client not sending metadata. + +**Solution**: Verify client configuration: + +```yaml +spring: + boot: + admin: + client: + instance: + metadata: + environment: production +``` + +--- + +## Best Practices + +1. **Keep filters simple**: Complex filters can impact performance +2. **Document filter logic**: Make it clear why instances are excluded +3. **Test thoroughly**: Ensure correct instances are visible +4. **Use metadata**: Don't filter based on volatile data like status +5. **Consider multiple Admin Servers**: Instead of complex filtering, run separate Admin Servers for different + environments + +--- + +## Examples + +### Example 1: Multi-Tenant Filter + +```java +@Bean +public InstanceFilter instanceFilter(@Value("${tenant.id}") String tenantId) { + return instance -> { + String instanceTenant = instance.getRegistration() + .getMetadata() + .get("tenant"); + return tenantId.equals(instanceTenant); + }; +} +``` + +### Example 2: Region-Based Filter + +```java +@Bean +public InstanceFilter instanceFilter(@Value("${aws.region}") String currentRegion) { + return instance -> { + String instanceRegion = instance.getRegistration() + .getMetadata() + .get("region"); + return currentRegion.equals(instanceRegion); + }; +} +``` + +### Example 3: Whitelist/Blacklist Filter + +```java +@Bean +public InstanceFilter instanceFilter( + @Value("${admin.whitelist:}") List whitelist, + @Value("${admin.blacklist:}") List blacklist) { + + return instance -> { + String serviceName = instance.getRegistration().getName(); + + // Blacklist takes precedence + if (blacklist.contains(serviceName)) { + return false; + } + + // If whitelist is empty, allow all (except blacklisted) + if (whitelist.isEmpty()) { + return true; + } + + // Otherwise, only allow whitelisted + return whitelist.contains(serviceName); + }; +} +``` + +**Configuration**: + +```yaml +admin: + whitelist: + - payment-service + - user-service + blacklist: + - test-service +``` + +--- + +## See Also + +- [Server Configuration](../../02-server/01-server.mdx) +- [Custom Health Status](./02-custom-health-status.md) +- [Endpoint Detection](../server/04-endpoint-detection.md) diff --git a/spring-boot-admin-docs/src/site/docs/06-customization/monitoring/02-custom-health-status.md b/spring-boot-admin-docs/src/site/docs/06-customization/monitoring/02-custom-health-status.md new file mode 100644 index 00000000000..c690938c46f --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/06-customization/monitoring/02-custom-health-status.md @@ -0,0 +1,861 @@ +--- + +sidebar_position: 2 +sidebar_custom_props: + icon: 'wrench' +--- + +# Custom Health Status + +Customize how Spring Boot Admin Server retrieves and interprets health status from instances. + +## Overview + +The Admin Server monitors instance health by querying the `/actuator/health` endpoint. You can customize: + +1. **StatusUpdater** - How health status is retrieved and parsed +2. **InfoUpdater** - How instance info is retrieved +3. **Status interpretation** - Custom status codes and logic + +```mermaid +graph TD + A[StatusUpdater every 10 seconds] --> B[GET /actuator/health] + B --> C[Parse response] + C --> D[Create StatusInfo] + D --> E[Update Instance.statusInfo] +``` + +--- + +## Default Behavior + +### StatusUpdater + +By default, `StatusUpdater` queries the health endpoint: + +**StatusUpdater.java** (simplified): + +```java +protected Mono doUpdateStatus(Instance instance) { + return instanceWebClient.instance(instance) + .get() + .uri(Endpoint.HEALTH) + .exchangeToMono(this::convertStatusInfo) + .timeout(Duration.ofSeconds(10)) + .onErrorResume(this::handleError) + .map(instance::withStatusInfo); +} + +protected StatusInfo getStatusInfoFromStatus(HttpStatusCode httpStatus, Map body) { + if (httpStatus.is2xxSuccessful()) { + return StatusInfo.ofUp(); + } + // Return DOWN with error details + return StatusInfo.ofDown(details); +} + +private Mono handleError(Throwable ex) { + Map details = new HashMap<>(); + details.put("message", ex.getMessage()); + details.put("exception", ex.getClass().getName()); + return Mono.just(StatusInfo.ofOffline(details)); +} +``` + +**Status Mapping**: + +- HTTP 2xx → `UP` +- HTTP 4xx/5xx → `DOWN` +- Network error → `OFFLINE` + +--- + +## StatusInfo + +### Built-in Status Codes + +```java +public static final String STATUS_UNKNOWN = "UNKNOWN"; +public static final String STATUS_OUT_OF_SERVICE = "OUT_OF_SERVICE"; +public static final String STATUS_UP = "UP"; +public static final String STATUS_DOWN = "DOWN"; +public static final String STATUS_OFFLINE = "OFFLINE"; +public static final String STATUS_RESTRICTED = "RESTRICTED"; +``` + +**Status Priority** (highest to lowest): + +1. `DOWN` +2. `OUT_OF_SERVICE` +3. `OFFLINE` +4. `UNKNOWN` +5. `RESTRICTED` +6. `UP` + +### Creating StatusInfo + +```java +// UP status +StatusInfo.ofUp(); +StatusInfo.ofUp(Map.of("version", "1.0.0")); + +// DOWN status +StatusInfo.ofDown(); +StatusInfo.ofDown(Map.of("error", "Database unreachable")); + +// OFFLINE status +StatusInfo.ofOffline(); +StatusInfo.ofOffline(Map.of("message", "Connection timeout")); + +// Custom status +StatusInfo.valueOf("DEGRADED", Map.of("reason", "High latency")); +``` + +--- + +## Custom StatusUpdater + +### Example: Custom Timeout + +```java +package com.example.admin; + +import java.time.Duration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.StatusUpdater; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +@Configuration +public class StatusUpdaterConfig { + + @Bean + public StatusUpdater statusUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + + return new StatusUpdater(repository, instanceWebClient, apiMediaTypeHandler) + .timeout(Duration.ofSeconds(30)); // Increase timeout + } +} +``` + +### Example: Custom Status Interpretation + +```java +package com.example.admin; + +import java.util.Map; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatusCode; + +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.domain.values.StatusInfo; +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.StatusUpdater; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +@Configuration +public class StatusUpdaterConfig { + + @Bean + public StatusUpdater statusUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + + return new CustomStatusUpdater(repository, instanceWebClient, apiMediaTypeHandler); + } + + static class CustomStatusUpdater extends StatusUpdater { + + public CustomStatusUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + super(repository, instanceWebClient, apiMediaTypeHandler); + } + + @Override + protected StatusInfo getStatusInfoFromStatus(HttpStatusCode httpStatus, Map body) { + // Custom logic: 503 Service Unavailable = OUT_OF_SERVICE + if (httpStatus.value() == 503) { + return StatusInfo.valueOf("OUT_OF_SERVICE", body); + } + + // Custom logic: 429 Too Many Requests = RESTRICTED + if (httpStatus.value() == 429) { + return StatusInfo.valueOf("RESTRICTED", + Map.of("reason", "Rate limited")); + } + + // Delegate to default behavior + return super.getStatusInfoFromStatus(httpStatus, body); + } + } +} +``` + +### Example: Custom Health Endpoint + +Query a different health endpoint: + +```java +package com.example.admin; + +import reactor.core.publisher.Mono; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.StatusUpdater; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +public class CustomHealthEndpointStatusUpdater extends StatusUpdater { + + public CustomHealthEndpointStatusUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + super(repository, instanceWebClient, apiMediaTypeHandler); + } + + @Override + protected Mono doUpdateStatus(Instance instance) { + if (!instance.isRegistered()) { + return Mono.empty(); + } + + // Query custom health endpoint based on metadata + String customHealthPath = instance.getRegistration() + .getMetadata() + .getOrDefault("health-path", "/actuator/health"); + + return instanceWebClient.instance(instance) + .get() + .uri(customHealthPath) + .exchangeToMono(this::convertStatusInfo) + .timeout(getTimeoutWithMargin()) + .onErrorResume(this::handleError) + .map(instance::withStatusInfo); + } +} +``` + +**Client Configuration**: + +```yaml +spring: + boot: + admin: + client: + instance: + metadata: + health-path: /custom/health +``` + +### Example: Combine Multiple Health Checks + +```java +package com.example.admin; + +import java.util.Map; + +import reactor.core.publisher.Mono; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.domain.values.StatusInfo; +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.StatusUpdater; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +public class AggregatedStatusUpdater extends StatusUpdater { + + public AggregatedStatusUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + super(repository, instanceWebClient, apiMediaTypeHandler); + } + + @Override + protected Mono doUpdateStatus(Instance instance) { + if (!instance.isRegistered()) { + return Mono.empty(); + } + + // Check both health and readiness + Mono health = instanceWebClient.instance(instance) + .get() + .uri("/actuator/health") + .exchangeToMono(this::convertStatusInfo) + .onErrorResume(ex -> Mono.just(StatusInfo.ofOffline())); + + Mono readiness = instanceWebClient.instance(instance) + .get() + .uri("/actuator/health/readiness") + .exchangeToMono(this::convertStatusInfo) + .onErrorResume(ex -> Mono.just(StatusInfo.ofUp())); + + return Mono.zip(health, readiness, this::combineStatus) + .map(instance::withStatusInfo); + } + + private StatusInfo combineStatus(StatusInfo health, StatusInfo readiness) { + // If either is DOWN, overall is DOWN + if (health.isDown() || readiness.isDown()) { + return StatusInfo.ofDown(Map.of( + "health", health.getStatus(), + "readiness", readiness.getStatus() + )); + } + + // If either is OFFLINE, overall is OFFLINE + if (health.isOffline() || readiness.isOffline()) { + return StatusInfo.ofOffline(Map.of( + "health", health.getStatus(), + "readiness", readiness.getStatus() + )); + } + + // Otherwise UP + return StatusInfo.ofUp(Map.of( + "health", health.getStatus(), + "readiness", readiness.getStatus() + )); + } +} +``` + +--- + +## Custom InfoUpdater + +### Default Behavior + +`InfoUpdater` queries `/actuator/info`: + +```java +protected Mono doUpdateInfo(Instance instance) { + if (instance.getStatusInfo().isOffline() || instance.getStatusInfo().isUnknown()) { + return Mono.empty(); // Skip if offline + } + + if (!instance.getEndpoints().isPresent(Endpoint.INFO)) { + return Mono.empty(); // Skip if no info endpoint + } + + return instanceWebClient.instance(instance) + .get() + .uri(Endpoint.INFO) + .exchangeToMono(response -> convertInfo(instance, response)) + .onErrorResume(ex -> Mono.just(convertInfo(instance, ex))) + .map(instance::withInfo); +} +``` + +### Example: Custom Info Endpoint + +```java +package com.example.admin; + +import reactor.core.publisher.Mono; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.domain.values.Info; +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.InfoUpdater; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +public class CustomInfoUpdater extends InfoUpdater { + + public CustomInfoUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + super(repository, instanceWebClient, apiMediaTypeHandler); + } + + @Override + protected Mono doUpdateInfo(Instance instance) { + if (instance.getStatusInfo().isOffline() || instance.getStatusInfo().isUnknown()) { + return Mono.empty(); + } + + // Query custom info endpoint + String infoPath = instance.getRegistration() + .getMetadata() + .getOrDefault("info-path", "/actuator/info"); + + return instanceWebClient.instance(instance) + .get() + .uri(infoPath) + .exchangeToMono(response -> convertInfo(instance, response)) + .onErrorResume(ex -> Mono.just(Info.empty())) + .map(instance::withInfo); + } +} +``` + +### Example: Enrich Info with Metadata + +```java +package com.example.admin; + +import java.util.HashMap; +import java.util.Map; + +import reactor.core.publisher.Mono; +import org.springframework.web.reactive.function.client.ClientResponse; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.domain.values.Info; +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.InfoUpdater; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +public class EnrichedInfoUpdater extends InfoUpdater { + + public EnrichedInfoUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + super(repository, instanceWebClient, apiMediaTypeHandler); + } + + @Override + protected Mono convertInfo(Instance instance, ClientResponse response) { + return super.convertInfo(instance, response) + .map(info -> enrichInfo(instance, info)); + } + + private Info enrichInfo(Instance instance, Info originalInfo) { + Map enriched = new HashMap<>(originalInfo.getValues()); + + // Add metadata to info + enriched.put("metadata", instance.getRegistration().getMetadata()); + + // Add custom fields + enriched.put("registrationTime", + instance.getRegistration().getTimestamp().toString()); + + enriched.put("instanceId", instance.getId().getValue()); + + return Info.from(enriched); + } +} +``` + +--- + +## Custom Status Codes + +Define custom status codes for specific scenarios: + +```java +package com.example.admin; + +import java.util.Map; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatusCode; + +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.domain.values.StatusInfo; +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.StatusUpdater; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +@Configuration +public class CustomStatusConfig { + + @Bean + public StatusUpdater statusUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + + return new CustomStatusCodeUpdater(repository, instanceWebClient, apiMediaTypeHandler); + } + + static class CustomStatusCodeUpdater extends StatusUpdater { + + public CustomStatusCodeUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + super(repository, instanceWebClient, apiMediaTypeHandler); + } + + @Override + protected StatusInfo getStatusInfoFromStatus(HttpStatusCode httpStatus, Map body) { + // Custom status codes + if (body.containsKey("status")) { + String status = body.get("status").toString(); + + return switch (status) { + case "DEGRADED" -> StatusInfo.valueOf("DEGRADED", + Map.of("details", "Service running with reduced capacity")); + case "MAINTENANCE" -> StatusInfo.valueOf("OUT_OF_SERVICE", + Map.of("reason", "Under maintenance")); + case "WARMING_UP" -> StatusInfo.valueOf("RESTRICTED", + Map.of("reason", "Service is warming up")); + default -> super.getStatusInfoFromStatus(httpStatus, body); + }; + } + + return super.getStatusInfoFromStatus(httpStatus, body); + } + } +} +``` + +**Client Health Indicator**: + +```java +package com.example.client; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Component; + +@Component +public class CustomHealthIndicator implements HealthIndicator { + + private boolean warming = true; + + @Override + public Health health() { + if (warming) { + return Health.status("WARMING_UP") + .withDetail("progress", "50%") + .build(); + } + + return Health.up().build(); + } +} +``` + +--- + +## Advanced Scenarios + +### Scenario 1: External Health Check + +Query an external monitoring service: + +```java +package com.example.admin; + +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.domain.values.StatusInfo; +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.StatusUpdater; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +public class ExternalHealthCheckStatusUpdater extends StatusUpdater { + + private final WebClient externalMonitor; + + public ExternalHealthCheckStatusUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler, + WebClient.Builder webClientBuilder) { + super(repository, instanceWebClient, apiMediaTypeHandler); + this.externalMonitor = webClientBuilder + .baseUrl("https://monitoring-service.company.com") + .build(); + } + + @Override + protected Mono doUpdateStatus(Instance instance) { + String serviceName = instance.getRegistration().getName(); + + // Query external monitoring service + Mono externalStatus = externalMonitor.get() + .uri("/health/{service}", serviceName) + .retrieve() + .bodyToMono(ExternalHealthResponse.class) + .map(this::convertExternalHealth) + .onErrorResume(ex -> super.doUpdateStatus(instance) + .map(Instance::getStatusInfo)); + + return externalStatus.map(instance::withStatusInfo); + } + + private StatusInfo convertExternalHealth(ExternalHealthResponse response) { + return StatusInfo.valueOf(response.getStatus(), response.getDetails()); + } + + record ExternalHealthResponse(String status, Map details) { + public String getStatus() { return status; } + public Map getDetails() { return details; } + } +} +``` + +### Scenario 2: Synthetic Monitoring + +Perform synthetic transactions: + +```java +package com.example.admin; + +import java.util.Map; + +import reactor.core.publisher.Mono; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.domain.values.StatusInfo; +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.StatusUpdater; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +public class SyntheticMonitoringStatusUpdater extends StatusUpdater { + + public SyntheticMonitoringStatusUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + super(repository, instanceWebClient, apiMediaTypeHandler); + } + + @Override + protected Mono doUpdateStatus(Instance instance) { + // 1. Check health endpoint + Mono healthCheck = super.doUpdateStatus(instance) + .map(Instance::getStatusInfo); + + // 2. Perform synthetic transaction + Mono syntheticCheck = performSyntheticTransaction(instance); + + return Mono.zip(healthCheck, syntheticCheck, + (health, synthetic) -> { + if (health.isDown()) { + return health; // Already down + } + + if (!synthetic) { + return StatusInfo.valueOf("DEGRADED", + Map.of("reason", "Synthetic transaction failed")); + } + + return health; + }) + .map(instance::withStatusInfo); + } + + private Mono performSyntheticTransaction(Instance instance) { + // Example: Try to fetch a known endpoint + return instanceWebClient.instance(instance) + .get() + .uri("/api/health-check") + .retrieve() + .toBodilessEntity() + .map(response -> response.getStatusCode().is2xxSuccessful()) + .onErrorReturn(false); + } +} +``` + +### Scenario 3: Database-Backed Status + +Store and retrieve status from database: + +```java +package com.example.admin; + +import reactor.core.publisher.Mono; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.domain.values.StatusInfo; +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.StatusUpdater; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +public class DatabaseBackedStatusUpdater extends StatusUpdater { + + private final HealthStatusRepository healthStatusRepository; + + public DatabaseBackedStatusUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler, + HealthStatusRepository healthStatusRepository) { + super(repository, instanceWebClient, apiMediaTypeHandler); + this.healthStatusRepository = healthStatusRepository; + } + + @Override + protected Mono doUpdateStatus(Instance instance) { + return super.doUpdateStatus(instance) + .flatMap(updatedInstance -> { + // Save status to database + HealthStatus status = new HealthStatus( + instance.getId().getValue(), + updatedInstance.getStatusInfo().getStatus(), + updatedInstance.getStatusInfo().getDetails() + ); + + return healthStatusRepository.save(status) + .thenReturn(updatedInstance); + }); + } +} + +interface HealthStatusRepository { + Mono save(HealthStatus status); +} + +record HealthStatus(String instanceId, String status, Map details) {} +``` + +--- + +## Debugging + +### Enable Debug Logging + +```yaml +logging: + level: + de.codecentric.boot.admin.server.services.StatusUpdater: DEBUG + de.codecentric.boot.admin.server.services.InfoUpdater: DEBUG +``` + +**Log Output**: + +``` +DEBUG StatusUpdater - Update status for Instance{id=abc123, name=my-service} +DEBUG StatusUpdater - Status updated: UP +``` + +### Monitor Status Updates + +Listen to `InstanceStatusChangedEvent`: + +```java +package com.example.admin; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent; + +@Component +public class StatusChangeLogger { + + private static final Logger log = LoggerFactory.getLogger(StatusChangeLogger.class); + + @EventListener + public void onStatusChanged(InstanceStatusChangedEvent event) { + log.info("Status changed for instance {}: {} -> {}", + event.getInstance(), + event.getStatusInfo().getStatus(), + event.getInstance().getStatusInfo().getStatus()); + } +} +``` + +--- + +## Complete Example + +```java +package com.example.admin; + +import java.time.Duration; +import java.util.Map; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatusCode; + +import de.codecentric.boot.admin.server.domain.entities.InstanceRepository; +import de.codecentric.boot.admin.server.domain.values.StatusInfo; +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.StatusUpdater; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +@Configuration +public class CustomMonitoringConfig { + + @Bean + public StatusUpdater statusUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + + return new EnhancedStatusUpdater(repository, instanceWebClient, apiMediaTypeHandler) + .timeout(Duration.ofSeconds(15)); + } + + static class EnhancedStatusUpdater extends StatusUpdater { + + public EnhancedStatusUpdater( + InstanceRepository repository, + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + super(repository, instanceWebClient, apiMediaTypeHandler); + } + + @Override + protected StatusInfo getStatusInfoFromStatus(HttpStatusCode httpStatus, Map body) { + // Support custom status codes + if (body.containsKey("status")) { + String status = body.get("status").toString().toUpperCase(); + + return switch (status) { + case "DEGRADED" -> StatusInfo.valueOf("DEGRADED", body); + case "MAINTENANCE" -> StatusInfo.valueOf("OUT_OF_SERVICE", body); + case "STARTING" -> StatusInfo.valueOf("RESTRICTED", + Map.of("reason", "Service starting")); + default -> StatusInfo.valueOf(status, body); + }; + } + + // HTTP 503 = OUT_OF_SERVICE + if (httpStatus.value() == 503) { + return StatusInfo.valueOf("OUT_OF_SERVICE", body); + } + + // HTTP 429 = RESTRICTED + if (httpStatus.value() == 429) { + return StatusInfo.valueOf("RESTRICTED", + Map.of("reason", "Rate limited")); + } + + return super.getStatusInfoFromStatus(httpStatus, body); + } + } +} +``` + +--- + +## See Also + +- [Server Configuration](../../02-server/01-server.mdx) +- [Instance Filters](./01-instance-filters.md) +- [Spring Boot Actuator Health](https://docs.spring.io/spring-boot/reference/actuator/endpoints.html#actuator.endpoints.health) diff --git a/spring-boot-admin-docs/src/site/docs/06-customization/monitoring/_category_.json b/spring-boot-admin-docs/src/site/docs/06-customization/monitoring/_category_.json new file mode 100644 index 00000000000..f9e6f4c4772 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/06-customization/monitoring/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Monitoring", +} diff --git a/spring-boot-admin-docs/src/site/docs/06-customization/server/04-endpoint-detection.md b/spring-boot-admin-docs/src/site/docs/06-customization/server/04-endpoint-detection.md new file mode 100644 index 00000000000..766120ff31f --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/06-customization/server/04-endpoint-detection.md @@ -0,0 +1,893 @@ +--- + +sidebar_position: 4 +sidebar_custom_props: + icon: 'wrench' +--- + +# Custom Endpoint Detection + +Customize how Spring Boot Admin Server discovers actuator endpoints from registered instances. + +## Overview + +When a client registers, the Admin Server detects available actuator endpoints. This detection uses strategies that can +be customized: + +1. **QueryIndexEndpointStrategy** (default) - Queries `/actuator` index for links +2. **ProbeEndpointsStrategy** - Probes individual endpoints with OPTIONS requests +3. **ChainingStrategy** - Combines multiple strategies with fallback + +```mermaid +graph TD + A["Client Registers
POST /instances
{managementUrl: 'http://client:8080/actuator'}"] --> B[EndpointDetector] + B --> C["1. QueryIndexEndpointStrategy
GET /actuator → Read _links"] + B --> D["2. Fallback ProbeEndpointsStrategy
OPTIONS /actuator/health → 200 OK
OPTIONS /actuator/metrics → 200 OK
OPTIONS /actuator/info → 200 OK"] + C --> E["Instance.endpoints updated
{health, info, metrics, env, loggers, ...}"] + D --> E +``` + +--- + +## Default Behavior + +By default, Admin Server uses a **ChainingStrategy** that: + +1. First tries **QueryIndexEndpointStrategy** (Spring Boot 2.x+ with `/actuator` index) +2. Falls back to **ProbeEndpointsStrategy** (Spring Boot 1.x or if index query fails) + +**AdminServerAutoConfiguration.java**: + +```java +@Bean +@ConditionalOnMissingBean +public EndpointDetectionStrategy endpointDetectionStrategy( + InstanceWebClient instanceWebClient, + AdminServerProperties adminServerProperties, + ApiMediaTypeHandler apiMediaTypeHandler) { + + return new ChainingStrategy( + new QueryIndexEndpointStrategy(instanceWebClient, apiMediaTypeHandler), + new ProbeEndpointsStrategy(instanceWebClient, + adminServerProperties.getProbedEndpoints()) + ); +} +``` + +--- + +## QueryIndexEndpointStrategy + +Queries the actuator index at `/actuator` to discover endpoints. + +### How It Works + +**Request**: + +```http +GET /actuator HTTP/1.1 +Accept: application/vnd.spring-boot.actuator.v3+json +``` + +**Response**: + +```json +{ + "_links": { + "self": { + "href": "http://localhost:8080/actuator", + "templated": false + }, + "health": { + "href": "http://localhost:8080/actuator/health", + "templated": false + }, + "info": { + "href": "http://localhost:8080/actuator/info", + "templated": false + }, + "metrics": { + "href": "http://localhost:8080/actuator/metrics/{requiredMetricName}", + "templated": true + } + } +} +``` + +**Extracted Endpoints**: + +- `health` → `http://localhost:8080/actuator/health` +- `info` → `http://localhost:8080/actuator/info` +- `metrics` is **excluded** (templated) + +### Use Only QueryIndexEndpointStrategy + +```java +package com.example.admin; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.endpoints.EndpointDetectionStrategy; +import de.codecentric.boot.admin.server.services.endpoints.QueryIndexEndpointStrategy; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +@Configuration +public class EndpointDetectionConfig { + + @Bean + public EndpointDetectionStrategy endpointDetectionStrategy( + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + + return new QueryIndexEndpointStrategy(instanceWebClient, apiMediaTypeHandler); + } +} +``` + +**When to use**: + +- All clients are Spring Boot 2.x or newer +- `/actuator` index is available on all clients +- Want fastest detection + +--- + +## ProbeEndpointsStrategy + +Probes individual endpoints using HTTP OPTIONS requests. + +### How It Works + +For each endpoint in the probed list: + +```http +OPTIONS /actuator/health HTTP/1.1 +``` + +If response is `2xx`, the endpoint is considered available. + +### Configuration + +**application.yml**: + +```yaml +spring: + boot: + admin: + probed-endpoints: + - health + - info + - metrics + - env + - loggers + - logfile + - threaddump + - heapdump +``` + +### Custom Endpoint Paths + +If endpoint ID differs from path, use `id:path` syntax: + +```yaml +spring: + boot: + admin: + probed-endpoints: + - health:ping # Endpoint ID "health" at path "/actuator/ping" + - metrics:stats # Endpoint ID "metrics" at path "/actuator/stats" + - custom:my-custom # Endpoint ID "custom" at path "/actuator/my-custom" +``` + +### Use Only ProbeEndpointsStrategy + +```java +package com.example.admin; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.config.AdminServerProperties; +import de.codecentric.boot.admin.server.services.endpoints.EndpointDetectionStrategy; +import de.codecentric.boot.admin.server.services.endpoints.ProbeEndpointsStrategy; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +@Configuration +public class EndpointDetectionConfig { + + @Bean + public EndpointDetectionStrategy endpointDetectionStrategy( + InstanceWebClient instanceWebClient, + AdminServerProperties properties) { + + return new ProbeEndpointsStrategy( + instanceWebClient, + properties.getProbedEndpoints() + ); + } +} +``` + +**When to use**: + +- Supporting Spring Boot 1.x applications +- Actuator index is disabled/unavailable +- Need to detect specific custom endpoints + +--- + +## ChainingStrategy + +Combines multiple strategies with fallback. + +### How It Works + +Tries strategies in order until one succeeds: + +```java +ChainingStrategy( + new QueryIndexEndpointStrategy(...), // Try first + new ProbeEndpointsStrategy(...) // Fallback +) +``` + +If first strategy returns empty, tries next strategy. + +### Custom Chaining + +```java +package com.example.admin; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import de.codecentric.boot.admin.server.config.AdminServerProperties; +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.endpoints.ChainingStrategy; +import de.codecentric.boot.admin.server.services.endpoints.EndpointDetectionStrategy; +import de.codecentric.boot.admin.server.services.endpoints.ProbeEndpointsStrategy; +import de.codecentric.boot.admin.server.services.endpoints.QueryIndexEndpointStrategy; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +@Configuration +public class EndpointDetectionConfig { + + @Bean + public EndpointDetectionStrategy endpointDetectionStrategy( + InstanceWebClient instanceWebClient, + AdminServerProperties properties, + ApiMediaTypeHandler apiMediaTypeHandler) { + + return new ChainingStrategy( + new QueryIndexEndpointStrategy(instanceWebClient, apiMediaTypeHandler), + new ProbeEndpointsStrategy(instanceWebClient, properties.getProbedEndpoints()), + new CustomEndpointStrategy() // Your custom strategy as last resort + ); + } +} +``` + +--- + +## Custom EndpointDetectionStrategy + +Implement `EndpointDetectionStrategy` interface for custom detection logic. + +### Interface + +```java +package de.codecentric.boot.admin.server.services.endpoints; + +import reactor.core.publisher.Mono; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.values.Endpoints; + +public interface EndpointDetectionStrategy { + + Mono detectEndpoints(Instance instance); + +} +``` + +### Example: Static Endpoint Strategy + +Define endpoints based on metadata: + +```java +package com.example.admin; + +import reactor.core.publisher.Mono; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.values.Endpoint; +import de.codecentric.boot.admin.server.domain.values.Endpoints; +import de.codecentric.boot.admin.server.services.endpoints.EndpointDetectionStrategy; + +public class MetadataEndpointStrategy implements EndpointDetectionStrategy { + + @Override + public Mono detectEndpoints(Instance instance) { + String managementUrl = instance.getRegistration().getManagementUrl(); + if (managementUrl == null) { + return Mono.empty(); + } + + // Read endpoints from metadata + String endpointList = instance.getRegistration() + .getMetadata() + .get("endpoints"); + + if (endpointList == null || endpointList.isBlank()) { + return Mono.empty(); + } + + // Parse comma-separated endpoint IDs + List endpoints = Arrays.stream(endpointList.split(",")) + .map(String::trim) + .map(id -> Endpoint.of(id, managementUrl + "/" + id)) + .toList(); + + return Mono.just(Endpoints.of(endpoints)); + } +} +``` + +**Client Configuration**: + +```yaml +spring: + boot: + admin: + client: + instance: + metadata: + endpoints: health,info,metrics,env +``` + +**Server Configuration**: + +```java +@Bean +public EndpointDetectionStrategy endpointDetectionStrategy() { + return new ChainingStrategy( + new MetadataEndpointStrategy(), + new QueryIndexEndpointStrategy(...) + ); +} +``` + +### Example: Service-Specific Endpoints + +Different endpoint detection based on service name: + +```java +package com.example.admin; + +import reactor.core.publisher.Mono; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.values.Endpoint; +import de.codecentric.boot.admin.server.domain.values.Endpoints; +import de.codecentric.boot.admin.server.services.endpoints.EndpointDetectionStrategy; + +public class ServiceSpecificEndpointStrategy implements EndpointDetectionStrategy { + + private final Map> serviceEndpoints; + + public ServiceSpecificEndpointStrategy() { + this.serviceEndpoints = Map.of( + "payment-service", List.of("health", "info", "metrics", "payments"), + "user-service", List.of("health", "info", "metrics", "users"), + "legacy-service", List.of("health", "info") // Limited endpoints + ); + } + + @Override + public Mono detectEndpoints(Instance instance) { + String serviceName = instance.getRegistration().getName(); + String managementUrl = instance.getRegistration().getManagementUrl(); + + if (managementUrl == null) { + return Mono.empty(); + } + + List endpointIds = serviceEndpoints.get(serviceName); + if (endpointIds == null) { + return Mono.empty(); // Fall back to next strategy + } + + List endpoints = endpointIds.stream() + .map(id -> Endpoint.of(id, managementUrl + "/" + id)) + .toList(); + + return Mono.just(Endpoints.of(endpoints)); + } +} +``` + +### Example: Database-Driven Endpoints + +Load endpoint configuration from database: + +```java +package com.example.admin; + +import java.util.List; + +import reactor.core.publisher.Mono; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.values.Endpoint; +import de.codecentric.boot.admin.server.domain.values.Endpoints; +import de.codecentric.boot.admin.server.services.endpoints.EndpointDetectionStrategy; + +public class DatabaseEndpointStrategy implements EndpointDetectionStrategy { + + private final EndpointConfigRepository endpointConfigRepository; + + public DatabaseEndpointStrategy(EndpointConfigRepository repository) { + this.endpointConfigRepository = repository; + } + + @Override + public Mono detectEndpoints(Instance instance) { + String serviceName = instance.getRegistration().getName(); + String managementUrl = instance.getRegistration().getManagementUrl(); + + if (managementUrl == null) { + return Mono.empty(); + } + + return endpointConfigRepository.findByServiceName(serviceName) + .map(config -> { + List endpoints = config.getEndpointIds().stream() + .map(id -> Endpoint.of(id, managementUrl + "/" + id)) + .toList(); + return Endpoints.of(endpoints); + }) + .switchIfEmpty(Mono.empty()); + } +} + +@Repository +interface EndpointConfigRepository extends ReactiveMongoRepository { + Mono findByServiceName(String serviceName); +} + +@Document +class EndpointConfig { + private String serviceName; + private List endpointIds; + // getters/setters +} +``` + +### Example: HTTP-Based Discovery + +Fetch endpoints from custom discovery endpoint: + +```java +package com.example.admin; + +import reactor.core.publisher.Mono; +import org.springframework.web.reactive.function.client.WebClient; + +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.values.Endpoint; +import de.codecentric.boot.admin.server.domain.values.Endpoints; +import de.codecentric.boot.admin.server.services.endpoints.EndpointDetectionStrategy; + +public class DiscoveryServiceEndpointStrategy implements EndpointDetectionStrategy { + + private final WebClient webClient; + + public DiscoveryServiceEndpointStrategy(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder.baseUrl("http://discovery-service").build(); + } + + @Override + public Mono detectEndpoints(Instance instance) { + String serviceName = instance.getRegistration().getName(); + String managementUrl = instance.getRegistration().getManagementUrl(); + + if (managementUrl == null) { + return Mono.empty(); + } + + return webClient.get() + .uri("/services/{name}/endpoints", serviceName) + .retrieve() + .bodyToFlux(String.class) + .collectList() + .map(endpointIds -> { + List endpoints = endpointIds.stream() + .map(id -> Endpoint.of(id, managementUrl + "/" + id)) + .toList(); + return Endpoints.of(endpoints); + }) + .onErrorResume(e -> Mono.empty()); + } +} +``` + +--- + +## Endpoint Detection Lifecycle + +### When Detection Occurs + +Endpoints are detected: + +1. **After instance registration** (InstanceRegisteredEvent) +2. **When endpoints are first accessed** (if not yet detected) +3. **Periodically** (can be triggered manually) + +### Trigger Manual Detection + +```java +@Autowired +private EndpointDetector endpointDetector; + +public void refreshEndpoints(InstanceId instanceId) { + endpointDetector.detectEndpoints(instanceId).subscribe(); +} +``` + +--- + +## Debugging Endpoint Detection + +### Enable Debug Logging + +```yaml +logging: + level: + de.codecentric.boot.admin.server.services.EndpointDetector: DEBUG + de.codecentric.boot.admin.server.services.endpoints: DEBUG +``` + +**Log Output**: + +``` +DEBUG EndpointDetector - Detect endpoints for Instance{id=abc123} +DEBUG QueryIndexEndpointStrategy - Querying actuator-index for instance abc123 on 'http://client:8080/actuator' successful. +DEBUG EndpointDetector - Detected endpoints: [health, info, metrics, env] +``` + +### Check Detected Endpoints + +**API**: + +```bash +curl http://admin-server:8080/instances/{id} | jq '.endpoints' +``` + +**Response**: + +```json +[ + { + "id": "health", + "url": "http://localhost:8080/actuator/health" + }, + { + "id": "info", + "url": "http://localhost:8080/actuator/info" + }, + { + "id": "metrics", + "url": "http://localhost:8080/actuator/metrics" + } +] +``` + +--- + +## Advanced Scenarios + +### Scenario 1: Legacy Spring Boot 1.x Support + +**Configuration**: + +```yaml +spring: + boot: + admin: + probed-endpoints: + # Spring Boot 1.x endpoints + - health + - info + - metrics + - env + - trace:httptrace + - dump:threaddump +``` + +**Strategy**: + +```java +@Bean +public EndpointDetectionStrategy endpointDetectionStrategy( + InstanceWebClient instanceWebClient, + AdminServerProperties properties) { + + // Only use probing for legacy apps + return new ProbeEndpointsStrategy( + instanceWebClient, + properties.getProbedEndpoints() + ); +} +``` + +### Scenario 2: Mixed Spring Boot Versions + +**Strategy**: + +```java +@Bean +public EndpointDetectionStrategy endpointDetectionStrategy( + InstanceWebClient instanceWebClient, + AdminServerProperties properties, + ApiMediaTypeHandler apiMediaTypeHandler) { + + return new ChainingStrategy( + // Try modern Spring Boot 2.x+ first + new QueryIndexEndpointStrategy(instanceWebClient, apiMediaTypeHandler), + // Fall back to probing for legacy apps + new ProbeEndpointsStrategy(instanceWebClient, properties.getProbedEndpoints()) + ); +} +``` + +### Scenario 3: Custom Actuator Path + +**Client** (custom actuator base path): + +```yaml +management: + endpoints: + web: + base-path: /management # Not /actuator +``` + +**Server Strategy**: + +```java +public class CustomPathEndpointStrategy implements EndpointDetectionStrategy { + + @Override + public Mono detectEndpoints(Instance instance) { + String managementUrl = instance.getRegistration().getManagementUrl(); + + // Adjust for custom base path + if (managementUrl != null && !managementUrl.endsWith("/actuator")) { + // Query custom index endpoint + return queryIndex(instance, managementUrl); + } + + return Mono.empty(); + } +} +``` + +### Scenario 4: Conditional Detection Based on Metadata + +```java +public class ConditionalEndpointStrategy implements EndpointDetectionStrategy { + + private final QueryIndexEndpointStrategy queryStrategy; + private final ProbeEndpointsStrategy probeStrategy; + + @Override + public Mono detectEndpoints(Instance instance) { + String version = instance.getRegistration() + .getMetadata() + .get("spring-boot-version"); + + if (version != null && version.startsWith("1.")) { + // Use probing for Spring Boot 1.x + return probeStrategy.detectEndpoints(instance); + } else { + // Use index query for Spring Boot 2.x+ + return queryStrategy.detectEndpoints(instance); + } + } +} +``` + +--- + +## Performance Considerations + +### QueryIndexEndpointStrategy + +**Pros**: + +- Single HTTP request +- Fast detection +- Accurate (no false positives) + +**Cons**: + +- Requires Spring Boot 2.x+ +- Requires `/actuator` index enabled + +### ProbeEndpointsStrategy + +**Pros**: + +- Works with any Spring Boot version +- Detects custom endpoints + +**Cons**: + +- Multiple HTTP requests (one per endpoint) +- Slower detection +- Potential false positives if OPTIONS not supported + +### Optimization + +Limit probed endpoints to essentials: + +```yaml +spring: + boot: + admin: + probed-endpoints: + - health + - info + - metrics + # Remove rarely-used endpoints +``` + +--- + +## Troubleshooting + +### Issue: No endpoints detected + +**Cause**: Detection strategy failing. + +**Debug**: + +```yaml +logging: + level: + de.codecentric.boot.admin.server.services.endpoints: DEBUG +``` + +**Check**: + +1. `managementUrl` is set +2. Instance is reachable +3. Actuator endpoints are exposed + +### Issue: Wrong endpoints detected + +**Cause**: Probing detecting endpoints that don't exist. + +**Solution**: Use `QueryIndexEndpointStrategy` only: + +```java +@Bean +public EndpointDetectionStrategy endpointDetectionStrategy( + InstanceWebClient instanceWebClient, + ApiMediaTypeHandler apiMediaTypeHandler) { + return new QueryIndexEndpointStrategy(instanceWebClient, apiMediaTypeHandler); +} +``` + +### Issue: Custom endpoints not detected + +**Cause**: Custom endpoints not in probed list or not in actuator index. + +**Solution**: Add to probed endpoints: + +```yaml +spring: + boot: + admin: + probed-endpoints: + - health + - info + - my-custom-endpoint +``` + +Or create custom strategy. + +--- + +## See Also + +- [Server Configuration](../../02-server/01-server.mdx) +- [Spring Boot Actuator Endpoints](https://docs.spring.io/spring-boot/reference/actuator/endpoints.html) + +--- + +## Complete Example + +```java +package com.example.admin; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import reactor.core.publisher.Mono; + +import de.codecentric.boot.admin.server.config.AdminServerProperties; +import de.codecentric.boot.admin.server.domain.entities.Instance; +import de.codecentric.boot.admin.server.domain.values.Endpoint; +import de.codecentric.boot.admin.server.domain.values.Endpoints; +import de.codecentric.boot.admin.server.services.ApiMediaTypeHandler; +import de.codecentric.boot.admin.server.services.endpoints.ChainingStrategy; +import de.codecentric.boot.admin.server.services.endpoints.EndpointDetectionStrategy; +import de.codecentric.boot.admin.server.services.endpoints.ProbeEndpointsStrategy; +import de.codecentric.boot.admin.server.services.endpoints.QueryIndexEndpointStrategy; +import de.codecentric.boot.admin.server.web.client.InstanceWebClient; + +@Configuration +public class EndpointDetectionConfig { + + @Bean + public EndpointDetectionStrategy endpointDetectionStrategy( + InstanceWebClient instanceWebClient, + AdminServerProperties properties, + ApiMediaTypeHandler apiMediaTypeHandler) { + + return new ChainingStrategy( + // 1. Try metadata-based detection + new MetadataEndpointStrategy(), + // 2. Try standard actuator index query + new QueryIndexEndpointStrategy(instanceWebClient, apiMediaTypeHandler), + // 3. Fall back to probing + new ProbeEndpointsStrategy(instanceWebClient, properties.getProbedEndpoints()) + ); + } + + /** + * Detect endpoints from instance metadata + */ + static class MetadataEndpointStrategy implements EndpointDetectionStrategy { + + @Override + public Mono detectEndpoints(Instance instance) { + String managementUrl = instance.getRegistration().getManagementUrl(); + if (managementUrl == null) { + return Mono.empty(); + } + + String endpointList = instance.getRegistration() + .getMetadata() + .get("endpoints"); + + if (endpointList == null || endpointList.isBlank()) { + return Mono.empty(); + } + + List endpoints = Arrays.stream(endpointList.split(",")) + .map(String::trim) + .map(id -> Endpoint.of(id, managementUrl + "/" + id)) + .toList(); + + return Mono.just(Endpoints.of(endpoints)); + } + } +} +``` + +**Client Configuration** (optional metadata): + +```yaml +spring: + boot: + admin: + client: + instance: + metadata: + endpoints: health,info,metrics,custom +``` diff --git a/spring-boot-admin-docs/src/site/docs/06-customization/server/_category_.json b/spring-boot-admin-docs/src/site/docs/06-customization/server/_category_.json new file mode 100644 index 00000000000..8e1e89bc700 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/06-customization/server/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Server" +} diff --git a/spring-boot-admin-docs/src/site/docs/third-party/_category_.json b/spring-boot-admin-docs/src/site/docs/08-third-party/_category_.json similarity index 71% rename from spring-boot-admin-docs/src/site/docs/third-party/_category_.json rename to spring-boot-admin-docs/src/site/docs/08-third-party/_category_.json index 031517b44a6..e566ce5db8b 100644 --- a/spring-boot-admin-docs/src/site/docs/third-party/_category_.json +++ b/spring-boot-admin-docs/src/site/docs/08-third-party/_category_.json @@ -1,4 +1,4 @@ { - "position": 5, + "position": 8, "label": "Third Party Integrations" } diff --git a/spring-boot-admin-docs/src/site/docs/08-third-party/index.md b/spring-boot-admin-docs/src/site/docs/08-third-party/index.md new file mode 100644 index 00000000000..5b3741ac1be --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/08-third-party/index.md @@ -0,0 +1,10 @@ +--- +sidebar_custom_props: + icon: 'puzzle' +--- + +import DocCardList from '@theme/DocCardList'; + +# Third Party Integrations + + diff --git a/spring-boot-admin-docs/src/site/docs/08-third-party/pyctuator.md b/spring-boot-admin-docs/src/site/docs/08-third-party/pyctuator.md new file mode 100644 index 00000000000..8a357f6e7da --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/08-third-party/pyctuator.md @@ -0,0 +1,44 @@ +--- +sidebar_custom_props: + icon: 'python' +--- + +# Pyctuator + +You can easily integrate Spring Boot Admin with [Flask](https://flask.palletsprojects.com) +or [FastAPI](https://fastapi.tiangolo.com/) Python applications using +the [Pyctuator](https://github.com/SolarEdgeTech/pyctuator) project. + +The following steps uses Flask, but other web frameworks are supported as well. See Pyctuator’s documentation for an +updated list of supported frameworks and features. + +1. Install the pyctuator package: + +```bash +pip install pyctuator +``` + +2. Enable pyctuator by pointing it to your Flask app and letting it know where Spring Boot Admin is running: + +```python title="app.py" +import os +from flask import Flask +from pyctuator.pyctuator import Pyctuator +app_name = "Flask App with Pyctuator" +app = Flask(app_name) +@app.route("/") +def hello(): + return "Hello World!" +Pyctuator( + app, + app_name, + app_url="http://example-app.com", + pyctuator_endpoint_url="http://example-app.com/pyctuator", + registration_url=os.getenv("SPRING_BOOT_ADMIN_URL") +) +app.run() +``` + +For further details and examples, see +Pyctuator’s [documentation](https://github.com/SolarEdgeTech/pyctuator/blob/master/README.md) +and [examples](https://github.com/SolarEdgeTech/pyctuator/tree/master/examples). diff --git a/spring-boot-admin-docs/src/site/docs/09-samples/10-sample-servlet.md b/spring-boot-admin-docs/src/site/docs/09-samples/10-sample-servlet.md new file mode 100644 index 00000000000..cb31e4707bb --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/09-samples/10-sample-servlet.md @@ -0,0 +1,668 @@ +--- + +sidebar_position: 10 +sidebar_custom_props: + icon: 'file-code' +--- + +# Servlet Sample + +The Servlet sample demonstrates a complete Spring Boot Admin Server deployment using traditional servlet-based Spring +MVC. This is the most feature-rich sample, showcasing security, custom UI extensions, notifications, and +self-monitoring. + +## Overview + +**Location**: `spring-boot-admin-samples/spring-boot-admin-sample-servlet/` + +**Features**: + +- Traditional servlet-based deployment (Spring MVC) +- Spring Security integration with form login +- Self-monitoring using Admin Client +- Mail notifications configured +- Custom UI extensions included +- Custom notifier implementation +- HTTP exchange tracking +- Audit event logging +- Session persistence with JDBC +- Custom actuator endpoint +- JMX support via Jolokia + +## Prerequisites + +- Java 17 or higher +- Maven 3.6+ +- Optional: Mail server for notifications + +## Running the Sample + +### Quick Start + +```bash +cd spring-boot-admin-samples/spring-boot-admin-sample-servlet +mvn spring-boot:run +``` + +Access the application at: `http://localhost:8080` + +### With Security Enabled + +```bash +mvn spring-boot:run -Dspring-boot.run.profiles=secure +``` + +**Login Credentials**: + +- Username: `user` +- Password: `password` + +### Change Port + +```bash +SERVER_PORT=9090 mvn spring-boot:run +``` + +## Project Structure + +### Dependencies + +Key dependencies from `pom.xml`: + +```xml + + + + de.codecentric + spring-boot-admin-starter-server + + + + + de.codecentric + spring-boot-admin-starter-client + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + + + org.springframework.boot + spring-boot-starter-mail + + + + + org.springframework.session + spring-session-jdbc + + + + + de.codecentric + spring-boot-admin-sample-custom-ui + + +``` + +### Main Application Class + +```java title="SpringBootAdminServletApplication.java" +@SpringBootApplication +@EnableAdminServer +@EnableCaching +public class SpringBootAdminServletApplication { + + static void main(String[] args) { + SpringApplication app = new SpringApplication( + SpringBootAdminServletApplication.class + ); + app.setApplicationStartup(new BufferingApplicationStartup(1500)); + app.run(args); + } + + @Bean + public CustomNotifier customNotifier(InstanceRepository repository) { + return new CustomNotifier(repository); + } + + @Bean + public HttpHeadersProvider customHttpHeadersProvider() { + return (instance) -> { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add("X-CUSTOM", "My Custom Value"); + return httpHeaders; + }; + } + + @Bean + public InstanceExchangeFilterFunction auditLog() { + return (instance, request, next) -> next.exchange(request) + .doOnSubscribe((s) -> { + if (HttpMethod.DELETE.equals(request.method()) + || HttpMethod.POST.equals(request.method())) { + log.info("{} for {} on {}", + request.method(), instance.getId(), request.url()); + } + }); + } +} +``` + +**Key Points**: + +- `@EnableAdminServer` activates Admin Server functionality +- Custom HTTP headers added to all instance requests +- Audit logging for DELETE/POST operations +- Application startup tracking enabled + +## Configuration + +### Application Configuration + +```yaml title="application.yml" +spring: + application: + name: spring-boot-admin-sample-servlet + + boot: + admin: + client: + url: http://localhost:8080 # Self-registration + instance: + service-host-type: IP + metadata: + tags: + environment: test + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS + shutdown: + enabled: true + restart: + enabled: true + +logging: + file: + name: "target/boot-admin-sample-servlet.log" + level: + de.codecentric: info +``` + +### Static Instance Configuration + +The sample includes multiple static instances with creative names: + +```yaml +spring: + cloud: + discovery: + client: + simple: + instances: + "Captain Debugbeard": + - uri: http://localhost:8080 + metadata: + management.context-path: /actuator + group: Pirates of the Caribbean Bean + + "Stack Overflow Sorcerer": + - uri: http://localhost:8080 + metadata: + management.context-path: /actuator + group: Wizarding World +``` + +## Security Configuration + +### Spring Security Setup + +```java title="SecuritySecureConfig.java" +@Profile("secure") +@Configuration +public class SecuritySecureConfig { + + private final AdminServerProperties adminServer; + + @Bean + protected SecurityFilterChain filterChain(HttpSecurity http) + throws Exception { + SavedRequestAwareAuthenticationSuccessHandler successHandler = + new SavedRequestAwareAuthenticationSuccessHandler(); + successHandler.setTargetUrlParameter("redirectTo"); + successHandler.setDefaultTargetUrl(adminServer.path("/")); + + http.authorizeHttpRequests((authorizeRequests) -> authorizeRequests + .requestMatchers(adminServer.path("/assets/**")) + .permitAll() // Allow static resources + .requestMatchers(adminServer.path("/actuator/info")) + .permitAll() + .requestMatchers(adminServer.path("/actuator/health")) + .permitAll() + .requestMatchers(adminServer.path("/login")) + .permitAll() + .anyRequest() + .authenticated()) + .formLogin((formLogin) -> formLogin + .loginPage(adminServer.path("/login")) + .successHandler(successHandler)) + .logout((logout) -> logout + .logoutUrl(adminServer.path("/logout"))) + .httpBasic(Customizer.withDefaults()); + + // CSRF Configuration + http.csrf((csrf) -> csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .ignoringRequestMatchers( + adminServer.path("/instances"), // Instance registration + adminServer.path("/instances/*"), // Instance deregistration + adminServer.path("/actuator/**") // Actuator endpoints + )); + + http.rememberMe((rememberMe) -> rememberMe + .key(UUID.randomUUID().toString()) + .tokenValiditySeconds(1209600)); // 14 days + + return http.build(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService( + PasswordEncoder passwordEncoder) { + UserDetails user = User.withUsername("user") + .password(passwordEncoder.encode("password")) + .roles("USER") + .build(); + return new InMemoryUserDetailsManager(user); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} +``` + +**Security Features**: + +1. Form-based login with custom page +2. HTTP Basic authentication support +3. CSRF protection with token repository +4. Remember-me functionality (14 days) +5. Static resources publicly accessible +6. Health/info endpoints publicly accessible + +## Custom Notifier + +The sample includes a custom notifier implementation: + +```java title="CustomNotifier.java" +public class CustomNotifier extends AbstractEventNotifier { + + private static final Logger LOGGER = + LoggerFactory.getLogger(CustomNotifier.class); + + public CustomNotifier(InstanceRepository repository) { + super(repository); + } + + @Override + protected Mono doNotify(InstanceEvent event, Instance instance) { + return Mono.fromRunnable(() -> { + if (event instanceof InstanceStatusChangedEvent statusChangedEvent) { + LOGGER.info("Instance {} ({}) is {}", + instance.getRegistration().getName(), + event.getInstance(), + statusChangedEvent.getStatusInfo().getStatus()); + } else { + LOGGER.info("Instance {} ({}) {}", + instance.getRegistration().getName(), + event.getInstance(), + event.getType()); + } + }); + } +} +``` + +### Notifier Configuration + +```java title="NotifierConfig.java" +@Configuration +public class NotifierConfig { + + @Bean + public FilteringNotifier filteringNotifier() { + CompositeNotifier delegate = new CompositeNotifier( + otherNotifiers.getIfAvailable(Collections::emptyList) + ); + return new FilteringNotifier(delegate, repository); + } + + @Primary + @Bean(initMethod = "start", destroyMethod = "stop") + public RemindingNotifier remindingNotifier() { + RemindingNotifier notifier = new RemindingNotifier( + filteringNotifier(), repository + ); + notifier.setReminderPeriod(Duration.ofMinutes(10)); + notifier.setCheckReminderInverval(Duration.ofSeconds(10)); + return notifier; + } +} +``` + +**Notification Features**: + +- Custom event logging +- Filtering notifier for selective notifications +- Reminding notifier (sends reminders every 10 minutes) +- Composable notifier chain + +## UI Customizations + +### External Views + +The sample demonstrates various external view configurations: + +```yaml +spring: + boot: + admin: + ui: + external-views: + # Simple link + - label: "🚀" + url: "https://codecentric.de" + order: 2000 + + # Dropdown with links + - label: Resources + children: + - label: "📖 Docs" + url: https://codecentric.github.io/spring-boot-admin/ + - label: "📦 Maven" + url: https://search.maven.org/... + - label: "🐙 GitHub" + url: https://github.com/codecentric/spring-boot-admin + + # Iframe view + - label: "🎅 Is it christmas" + url: https://isitchristmas.com + iframe: true +``` + +### View Settings + +```yaml +spring: + boot: + admin: + ui: + view-settings: + - name: "journal" + enabled: false +``` + +## Session Management + +The sample uses JDBC-based session persistence: + +```java +@Bean +public EmbeddedDatabase dataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("org/springframework/session/jdbc/schema-hsqldb.sql") + .build(); +} +``` + +**Benefits**: + +- Session persistence across restarts +- Support for clustered deployments +- Built-in session cleanup + +## Testing the Sample + +### Access the UI + +1. Start the application +2. Navigate to `http://localhost:8080` +3. Login (if secure profile is active) +4. View the registered instances + +### Test Self-Monitoring + +The application monitors itself via the Admin Client. You should see: + +- Application name: `spring-boot-admin-sample-servlet` +- Status: UP +- All actuator endpoints available +- Custom metadata and tags + +### Test Notifications + +Monitor the logs for custom notification events: + +``` +INFO - Instance spring-boot-admin-sample-servlet (...) is UP +INFO - Instance spring-boot-admin-sample-servlet (...) ENDPOINTS_DETECTED +``` + +### Test Custom Headers + +All requests to instances include the custom header `X-CUSTOM: My Custom Value`. + +### Test External Views + +Click the external view links in the navigation: + +- Rocket emoji (🚀) - Opens codecentric.de +- Resources dropdown - Multiple documentation links +- "Is it christmas" - Iframe view + +## Build and Deploy + +### Build JAR + +```bash +mvn clean package +``` + +Produces: `target/spring-boot-admin-sample-servlet.jar` + +### Run JAR + +```bash +java -jar target/spring-boot-admin-sample-servlet.jar +``` + +### With Profiles + +```bash +java -jar target/spring-boot-admin-sample-servlet.jar \ + --spring.profiles.active=secure +``` + +### Deployment Considerations + +When deploying, consider: + +1. **External Database**: Replace HSQLDB with PostgreSQL/MySQL +2. **Mail Server**: Configure SMTP for real notifications +3. **Security**: Use external user store (LDAP/OAuth2) +4. **HTTPS**: Enable TLS/SSL +5. **Session Store**: Use Redis or external database + +Example deployment configuration: + +```yaml +spring: + datasource: + url: jdbc:postgresql://localhost:5432/admin + username: admin + password: ${DB_PASSWORD} + + mail: + host: smtp.company.com + port: 587 + username: ${SMTP_USER} + password: ${SMTP_PASSWORD} + +server: + port: 8443 + ssl: + enabled: true + key-store: classpath:keystore.p12 + key-store-password: ${KEYSTORE_PASSWORD} +``` + +## Customization Ideas + +### Add Custom Actuator Endpoint + +Create a custom endpoint (example included): + +```java +@Component +@Endpoint(id = "custom") +public class CustomEndpoint { + + @ReadOperation + public Map customEndpoint() { + return Map.of( + "message", "Hello from custom endpoint", + "timestamp", Instant.now() + ); + } +} +``` + +### Add Database Notifier + +Replace log-based notifier with database persistence: + +```java +public class DatabaseNotifier extends AbstractEventNotifier { + + private final EventRepository eventRepository; + + @Override + protected Mono doNotify(InstanceEvent event, Instance instance) { + return Mono.fromRunnable(() -> { + eventRepository.save(new EventEntity(event, instance)); + }); + } +} +``` + +### Add Custom Metadata + +Enhance self-registration with custom metadata: + +```yaml +spring: + boot: + admin: + client: + instance: + metadata: + version: ${project.version} + region: us-east-1 + team: platform + tags: + environment: production + cost-center: engineering +``` + +## Troubleshooting + +### Port Already in Use + +```bash +# Change port +SERVER_PORT=9090 mvn spring-boot:run +``` + +### Security Issues + +If you cannot access the UI: + +1. Check if `secure` profile is active +2. Verify credentials: `user` / `password` +3. Check CSRF token in browser console +4. Clear browser cookies + +### Self-Registration Fails + +1. Verify client URL matches server URL +2. Check actuator endpoints are exposed +3. Review logs for connection errors +4. Ensure security permits instance registration + +### Mail Notifications Not Working + +1. Configure valid SMTP server +2. Check firewall/network access +3. Verify credentials +4. Enable debug logging: + +```yaml +logging: + level: + org.springframework.mail: DEBUG +``` + +## Key Takeaways + +This sample demonstrates: + +✅ **Complete Deployment Setup** + +- Security, session management, notifications + +✅ **Self-Monitoring Pattern** + +- Admin Server monitoring itself via Admin Client + +✅ **Extensibility** + +- Custom notifiers, endpoints, headers, UI views + +✅ **Best Practices** + +- Profile-based configuration +- CSRF protection +- Proper security configuration + +## Next Steps + +- Explore [Reactive Sample](./20-sample-reactive.md) for WebFlux alternative +- Review [Security Documentation](../05-security/) for deployment hardening +- Check [Customization Guide](../06-customization/) for more extensions +- See [Notification Configuration](../02-server/notifications/) for notifier options + +## See Also + +- [Server Configuration](../02-server/01-server.mdx) +- [Client Registration](../03-client/20-registration.md) +- [UI Customization](../06-customization/ui/) +- [Spring Security Integration](../05-security/10-server-authentication.md) diff --git a/spring-boot-admin-docs/src/site/docs/09-samples/20-sample-reactive.md b/spring-boot-admin-docs/src/site/docs/09-samples/20-sample-reactive.md new file mode 100644 index 00000000000..14c1476e751 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/09-samples/20-sample-reactive.md @@ -0,0 +1,554 @@ +--- + +sidebar_position: 20 +sidebar_custom_props: + icon: 'file-code' +--- + +# Reactive Sample + +The Reactive sample demonstrates a Spring Boot Admin Server deployment using Spring WebFlux, the reactive, non-blocking +web framework. This sample showcases how to run Admin Server in a fully reactive environment with minimal dependencies. + +## Overview + +**Location**: `spring-boot-admin-samples/spring-boot-admin-sample-reactive/` + +**Features**: + +- Reactive stack using Spring WebFlux +- Non-blocking I/O operations +- Spring Security for WebFlux +- Self-monitoring using Admin Client +- Profile-based security configuration +- Minimal dependencies +- DevTools support for development + +## Prerequisites + +- Java 17 or higher +- Maven 3.6+ + +## Running the Sample + +### Quick Start (Insecure Mode) + +```bash +cd spring-boot-admin-samples/spring-boot-admin-sample-reactive +mvn spring-boot:run +``` + +Access the application at: `http://localhost:8080` + +The application runs with the `insecure` profile by default, allowing access without authentication. + +### With Security Enabled + +```bash +mvn spring-boot:run -Dspring-boot.run.profiles=secure +``` + +**Login Credentials**: Configure in `application.yml` or use default Spring Security credentials + +### Change Port + +```bash +SERVER_PORT=9090 mvn spring-boot:run +``` + +## Key Differences from Servlet Sample + +### Dependencies + +The reactive sample uses minimal dependencies: + +```xml + + + + de.codecentric + spring-boot-admin-starter-server + + + + + de.codecentric + spring-boot-admin-starter-client + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.boot + spring-boot-devtools + true + + +``` + +**Notice**: No explicit WebFlux dependency needed - it's pulled in transitively by `spring-boot-admin-starter-server` +when no servlet container is present. + +### Reactive Architecture + +The reactive sample leverages: + +- **Non-blocking I/O**: All HTTP requests are handled reactively +- **Backpressure**: Built-in flow control for data streams +- **Event Loop**: Efficient thread utilization with Netty +- **Reactive Types**: `Mono` and `Flux` for asynchronous operations + +## Application Structure + +### Main Application Class + +```java title="SpringBootAdminReactiveApplication.java" +@SpringBootApplication +@EnableAdminServer +public class SpringBootAdminReactiveApplication { + + private final AdminServerProperties adminServer; + + public SpringBootAdminReactiveApplication(AdminServerProperties adminServer) { + this.adminServer = adminServer; + } + + static void main(String[] args) { + SpringApplication.run(SpringBootAdminReactiveApplication.class, args); + } + + @Bean + public Notifier notifier() { + return (e) -> Mono.empty(); // No-op notifier + } +} +``` + +**Key Points**: + +- `@EnableAdminServer` enables Admin Server functionality +- AdminServerProperties injected for security configuration +- Simple no-op notifier returns `Mono.empty()` + +## Security Configuration + +### Insecure Profile (Default) + +```java +@Bean +@Profile("insecure") +public SecurityWebFilterChain securityWebFilterChainPermitAll( + ServerHttpSecurity http) { + return http + .authorizeExchange((authorizeExchange) -> + authorizeExchange.anyExchange().permitAll()) + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .build(); +} +``` + +**Characteristics**: + +- All endpoints accessible without authentication +- CSRF protection disabled +- Useful for development and testing + +:::warning Development Only +The insecure profile should only be used for local development and testing. Always enable security when deploying. +::: + +### Secure Profile + +```java +@Bean +@Profile("secure") +public SecurityWebFilterChain securityWebFilterChainSecure( + ServerHttpSecurity http) { + return http + .authorizeExchange((authorizeExchange) -> + authorizeExchange + .pathMatchers(adminServer.path("/assets/**")) + .permitAll() // Static resources + .pathMatchers("/actuator/health/**") + .permitAll() // Health endpoint + .pathMatchers(adminServer.path("/login")) + .permitAll() // Login page + .anyExchange() + .authenticated()) // Everything else requires auth + .formLogin((formLogin) -> formLogin + .loginPage(adminServer.path("/login")) + .authenticationSuccessHandler( + loginSuccessHandler(adminServer.path("/")))) + .logout((logout) -> logout + .logoutUrl(adminServer.path("/logout")) + .logoutSuccessHandler( + logoutSuccessHandler(adminServer.path("/login?logout")))) + .httpBasic(Customizer.withDefaults()) + .csrf(ServerHttpSecurity.CsrfSpec::disable) // Simplified for demo + .build(); +} + +private ServerAuthenticationSuccessHandler loginSuccessHandler(String uri) { + RedirectServerAuthenticationSuccessHandler successHandler = + new RedirectServerAuthenticationSuccessHandler(); + successHandler.setLocation(URI.create(uri)); + return successHandler; +} + +private ServerLogoutSuccessHandler logoutSuccessHandler(String uri) { + RedirectServerLogoutSuccessHandler successHandler = + new RedirectServerLogoutSuccessHandler(); + successHandler.setLogoutSuccessUrl(URI.create(uri)); + return successHandler; +} +``` + +**Security Features**: + +1. **Form Login**: Custom login page at Admin Server path +2. **HTTP Basic**: Support for basic authentication +3. **Public Endpoints**: Static resources, health, and login page +4. **Custom Redirects**: Success handlers for login/logout +5. **Path-based Authorization**: Uses `ServerHttpSecurity` for reactive security + +:::info Reactive Security +Notice the use of `SecurityWebFilterChain` and `ServerHttpSecurity` instead of servlet-based `SecurityFilterChain` and +`HttpSecurity`. +::: + +## Configuration + +### Application Configuration + +```yaml title="application.yml" +spring: + application: + name: spring-boot-admin-sample-reactive + + boot: + admin: + client: + url: http://localhost:8080 # Self-registration + + profiles: + active: + - insecure # Default profile + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS + +logging: + file: + name: "target/boot-admin-sample-reactive.log" +``` + +**Configuration Highlights**: + +- Self-monitoring via Admin Client +- All actuator endpoints exposed +- Health details always shown +- Insecure profile active by default + +## Reactive Stack Benefits + +### 1. Non-Blocking I/O + +All operations are non-blocking: + +```java +// Instance queries are reactive +Flux instances = instanceRepository.findAll(); + +// Event streams are reactive +Flux events = eventStore.findAll(); + +// HTTP calls are reactive +Mono response = webClient + .get() + .uri("/actuator/health") + .exchange(); +``` + +### 2. Efficient Resource Usage + +- **Thread Pool**: Small fixed thread pool (typically 2x CPU cores) +- **Memory**: Lower memory footprint +- **Scalability**: Handles more concurrent connections with fewer threads + +### 3. Backpressure Support + +The reactive stack automatically handles backpressure: + +```java +// Slow consumers won't overwhelm fast producers +eventStore.findAll() + .limitRate(100) // Process 100 events at a time + .subscribe(event -> processEvent(event)); +``` + +### 4. Better for Microservices + +- **Resilience**: Non-blocking calls prevent thread exhaustion +- **Latency**: Better tail latency under load +- **Throughput**: Higher throughput for I/O-bound operations + +## Testing the Sample + +### Access the UI + +1. Start the application +2. Navigate to `http://localhost:8080` +3. No login required (insecure mode) + +### Test Self-Monitoring + +The application monitors itself: + +- Application name: `spring-boot-admin-sample-reactive` +- Status: UP +- All actuator endpoints available +- Check logs: `target/boot-admin-sample-reactive.log` + +### Test Reactive Behavior + +Monitor thread usage: + +```bash +# Check thread count (should be low) +jcmd Thread.print | grep "nioEventLoopGroup" | wc -l +``` + +Expected: ~4-8 threads vs. hundreds in servlet mode under load + +### Performance Testing + +Compare reactive vs. servlet performance: + +```bash +# Reactive sample +ab -n 10000 -c 100 http://localhost:8080/actuator/health + +# Servlet sample +ab -n 10000 -c 100 http://localhost:8081/actuator/health +``` + +Reactive should handle higher concurrency with fewer resources. + +## Build and Deploy + +### Build JAR + +```bash +mvn clean package +``` + +Produces: `target/spring-boot-admin-sample-reactive.jar` + +### Run JAR + +```bash +java -jar target/spring-boot-admin-sample-reactive.jar +``` + +### With Security Profile + +```bash +java -jar target/spring-boot-admin-sample-reactive.jar \ + --spring.profiles.active=secure +``` + +### Production Configuration + +Example production configuration: + +```yaml +spring: + profiles: + active: + - secure # Enable security + + security: + user: + name: admin + password: ${ADMIN_PASSWORD} + +server: + port: 8443 + ssl: + enabled: true + key-store: classpath:keystore.p12 + key-store-password: ${KEYSTORE_PASSWORD} + +management: + server: + port: 8081 # Separate management port +``` + +## Comparison: Reactive vs. Servlet + +| Aspect | Reactive Sample | Servlet Sample | +|------------------|-----------------------------|-----------------------------------| +| **Web Stack** | WebFlux (Netty) | Spring MVC (Tomcat) | +| **Thread Model** | Event loop (4-8 threads) | Thread per request (200+ threads) | +| **I/O Model** | Non-blocking | Blocking | +| **Memory** | Lower footprint | Higher footprint | +| **Scalability** | High (10K+ connections) | Medium (100s of connections) | +| **Complexity** | Higher learning curve | Traditional, simpler | +| **Dependencies** | Minimal | More dependencies | +| **Use Case** | High concurrency, I/O-bound | CPU-bound, traditional apps | + +## When to Use Reactive Sample + +✅ **Use Reactive When**: + +- Monitoring many instances (100+) +- High concurrency requirements +- Microservices architecture +- Cloud-native deployments +- Limited resources (memory/CPU) +- I/O-bound workloads + +❌ **Use Servlet When**: + +- Traditional monolithic applications +- Team unfamiliar with reactive programming +- Heavy CPU-bound processing +- Existing servlet-based infrastructure +- Simpler debugging requirements + +## Common Issues + +### ClassNotFoundException + +If you see WebFlux-related errors: + +``` +java.lang.ClassNotFoundException: reactor.netty.http.server.HttpServer +``` + +**Solution**: Ensure no servlet dependencies are present: + +```xml + + + org.springframework.boot + spring-boot-starter-webmvc + +``` + +### Port Conflict + +If port 8080 is in use: + +```bash +SERVER_PORT=9090 mvn spring-boot:run +``` + +### Security Configuration Not Applied + +If security profile doesn't work: + +```bash +# Verify active profiles +curl http://localhost:8080/actuator/env | jq '.activeProfiles' +``` + +Ensure profile is set correctly in `application.yml` or via command line. + +## Customization Ideas + +### Add Custom Reactive Notifier + +```java +@Bean +public Notifier customReactiveNotifier() { + return (event) -> { + return webClient + .post() + .uri("https://webhook.site/...") + .bodyValue(event) + .retrieve() + .bodyToMono(Void.class) + .onErrorResume(e -> { + log.error("Notification failed", e); + return Mono.empty(); + }); + }; +} +``` + +### Add WebClient Customization + +```java +@Bean +public InstanceWebClientCustomizer customTimeout() { + return (builder) -> builder + .clientConnector(new ReactorClientHttpConnector( + HttpClient.create() + .responseTimeout(Duration.ofSeconds(10)) + )); +} +``` + +### Add Reactive Health Indicator + +```java +@Component +public class CustomHealthIndicator implements ReactiveHealthIndicator { + + @Override + public Mono health() { + return Mono.just(Health.up() + .withDetail("custom", "Reactive health check") + .build()); + } +} +``` + +## Key Takeaways + +This sample demonstrates: + +✅ **Reactive Architecture** + +- Non-blocking I/O with WebFlux +- Efficient resource utilization + +✅ **Security Options** + +- Profile-based security configuration +- Reactive security filters + +✅ **Minimal Dependencies** + +- Lightweight deployment +- Faster startup time + +✅ **Fully Configured** + +- Self-monitoring capability +- Scalable architecture + +## Next Steps + +- Explore [Servlet Sample](./10-sample-servlet.md) for traditional deployment +- Review [Eureka Sample](./30-sample-eureka.md) for service discovery +- Check [Hazelcast Sample](./60-sample-hazelcast.md) for clustering +- Read [Customization Guide](../06-customization/) for extensions + +## See Also + +- [Server Configuration](../02-server/01-server.mdx) +- [Client Registration](../03-client/20-registration.md) +- [Spring WebFlux Documentation](https://docs.spring.io/spring-framework/reference/web/webflux.html) diff --git a/spring-boot-admin-docs/src/site/docs/09-samples/30-sample-eureka.md b/spring-boot-admin-docs/src/site/docs/09-samples/30-sample-eureka.md new file mode 100644 index 00000000000..dbd99b13b76 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/09-samples/30-sample-eureka.md @@ -0,0 +1,716 @@ +--- + +sidebar_position: 30 +sidebar_custom_props: + icon: 'file-code' +--- + +# Eureka Sample + +The Eureka sample demonstrates Spring Boot Admin Server integration with Netflix Eureka service discovery. This sample +shows how to automatically discover and monitor Spring Boot applications registered with Eureka without using the Admin +Client. + +## Overview + +**Location**: `spring-boot-admin-samples/spring-boot-admin-sample-eureka/` + +**Features**: + +- Automatic service discovery via Eureka +- No Admin Client required on monitored applications +- Dynamic instance registration and deregistration +- Metadata-based configuration +- Docker Compose setup with multiple services +- Spring Security integration +- Health check integration with Eureka + +## Prerequisites + +- Java 17 or higher +- Maven 3.6+ +- Docker and Docker Compose (for full stack) +- Eureka Server running (or use Docker Compose) + +## Architecture + +```mermaid +graph TD + ES[Eureka Server
Port 8761] -->|Service Registry| AS[Admin Server
Port 8080] + ES -->|Service Registry| MA[Monitored Apps] +``` + +**Key Points**: + +1. Applications register with Eureka +2. Admin Server discovers applications from Eureka +3. No direct registration needed + +## Running the Sample + +### Option 1: With External Eureka Server + +#### Start Eureka Server + +```bash +# Using Docker +docker run -d -p 8761:8761 springcloud/eureka + +# Or using Spring Cloud Eureka server JAR +java -jar eureka-server.jar +``` + +Verify Eureka is running: `http://localhost:8761` + +#### Start Admin Server + +```bash +cd spring-boot-admin-samples/spring-boot-admin-sample-eureka +mvn spring-boot:run +``` + +Access Admin UI at: `http://localhost:8080` + +### Option 2: Using Docker Compose (Recommended) + +```bash +cd spring-boot-admin-samples/spring-boot-admin-sample-eureka +docker-compose up +``` + +This starts: + +- Eureka Server (port 8761) +- Admin Server (port 8080) +- Spring Cloud Config Server (port 8888) +- Sample microservices (customers, stores) +- Supporting infrastructure (MongoDB, RabbitMQ) + +**Access Points**: + +- Admin UI: `http://localhost:8080` +- Eureka UI: `http://localhost:8761` +- Config Server: `http://localhost:8888` +- Sample App UI: `http://localhost:80` + +### Change Eureka URL + +```bash +mvn spring-boot:run -Dspring-boot.run.arguments=\ + --eureka.client.serviceUrl.defaultZone=http://other-eureka:8761/eureka/ +``` + +Or set environment variable: + +```bash +export EUREKA_SERVICE_URL=http://other-eureka:8761 +mvn spring-boot:run +``` + +## Project Structure + +### Dependencies + +```xml + + + + de.codecentric + spring-boot-admin-starter-server + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-tomcat + + + + +``` + +**Note**: Tomcat is excluded, so this sample runs on Netty (reactive stack). + +### Main Application Class + +```java title="SpringBootAdminEurekaApplication.java" +@Configuration +@EnableAutoConfiguration +@EnableDiscoveryClient // Enable Eureka discovery +@EnableAdminServer // Enable Admin Server +public class SpringBootAdminEurekaApplication { + + private final AdminServerProperties adminServer; + + public SpringBootAdminEurekaApplication(AdminServerProperties adminServer) { + this.adminServer = adminServer; + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootAdminEurekaApplication.class, args); + } +} +``` + +**Key Annotations**: + +- `@EnableDiscoveryClient`: Enables Eureka client functionality +- `@EnableAdminServer`: Enables Admin Server +- Both work together to discover and monitor services + +## Configuration + +### Admin Server Configuration + +```yaml title="application.yml" +spring: + application: + name: spring-boot-admin-sample-eureka + profiles: + active: + - secure + +eureka: + instance: + leaseRenewalIntervalInSeconds: 10 # Heartbeat interval + health-check-url-path: /actuator/health + metadata-map: + startup: ${random.int} # Trigger refresh on restart + client: + registryFetchIntervalSeconds: 5 # Fetch registry every 5s + serviceUrl: + defaultZone: ${EUREKA_SERVICE_URL:http://localhost:8761}/eureka/ + +management: + endpoints: + web: + exposure: + include: "*" # Expose all actuator endpoints + endpoint: + health: + show-details: ALWAYS +``` + +**Configuration Details**: + +1. **Lease Renewal**: 10 seconds (faster detection of down instances) +2. **Registry Fetch**: 5 seconds (quick discovery of new services) +3. **Health Check**: Registered with Eureka +4. **Startup Metadata**: Random value triggers endpoint update after restart + +### Client Application Configuration + +For applications to be monitored, they only need: + +```yaml +spring: + application: + name: my-service # Service name in Eureka + +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + instance: + metadata-map: + management.context-path: /actuator # Tell Admin where actuator is + +management: + endpoints: + web: + exposure: + include: "*" # Expose endpoints +``` + +**No Admin Client dependency needed!** + +## Security Configuration + +### Insecure Profile + +```java +@Bean +@Profile("insecure") +public SecurityWebFilterChain securityWebFilterChainPermitAll( + ServerHttpSecurity http) { + return http + .authorizeExchange((authorizeExchange) -> + authorizeExchange.anyExchange().permitAll()) + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .build(); +} +``` + +### Secure Profile (Default) + +```java +@Bean +@Profile("secure") +public SecurityWebFilterChain securityWebFilterChainSecure( + ServerHttpSecurity http) { + return http + .authorizeExchange((authorizeExchange) -> + authorizeExchange + .pathMatchers(adminServer.path("/assets/**")) + .permitAll() + .pathMatchers("/actuator/health/**") + .permitAll() + .pathMatchers(adminServer.path("/login")) + .permitAll() + .anyExchange() + .authenticated()) + .formLogin((formLogin) -> formLogin + .loginPage(adminServer.path("/login")) + .authenticationSuccessHandler(loginSuccessHandler(...))) + .logout((logout) -> logout + .logoutUrl(adminServer.path("/logout")) + .logoutSuccessHandler(logoutSuccessHandler(...))) + .httpBasic(Customizer.withDefaults()) + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .build(); +} +``` + +## Docker Compose Setup + +### Complete Stack + +The `docker-compose.yml` provides a complete microservices environment: + +```yaml title="docker-compose.yml" +version: '2' + +services: + # Eureka Server + eureka: + image: springcloud/eureka + container_name: eureka + ports: + - "8761:8761" + environment: + - EUREKA_INSTANCE_PREFERIPADDRESS=true + + # Admin Server + admin: + build: + context: . + dockerfile: ./src/main/docker/Dockerfile + depends_on: + - eureka + ports: + - "8080:8080" + environment: + - EUREKA_SERVICE_URL=http://eureka:8761 + - EUREKA_INSTANCE_PREFER_IP_ADDRESS=true + + # Spring Cloud Config Server + config: + image: springcloud/configserver + depends_on: + - eureka + ports: + - "8888:8888" + environment: + - EUREKA_SERVICE_URL=http://eureka:8761 + + # Sample Microservices + customers: + image: springcloud/customers + depends_on: + - config + - rabbit + environment: + - CONFIG_SERVER_URI=http://config:8888 + - RABBITMQ_HOST=rabbit + + stores: + image: springcloud/stores + depends_on: + - config + - rabbit + - mongodb + environment: + - CONFIG_SERVER_URI=http://config:8888 + - RABBITMQ_HOST=rabbit + - MONGODB_HOST=mongodb + + # Infrastructure + mongodb: + image: tutum/mongodb + ports: + - "27017:27017" + environment: + - AUTH=no + + rabbit: + image: "rabbitmq:4" + ports: + - "5672:5672" +``` + +### Start Stack + +```bash +docker-compose up +``` + +### Stop Stack + +```bash +docker-compose down +``` + +### View Logs + +```bash +# All services +docker-compose logs -f + +# Specific service +docker-compose logs -f admin +``` + +## How It Works + +### Service Discovery Flow + +1. **Application Startup**: + - Application starts and registers with Eureka + - Sends metadata including actuator path + - Eureka assigns instance ID + +2. **Admin Server Discovery**: + - Admin Server fetches registry from Eureka every 5s + - Discovers new services + - Reads metadata to find actuator endpoints + +3. **Health Monitoring**: + - Admin Server polls actuator endpoints + - Updates instance status + - Triggers notifications on status changes + +4. **Application Shutdown**: + - Application deregisters from Eureka + - Admin Server removes instance from monitoring + +### Metadata Mapping + +Admin Server reads specific metadata keys: + +```yaml +eureka: + instance: + metadata-map: + # Required for proper endpoint detection + management.context-path: /actuator + management.port: 8081 # If different from service port + + # Optional - for authenticated actuators + user.name: admin + user.password: ${admin.password} + + # Optional - custom metadata + startup: ${random.int} # Triggers refresh + environment: production + version: ${project.version} +``` + +**Important Keys**: + +- `management.context-path`: Where actuator endpoints are located +- `management.port`: If management port differs from application port +- `user.name` / `user.password`: Credentials for secured actuators +- `startup`: Random value forces Admin to refresh endpoints after restart + +## Testing the Sample + +### Verify Eureka Registration + +1. Access Eureka UI: `http://localhost:8761` +2. Check "Instances currently registered with Eureka" +3. Should see: + - `SPRING-BOOT-ADMIN-SAMPLE-EUREKA` + - Other registered services + +### Verify Admin Server Discovery + +1. Access Admin UI: `http://localhost:8080` +2. Should see all Eureka-registered services +3. Click on each service to view: + - Health status + - Metrics + - Environment + - Logs + - JVM details + +### Test Dynamic Discovery + +#### Register New Service + +```bash +# Start another instance +SERVER_PORT=8081 mvn spring-boot:run +``` + +Within 5 seconds, it should appear in Admin UI. + +#### Deregister Service + +Stop the application (Ctrl+C). Within ~40 seconds (lease timeout), it should disappear from Admin UI. + +### Test Health Status Changes + +Stop a monitored service and watch status change from UP → DOWN in Admin UI. + +## Advanced Configuration + +### Custom Service Filtering + +Filter which services to monitor: + +```yaml +spring: + boot: + admin: + discovery: + ignored-services: + - eureka-server # Don't monitor Eureka itself + - config-server # Don't monitor Config Server +``` + +### Service Grouping + +Group services using metadata: + +```yaml +# Client application +eureka: + instance: + metadata-map: + group: backend-services + team: platform +``` + +### Secure Actuator Endpoints + +If client actuators are secured: + +```yaml +# Client application +eureka: + instance: + metadata-map: + user.name: actuator-admin + user.password: ${actuator.password} +``` + +Admin Server automatically uses these credentials. + +### Custom Health Check URL + +```yaml +eureka: + instance: + health-check-url-path: /custom/health + metadata-map: + management.context-path: /custom +``` + +## Comparison: Eureka vs. Direct Registration + +| Aspect | Eureka Discovery | Direct Registration | +|-----------------------|---------------------------|------------------------------| +| **Setup** | Eureka Server required | No additional infrastructure | +| **Client Dependency** | Only Eureka client | Admin Client required | +| **Discovery** | Automatic | Manual configuration | +| **Scalability** | Excellent (100+ services) | Limited (static config) | +| **Dynamic Updates** | Automatic | Manual restart | +| **Use Case** | Microservices | Monoliths, small deployments | +| **Complexity** | Higher | Lower | + +## Troubleshooting + +### Admin Server Not Discovering Services + +**Check Eureka connectivity**: + +```bash +# Test Eureka API +curl http://localhost:8761/eureka/apps + +# Check Admin logs +docker-compose logs admin | grep -i eureka +``` + +**Common Issues**: + +1. Eureka URL incorrect +2. Network connectivity issues +3. Services not exposing actuator endpoints + +**Solution**: + +```yaml +# Verify configuration +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ # Trailing slash! +``` + +### Services Show as DOWN + +**Check health endpoint**: + +```bash +curl http://localhost:8080/actuator/health +``` + +**Verify metadata**: + +```yaml +eureka: + instance: + metadata-map: + management.context-path: /actuator # Must match actual path +``` + +### Slow Discovery + +Services take too long to appear: + +```yaml +eureka: + client: + registryFetchIntervalSeconds: 5 # Reduce from default 30s + instance: + leaseRenewalIntervalInSeconds: 10 # Reduce from default 30s +``` + +### Docker Compose Issues + +**Port conflicts**: + +```bash +# Change ports in docker-compose.yml +ports: + - "9090:8080" # Map to different host port +``` + +**Container connectivity**: + +```bash +# Check network +docker network inspect spring-boot-admin-sample-eureka_discovery + +# Check container logs +docker-compose logs eureka +``` + +## Production Considerations + +### High Availability + +Run multiple Eureka servers: + +```yaml +eureka: + client: + serviceUrl: + defaultZone: http://eureka1:8761/eureka/,http://eureka2:8761/eureka/ +``` + +### Security + +Secure Eureka communication: + +```yaml +eureka: + client: + serviceUrl: + defaultZone: https://${eureka.user}:${eureka.password}@eureka:8761/eureka/ +``` + +### Performance Tuning + +Optimize for large deployments: + +```yaml +spring: + boot: + admin: + monitor: + period: 20000 # Increase polling interval (ms) + connect-timeout: 5000 + read-timeout: 10000 + +eureka: + client: + registryFetchIntervalSeconds: 10 # Balance freshness vs. load +``` + +### Monitoring Eureka Itself + +Register Admin Server with itself: + +```yaml +spring: + boot: + admin: + discovery: + ignored-services: [] # Don't ignore any services +``` + +## Key Takeaways + +This sample demonstrates: + +✅ **Service Discovery Integration** + +- Automatic discovery via Eureka +- No Admin Client dependency needed + +✅ **Dynamic Monitoring** + +- Services auto-register/deregister +- Real-time discovery updates + +✅ **Complete Setup** + +- Complete microservices stack +- Docker Compose orchestration + +✅ **Scalable Architecture** + +- Handles many services efficiently +- Centralized monitoring + +## Next Steps + +- Explore [Consul Sample](./40-sample-consul.md) for alternative service discovery +- Review [Zookeeper Sample](./50-sample-zookeeper.md) for Apache Zookeeper +- Check [Integration Guide](../04-integration/10-eureka.md) for detailed Eureka setup +- See [Hazelcast Sample](./60-sample-hazelcast.md) for clustering + +## See Also + +- [Eureka Integration Guide](../04-integration/10-eureka.md) +- [Service Discovery](../03-client/40-service-discovery.md) +- [Server Configuration](../02-server/01-server.mdx) +- [Netflix Eureka Documentation](https://github.com/Netflix/eureka/wiki) diff --git a/spring-boot-admin-docs/src/site/docs/09-samples/40-sample-consul.md b/spring-boot-admin-docs/src/site/docs/09-samples/40-sample-consul.md new file mode 100644 index 00000000000..452fcf1c017 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/09-samples/40-sample-consul.md @@ -0,0 +1,708 @@ +--- + +sidebar_position: 40 +sidebar_custom_props: + icon: 'file-code' +--- + +# Consul Sample + +The Consul sample demonstrates Spring Boot Admin Server integration with HashiCorp Consul for service discovery. This +sample shows how to leverage Consul's powerful service registry and health checking capabilities to automatically +discover and monitor Spring Boot applications. + +## Overview + +**Location**: `spring-boot-admin-samples/spring-boot-admin-sample-consul/` + +**Features**: + +- Automatic service discovery via Consul +- No Admin Client required on monitored applications +- Consul health check integration +- Metadata-based configuration +- Custom actuator endpoint paths +- Spring Security integration +- Servlet-based deployment + +## Prerequisites + +- Java 17 or higher +- Maven 3.6+ +- Consul installed and running + +## Installing Consul + +### macOS + +```bash +brew install consul +``` + +### Linux + +```bash +wget https://releases.hashicorp.com/consul/1.17.0/consul_1.17.0_linux_amd64.zip +unzip consul_1.17.0_linux_amd64.zip +sudo mv consul /usr/local/bin/ +``` + +### Windows + +Download from: https://www.consul.io/downloads + +### Docker + +```bash +docker run -d -p 8500:8500 -p 8600:8600/udp --name=consul consul agent -server -ui -bootstrap-expect=1 -client=0.0.0.0 +``` + +### Verify Installation + +```bash +consul version +``` + +## Running the Sample + +### Start Consul + +```bash +# Development mode (single node) +consul agent -dev +``` + +Verify Consul is running: `http://localhost:8500/ui` + +### Start Admin Server + +```bash +cd spring-boot-admin-samples/spring-boot-admin-sample-consul +mvn spring-boot:run +``` + +Access Admin UI at: `http://localhost:8080` + +### With Different Consul Host + +```bash +mvn spring-boot:run -Dspring-boot.run.arguments=\ + --spring.cloud.consul.host=consul-server +``` + +### Insecure Mode + +```bash +mvn spring-boot:run -Dspring-boot.run.profiles=insecure +``` + +## Project Structure + +### Dependencies + +```xml + + + + de.codecentric + spring-boot-admin-starter-server + + + + + org.springframework.cloud + spring-cloud-starter-consul-discovery + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + + + org.springframework.boot + spring-boot-starter-security + + +``` + +### Main Application Class + +```java title="SpringBootAdminConsulApplication.java" +@SpringBootApplication +@EnableDiscoveryClient // Enable Consul discovery +@EnableAdminServer // Enable Admin Server +public class SpringBootAdminConsulApplication { + + static void main(String[] args) { + SpringApplication.run(SpringBootAdminConsulApplication.class, args); + } +} +``` + +## Configuration + +### Admin Server Configuration + +```yaml title="application.yml" +spring: + application: + name: consul-example + + cloud: + config: + enabled: false # Disable config client + consul: + host: localhost + port: 8500 + discovery: + metadata: + # IMPORTANT: Use dashes, not dots in metadata keys! + management-context-path: /foo + health-path: /ping + user-name: user + user-password: password + + profiles: + active: + - secure + + boot: + admin: + discovery: + ignored-services: consul # Don't monitor Consul itself + +management: + endpoints: + web: + exposure: + include: "*" + base-path: /foo # Custom actuator base path + path-mapping: + health: /ping # Custom health endpoint path + endpoint: + health: + show-details: ALWAYS +``` + +:::warning Metadata Key Restriction +**CRITICAL**: Consul metadata keys **cannot contain dots**. Use dashes instead: + +- ✅ `management-context-path` +- ❌ `management.context-path` + +This is a Consul limitation, not a Spring Boot Admin limitation. +::: + +### Client Application Configuration + +For applications to be monitored: + +```yaml +spring: + application: + name: my-service + + cloud: + consul: + host: localhost + port: 8500 + discovery: + metadata: + management-context-path: /actuator # Use dashes! + health-path: /actuator/health + # For secured actuators + user-name: ${actuator.username} + user-password: ${actuator.password} + +management: + endpoints: + web: + exposure: + include: "*" +``` + +## Security Configuration + +### Insecure Profile + +```java +@Profile("insecure") +@Configuration +public static class SecurityPermitAllConfig { + + @Bean + protected SecurityFilterChain filterChain(HttpSecurity http) + throws Exception { + http.authorizeHttpRequests((authorizeRequests) -> + authorizeRequests.anyRequest().permitAll()) + .csrf((csrf) -> csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .ignoringRequestMatchers( + adminContextPath + "/instances", + adminContextPath + "/instances/*", + adminContextPath + "/actuator/**" + )); + return http.build(); + } +} +``` + +### Secure Profile (Default) + +```java +@Profile("secure") +@Configuration +public static class SecuritySecureConfig { + + @Bean + protected SecurityFilterChain filterChain(HttpSecurity http) + throws Exception { + SavedRequestAwareAuthenticationSuccessHandler successHandler = + new SavedRequestAwareAuthenticationSuccessHandler(); + successHandler.setTargetUrlParameter("redirectTo"); + successHandler.setDefaultTargetUrl(adminContextPath + "/"); + + http.authorizeHttpRequests((authorizeRequests) -> + authorizeRequests + .requestMatchers(adminContextPath + "/assets/**") + .permitAll() + .requestMatchers(adminContextPath + "/login") + .permitAll() + .anyRequest() + .authenticated()) + .formLogin((formLogin) -> formLogin + .loginPage(adminContextPath + "/login") + .successHandler(successHandler)) + .logout((logout) -> logout + .logoutUrl(adminContextPath + "/logout")) + .httpBasic(Customizer.withDefaults()) + .csrf((csrf) -> csrf + .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) + .ignoringRequestMatchers( + adminContextPath + "/instances", + adminContextPath + "/instances/*", + adminContextPath + "/actuator/**" + )); + + return http.build(); + } +} +``` + +## How It Works + +### Service Discovery Flow + +1. **Application Registration**: + - Application registers with Consul on startup + - Sends service metadata including actuator paths + - Consul assigns service ID and health checks + +2. **Health Checking**: + - Consul performs HTTP health checks + - Marks services as passing/failing + - Admin Server queries only healthy services + +3. **Admin Discovery**: + - Admin Server queries Consul for registered services + - Reads metadata to locate actuator endpoints + - Begins monitoring discovered services + +4. **Deregistration**: + - Application deregisters on shutdown + - Consul removes from registry + - Admin Server stops monitoring + +### Metadata Mapping + +Admin Server reads specific metadata keys from Consul: + +```yaml +spring: + cloud: + consul: + discovery: + metadata: + # Required for endpoint detection + management-context-path: /actuator # Dashes only! + management-port: 8081 # If different + + # Optional - for secured actuators + user-name: admin + user-password: ${ACTUATOR_PASSWORD} + + # Custom metadata + environment: production + version: 1.0.0 + team: platform +``` + +**Key Mappings**: + +- `management-context-path` → Where to find actuator endpoints +- `management-port` → Management port if different from service port +- `health-path` → Custom health endpoint path +- `user-name` / `user-password` → Actuator credentials + +## Custom Actuator Paths + +This sample demonstrates custom actuator paths: + +```yaml +management: + endpoints: + web: + base-path: /foo # Actuator at /foo instead of /actuator + path-mapping: + health: /ping # Health at /foo/ping instead of /foo/health +``` + +Admin Server discovers these via metadata: + +```yaml +spring: + cloud: + consul: + discovery: + metadata: + management-context-path: /foo + health-path: /ping +``` + +## Testing the Sample + +### Verify Consul Registration + +1. Access Consul UI: `http://localhost:8500/ui` +2. Navigate to "Services" +3. Should see: + - `consul-example` (Admin Server) + - Other registered services + +### Check Service Health + +In Consul UI, services should show: + +- Status: Passing (green) +- Health check URL displayed +- Metadata visible + +### Verify Admin Discovery + +1. Access Admin UI: `http://localhost:8080` +2. Should see services registered in Consul +3. Click service to view: + - Health status + - Metrics + - Environment + - Custom /ping endpoint + +### Test Dynamic Discovery + +Register a new service: + +```bash +# Register via Consul API +curl -X PUT -d '{ + "Name": "test-service", + "Address": "127.0.0.1", + "Port": 8081, + "Meta": { + "management-context-path": "/actuator" + }, + "Check": { + "HTTP": "http://127.0.0.1:8081/actuator/health", + "Interval": "10s" + } +}' http://localhost:8500/v1/agent/service/register +``` + +Service appears in Admin UI within seconds. + +## Consul Features + +### Health Checks + +Consul supports multiple health check types: + +#### HTTP Health Check + +```yaml +spring: + cloud: + consul: + discovery: + health-check-path: /actuator/health + health-check-interval: 10s + health-check-timeout: 5s +``` + +#### TTL Health Check + +```yaml +spring: + cloud: + consul: + discovery: + health-check-ttl: 30s +``` + +Application must send heartbeat to Consul every 30 seconds. + +### Service Tags + +Add tags for filtering: + +```yaml +spring: + cloud: + consul: + discovery: + tags: + - production + - backend + - v1.0.0 +``` + +### Service Filtering + +Filter services monitored by Admin: + +```yaml +spring: + boot: + admin: + discovery: + ignored-services: + - consul # Don't monitor Consul + - config-server # Don't monitor Config Server + services: # Only monitor these (if specified) + - my-service + - another-service +``` + +## Advanced Configuration + +### Consul ACL + +Secure Consul with ACL tokens: + +```yaml +spring: + cloud: + consul: + token: ${CONSUL_TOKEN} + discovery: + acl-token: ${CONSUL_ACL_TOKEN} +``` + +### Consul TLS + +Connect to Consul over TLS: + +```yaml +spring: + cloud: + consul: + scheme: https + tls: + enabled: true + key-store-path: classpath:consul-keystore.p12 + key-store-password: ${KEYSTORE_PASSWORD} +``` + +### Multiple Datacenters + +Register in specific datacenter: + +```yaml +spring: + cloud: + consul: + discovery: + datacenter: dc1 +``` + +### Prefer IP Address + +Use IP instead of hostname: + +```yaml +spring: + cloud: + consul: + discovery: + prefer-ip-address: true + ip-address: 192.168.1.100 +``` + +## Comparison: Consul vs. Eureka + +| Feature | Consul | Eureka | +|---------------------|-----------------------------------|-------------------------------| +| **Health Checks** | Built-in (HTTP, TCP, TTL, Script) | Via Spring Boot actuator only | +| **Key-Value Store** | Yes | No | +| **ACL** | Yes | Basic | +| **Multi-DC** | Native support | Requires setup | +| **DNS Interface** | Yes | No | +| **Metadata Keys** | No dots allowed | Dots allowed | +| **Complexity** | Higher | Lower | +| **Ecosystem** | HashiCorp ecosystem | Netflix stack | + +## Troubleshooting + +### Metadata Key Errors + +**Symptom**: Admin Server can't find actuator endpoints + +**Cause**: Used dots in metadata keys + +**Solution**: Use dashes instead: + +```yaml +# Wrong +metadata: + management.context-path: /actuator + +# Correct +metadata: + management-context-path: /actuator +``` + +### Services Not Discovered + +**Check Consul connectivity**: + +```bash +# Test Consul API +curl http://localhost:8500/v1/catalog/services + +# Check health +curl http://localhost:8500/v1/health/state/passing +``` + +**Verify Admin logs**: + +```bash +tail -f logs/spring-boot-admin.log | grep -i consul +``` + +### Health Check Failures + +Services show as "failing" in Consul: + +1. Verify health endpoint is accessible: + ```bash + curl http://localhost:8080/actuator/health + ``` + +2. Check health check interval: + ```yaml + spring: + cloud: + consul: + discovery: + health-check-interval: 30s # Increase if needed + ``` + +3. Review Consul logs: + ```bash + consul monitor + ``` + +### Connection Timeouts + +Increase timeout values: + +```yaml +spring: + cloud: + consul: + discovery: + health-check-timeout: 10s # Increase from default +``` + +## Production Considerations + +### Consul Cluster + +Run Consul in cluster mode (3 or 5 nodes): + +```bash +# Server node 1 +consul agent -server -bootstrap-expect=3 -data-dir=/consul/data \ + -bind=192.168.1.10 + +# Server node 2 +consul agent -server -data-dir=/consul/data \ + -bind=192.168.1.11 -join=192.168.1.10 + +# Server node 3 +consul agent -server -data-dir=/consul/data \ + -bind=192.168.1.12 -join=192.168.1.10 +``` + +### Enable ACL + +```yaml +spring: + cloud: + consul: + token: ${CONSUL_MANAGEMENT_TOKEN} + discovery: + acl-token: ${CONSUL_SERVICE_TOKEN} +``` + +### Monitor Consul Health + +Register Admin Server to monitor itself: + +```yaml +spring: + boot: + admin: + discovery: + ignored-services: [] # Don't ignore any services +``` + +## Key Takeaways + +This sample demonstrates: + +✅ **Consul Integration** + +- Service discovery via Consul +- Health check integration + +✅ **Metadata Handling** + +- Proper metadata key formatting (dashes not dots) +- Custom actuator paths + +✅ **Production Features** + +- ACL support +- TLS encryption +- Multi-datacenter awareness + +✅ **Flexibility** + +- Custom endpoint paths +- Secure and insecure modes + +## Next Steps + +- Explore [Eureka Sample](./30-sample-eureka.md) for Netflix Eureka +- Review [Zookeeper Sample](./50-sample-zookeeper.md) for Apache Zookeeper +- Check [Consul Integration Guide](../04-integration/20-consul.md) for detailed setup +- See [Hazelcast Sample](./60-sample-hazelcast.md) for clustering + +## See Also + +- [Consul Integration Guide](../04-integration/20-consul.md) +- [Service Discovery](../03-client/40-service-discovery.md) +- [Server Configuration](../02-server/01-server.mdx) +- [HashiCorp Consul Documentation](https://www.consul.io/docs) diff --git a/spring-boot-admin-docs/src/site/docs/09-samples/50-sample-zookeeper.md b/spring-boot-admin-docs/src/site/docs/09-samples/50-sample-zookeeper.md new file mode 100644 index 00000000000..3f9420c6406 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/09-samples/50-sample-zookeeper.md @@ -0,0 +1,92 @@ +--- + +sidebar_position: 50 +sidebar_custom_props: + icon: 'file-code' +--- + +# Zookeeper Sample + +Spring Boot Admin Server integration with Apache Zookeeper for service discovery. This sample shows how to use Zookeeper +as a service registry to automatically discover and monitor Spring Boot applications. + +## Overview + +**Location**: `spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/` + +**Features**: + +- Service discovery via Apache Zookeeper +- No Admin Client required on monitored apps +- Metadata-based configuration +- Custom actuator paths (/foo, /ping) +- Profile-based security + +## Prerequisites + +- Java 17+, Maven 3.6+ +- Apache Zookeeper installed + +## Running + +### Start Zookeeper + +```bash +# Docker +docker run -d -p 2181:2181 zookeeper:3.8 + +# Or download from https://zookeeper.apache.org/ +``` + +### Start Admin Server + +```bash +cd spring-boot-admin-samples/spring-boot-admin-sample-zookeeper +mvn spring-boot:run +``` + +Access at: `http://localhost:8080` + +## Configuration + +```yaml +spring: + application: + name: zookeeper-example + cloud: + zookeeper: + connect-string: localhost:2181 + discovery: + metadata: + management.context-path: /foo # Dots allowed (unlike Consul) + health.path: /ping + user.name: user + user.password: password + +management: + endpoints: + web: + base-path: /foo + path-mapping: + health: /ping +``` + +## Key Differences + +### vs. Consul + +- **Metadata keys**: Dots allowed in Zookeeper +- **Simplicity**: Fewer features, simpler setup +- **Use case**: Hadoop/Big Data ecosystems + +### vs. Eureka + +- **Maturity**: Zookeeper is older, more established +- **Ecosystem**: Hadoop/Kafka integration +- **Complexity**: More configuration required + +## See Also + +- [Zookeeper Integration](../04-integration/30-zookeeper.md) +- [Eureka Sample](./30-sample-eureka.md) +- [Consul Sample](./40-sample-consul.md) diff --git a/spring-boot-admin-docs/src/site/docs/09-samples/60-sample-hazelcast.md b/spring-boot-admin-docs/src/site/docs/09-samples/60-sample-hazelcast.md new file mode 100644 index 00000000000..1c6c823fe2b --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/09-samples/60-sample-hazelcast.md @@ -0,0 +1,251 @@ +--- + +sidebar_position: 60 +sidebar_custom_props: + icon: 'file-code' +--- + +# Hazelcast Sample + +Clustered Spring Boot Admin Server deployment using Hazelcast for distributed event storage. This sample demonstrates +high-availability setup where multiple Admin Server instances share state via Hazelcast. + +## Overview + +**Location**: `spring-boot-admin-samples/spring-boot-admin-sample-hazelcast/` + +**Features**: + +- Distributed event store with Hazelcast +- Multi-instance clustering +- Shared notification state +- High availability +- TCP/IP cluster discovery +- JMX monitoring enabled + +## Prerequisites + +- Java 17+, Maven 3.6+ + +## Running Multiple Instances + +### Terminal 1 (Port 8080) + +```bash +cd spring-boot-admin-samples/spring-boot-admin-sample-hazelcast +mvn spring-boot:run +``` + +### Terminal 2 (Port 8081) + +```bash +SERVER_PORT=8081 mvn spring-boot:run +``` + +### Terminal 3 (Port 8082) + +```bash +SERVER_PORT=8082 mvn spring-boot:run +``` + +All instances automatically form a cluster and share: + +- Instance events +- Notification state +- Application registry + +## Dependencies + +```xml + + de.codecentric + spring-boot-admin-starter-server + + + de.codecentric + spring-boot-admin-starter-client + + + com.hazelcast + hazelcast + +``` + +## Hazelcast Configuration + +```java +@Bean +public Config hazelcastConfig() { + // Event store map - holds all instance events + MapConfig eventStoreMap = new MapConfig(DEFAULT_NAME_EVENT_STORE_MAP) + .setInMemoryFormat(InMemoryFormat.OBJECT) + .setBackupCount(1) // 1 backup copy + .setMergePolicyConfig( + new MergePolicyConfig(PutIfAbsentMergePolicy.class.getName(), 100) + ); + + // Notification map - deduplicates notifications + MapConfig sentNotificationsMap = new MapConfig(DEFAULT_NAME_SENT_NOTIFICATIONS_MAP) + .setInMemoryFormat(InMemoryFormat.OBJECT) + .setBackupCount(1) + .setEvictionConfig( + new EvictionConfig() + .setEvictionPolicy(EvictionPolicy.LRU) + .setMaxSizePolicy(MaxSizePolicy.PER_NODE) + ); + + Config config = new Config(); + config.addMapConfig(eventStoreMap); + config.addMapConfig(sentNotificationsMap); + config.setProperty("hazelcast.jmx", "true"); + + // TCP/IP cluster discovery (local) + config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false); + TcpIpConfig tcpIpConfig = config.getNetworkConfig() + .getJoin() + .getTcpIpConfig(); + tcpIpConfig.setEnabled(true); + tcpIpConfig.setMembers(singletonList("127.0.0.1")); + + return config; +} +``` + +**Configuration Details**: + +- **Event Store**: Reliable storage with 1 backup +- **Sent Notifications**: LRU eviction to prevent memory growth +- **Cluster**: TCP/IP discovery on localhost +- **JMX**: Enabled for monitoring + +## How Clustering Works + +### Event Synchronization + +1. Instance A receives status change event +2. Event stored in Hazelcast distributed map +3. Instances B and C immediately see the event +4. All instances update their local state + +### Notification Deduplication + +1. Instance A sends notification +2. Records in Hazelcast sent-notifications map +3. Instance B sees event, checks sent-notifications +4. B skips sending (already sent by A) +5. No duplicate notifications to users + +### Load Balancing + +```mermaid +graph TD + LB[Load Balancer] --> A[Admin 8080] + LB --> B[Admin 8081] + LB --> C[Admin 8082] + A -.-> HC[Hazelcast Cluster] + B -.-> HC + C -.-> HC +``` + +Users can connect to any instance - they all show the same data. + +## Testing Clustering + +### Verify Cluster Formation + +Check logs for: + +``` +Members {size:3, ver:3} [ + Member [127.0.0.1]:5701 - e1f2g3h4 + Member [127.0.0.1]:5702 - a5b6c7d8 + Member [127.0.0.1]:5703 - i9j0k1l2 +] +``` + +### Test Event Sharing + +1. Register application on instance 8080 +2. Check instance 8081 - application appears +3. Stop instance 8080 +4. Application still visible on 8081 + +### Test High Availability + +1. Start 3 instances +2. Stop instance 8080 +3. Instances 8081 and 8082 continue operating +4. All data preserved (1 backup) + +## Production Configuration + +### Multicast Discovery + +```java +config.getNetworkConfig() + .getJoin() + .getMulticastConfig() + .setEnabled(true) + .setMulticastGroup("224.2.2.3") + .setMulticastPort(54327); +``` + +### TCP/IP with Multiple Hosts + +```java +tcpIpConfig.setMembers(Arrays.asList( + "admin-1.company.com", + "admin-2.company.com", + "admin-3.company.com" +)); +``` + +### Kubernetes Discovery + +```java +config.getNetworkConfig() + .getJoin() + .getKubernetesConfig() + .setEnabled(true) + .setProperty("namespace", "default") + .setProperty("service-name", "spring-boot-admin"); +``` + +## Monitoring Hazelcast + +### JMX + +Enable JMX and connect with JConsole: + +```bash +jconsole +``` + +Look for `com.hazelcast` MBeans. + +### Hazelcast Management Center + +```bash +docker run -p 8080:8080 hazelcast/management-center +``` + +Connect to: `http://localhost:8080` + +## Key Takeaways + +✅ **High Availability**: No single point of failure +✅ **Horizontal Scaling**: Add instances dynamically +✅ **Shared State**: All instances synchronized +✅ **Enterprise Features**: Backup, eviction, merge policies + +## Next Steps + +- [Hazelcast Integration Guide](../04-integration/40-hazelcast.md) +- [Servlet Sample](./10-sample-servlet.md) +- [Custom UI Sample](./70-sample-custom-ui.md) + +## See Also + +- [Hazelcast Documentation](https://docs.hazelcast.com/) +- [Clustering](../04-integration/40-hazelcast.md) +- [Server Configuration](../02-server/01-server.mdx) diff --git a/spring-boot-admin-docs/src/site/docs/09-samples/70-sample-custom-ui.md b/spring-boot-admin-docs/src/site/docs/09-samples/70-sample-custom-ui.md new file mode 100644 index 00000000000..406c5dafc94 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/09-samples/70-sample-custom-ui.md @@ -0,0 +1,430 @@ +--- + +sidebar_position: 70 +sidebar_custom_props: + icon: 'file-code' +--- + +# Custom UI Sample + +Demonstrates how to create custom UI extensions for Spring Boot Admin using Vue.js components. This sample shows how to +add custom views, menu items, and instance-specific endpoints to the Admin UI. + +## Overview + +**Location**: `spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/` + +**Features**: + +- Custom top-level navigation views +- Custom instance endpoint views +- Submenu integration +- Internationalization (i18n) +- Custom icons and handles +- Vue 3 components +- Access to SBA global components +- ApplicationStore integration + +## Prerequisites + +- Java 17+, Maven 3.6+ +- Node.js and npm (for building UI) + +## Project Structure + +This is a **library module** that gets included by other samples (like servlet sample): + +```xml + + + de.codecentric + spring-boot-admin-sample-custom-ui + +``` + +## Building + +```bash +cd spring-boot-admin-samples/spring-boot-admin-sample-custom-ui +mvn clean package +``` + +**Build Process**: + +1. Frontend Maven Plugin installs Node.js +2. Runs `npm ci` to install dependencies +3. Runs `npm run build` to compile Vue components +4. Copies dist files to `META-INF/spring-boot-admin-server-ui/extensions/custom/` +5. Admin Server auto-loads extensions from this location + +## Custom View Examples + +### 1. Top-Level View + +```javascript title="src/index.js" +SBA.use({ + install({ viewRegistry, i18n }) { + viewRegistry.addView({ + name: "custom", // Unique view name + path: "/custom", // URL path + component: custom, // Vue component + group: "custom", // Group for styling + handle, // Custom navigation handle + order: 1000, // Menu order + }); + + // Add translations + i18n.mergeLocaleMessage("en", { + custom: { + label: "My Extensions", + }, + }); + i18n.mergeLocaleMessage("de", { + custom: { + label: "Meine Erweiterung", + }, + }); + }, +}); +``` + +### 2. Submenu Item + +```javascript +SBA.viewRegistry.addView({ + name: "customSub", + parent: "custom", // Parent view name + path: "/customSub", + component: customSubitem, + label: "Custom Sub", + order: 1000, +}); +``` + +### 3. Instance Endpoint View + +```javascript +SBA.viewRegistry.addView({ + name: "instances/custom", + parent: "instances", // Under instance views + path: "custom", + component: customEndpoint, + label: "Custom", + group: "custom", + order: 1000, + isEnabled: ({ instance }) => { + return instance.hasEndpoint("custom"); // Conditional rendering + }, +}); +``` + +### 4. Custom Group Icon + +```javascript +SBA.viewRegistry.setGroupIcon( + "custom", + ` + + ` +); +``` + +## Vue Component Example + +```vue title="src/custom.vue" + + + +``` + +**Key Features**: + +- Access to `SBA.useApplicationStore()` for application data +- Global SBA components (`sba-panel`, `sba-status`, `sba-tag`) +- Reactive data from Vuex store +- Tailwind CSS classes for styling + +## Available SBA Components + +Global components you can use without importing: + +- `` - Card/panel container +- `` - Status indicator (UP/DOWN/etc.) +- `` - Tag display +- `` - Icon component +- `` - Button component +- `` - Input field +- `` - Toggle switch +- `` - Instance dropdown +- Many more in `spring-boot-admin-server-ui` module + +## Available Stores and APIs + +### ApplicationStore + +```javascript +const { applications } = SBA.useApplicationStore(); + +// applications is reactive +// Contains: { name, instances[], buildVersion, status, ... } +``` + +### Instance API + +```javascript +const instance = await SBA.getInstanceById(instanceId); +const health = await instance.fetchHealth(); +const metrics = await instance.fetchMetrics(); +const info = await instance.fetchInfo(); +``` + +### Event Bus + +```javascript +SBA.eventBus.on('event-name', (data) => { + // Handle event +}); + +SBA.eventBus.emit('custom-event', { foo: 'bar' }); +``` + +## File Structure + +``` +spring-boot-admin-sample-custom-ui/ +├── src/ +│ ├── index.js # Main entry point +│ ├── custom.vue # Top-level view component +│ ├── custom-subitem.vue # Submenu component +│ ├── custom-endpoint.vue # Instance endpoint component +│ ├── handle.vue # Navigation handle component +│ └── custom.css # Custom styles +├── package.json +├── vite.config.js +└── pom.xml +``` + +## Build Configuration + +### package.json + +```json +{ + "scripts": { + "build": "vite build" + }, + "dependencies": { + "vue": "^3.x" + } +} +``` + +### vite.config.js + +```javascript +export default { + build: { + lib: { + entry: 'src/index.js', + formats: ['es'], + fileName: 'index' + }, + rollupOptions: { + external: ['vue'], // Vue provided by SBA + output: { + globals: { + vue: 'Vue' + } + } + } + } +} +``` + +### pom.xml (Maven Integration) + +```xml + + com.github.eirslett + frontend-maven-plugin + + + install-node-and-npm + + install-node-and-npm + + + + npm-build + + npm + + + run build + + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-resources + process-resources + + copy-resources + + + + ${project.build.outputDirectory}/META-INF/spring-boot-admin-server-ui/extensions/custom + + + + ${project.build.directory}/dist + + + + + + +``` + +## Development Workflow + +### 1. Make Changes + +Edit Vue components in `src/`: + +```vue + +``` + +### 2. Build + +```bash +mvn clean package +``` + +### 3. Use in Application + +```xml + + de.codecentric + spring-boot-admin-sample-custom-ui + +``` + +### 4. Run & Test + +```bash +mvn spring-boot:run +``` + +Navigate to your custom view in the UI. + +## Common Use Cases + +### Custom Dashboard + +```javascript +viewRegistry.addView({ + name: "dashboard", + path: "/dashboard", + component: CustomDashboard, + label: "Dashboard", + order: 1, // First in menu +}); +``` + +### Instance Action Button + +```vue + + + +``` + +### Custom Metrics View + +```vue + + + +``` + +## Key Takeaways + +✅ **Full Customization**: Add any Vue component to UI +✅ **SBA Integration**: Access stores, components, APIs +✅ **Maven Integration**: Build with Maven +✅ **Reusable**: Package as library for multiple projects + +## Next Steps + +- [UI Customization Guide](../06-customization/ui/) +- [Servlet Sample](./10-sample-servlet.md) (uses this extension) + +## See Also + +- [Vue.js Documentation](https://vuejs.org/) diff --git a/spring-boot-admin-docs/src/site/docs/09-samples/_category_.json b/spring-boot-admin-docs/src/site/docs/09-samples/_category_.json new file mode 100644 index 00000000000..0abc048fd0e --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/09-samples/_category_.json @@ -0,0 +1,4 @@ +{ + "position": 9, + "label": "Samples" +} diff --git a/spring-boot-admin-docs/src/site/docs/09-samples/index.md b/spring-boot-admin-docs/src/site/docs/09-samples/index.md new file mode 100644 index 00000000000..d2469d07943 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/09-samples/index.md @@ -0,0 +1,278 @@ +--- + +sidebar_position: 70 +sidebar_custom_props: + icon: 'file-code' +--- + +# Sample Projects + +Spring Boot Admin includes several sample projects demonstrating different deployment scenarios and integration +patterns. These samples provide working examples you can use as starting points for your own implementations. + +## Available Samples + +### Basic Deployments + +- **[Servlet Sample](./10-sample-servlet.md)** - Traditional servlet-based deployment with security +- **[Reactive Sample](./20-sample-reactive.md)** - WebFlux reactive deployment + +### Service Discovery + +- **[Eureka Sample](./30-sample-eureka.md)** - Netflix Eureka integration +- **[Consul Sample](./40-sample-consul.md)** - HashiCorp Consul integration +- **[Zookeeper Sample](./50-sample-zookeeper.md)** - Apache Zookeeper integration + +### Advanced + +- **[Hazelcast Sample](./60-sample-hazelcast.md)** - Clustered deployment with Hazelcast +- **[Custom UI Sample](./70-sample-custom-ui.md)** - Custom UI extensions and branding + +## Repository Location + +All samples are available in +the [Spring Boot Admin GitHub repository](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples): + +``` +spring-boot-admin-samples/ +├── spring-boot-admin-sample-servlet/ +├── spring-boot-admin-sample-reactive/ +├── spring-boot-admin-sample-war/ +├── spring-boot-admin-sample-eureka/ +├── spring-boot-admin-sample-consul/ +├── spring-boot-admin-sample-zookeeper/ +├── spring-boot-admin-sample-hazelcast/ +└── spring-boot-admin-sample-custom-ui/ +``` + +## Running the Samples + +### Prerequisites + +- Java 17 or higher +- Maven 3.6 or higher +- Docker (optional, for some samples) + +### Build All Samples + +```bash +git clone https://github.com/codecentric/spring-boot-admin.git +cd spring-boot-admin +mvn clean install -DskipTests +``` + +### Run Individual Sample + +```bash +cd spring-boot-admin-samples/spring-boot-admin-sample-servlet +mvn spring-boot:run +``` + +Access the Admin UI at: `http://localhost:8080` + +### Default Credentials + +Most secured samples use: + +- **Username**: `user` +- **Password**: Check console output or `application.yml` + +## Sample Features Comparison + +| Feature | Servlet | Reactive | Eureka | Consul | Zookeeper | Hazelcast | Custom UI | WAR | +|-------------------|---------|----------|---------|---------|-----------|-----------|-----------|---------| +| Web Stack | Servlet | WebFlux | Servlet | Servlet | Servlet | Servlet | Servlet | Servlet | +| Security | ✅ | ✅ | ✅ | ✅ | - | - | - | - | +| Service Discovery | Static | Static | Eureka | Consul | Zookeeper | Static | Static | Static | +| Clustering | - | - | - | - | - | ✅ | - | - | +| Custom UI | - | - | - | - | - | - | ✅ | - | +| JMX Support | ✅ | - | - | - | - | - | - | ✅ | +| Notifications | ✅ | - | - | - | - | - | - | - | + +## Common Configuration + +All samples share common patterns: + +### Actuator Configuration + +```yaml +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS +``` + +### Logging Configuration + +```yaml +logging: + file: + name: "target/boot-admin-sample.log" + pattern: + file: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID}){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx" +``` + +### Build Info + +All samples generate build information: + +```xml + + org.springframework.boot + spring-boot-maven-plugin + + + + build-info + + + + +``` + +## Quick Start Guide + +### 1. Servlet Sample (Recommended for Beginners) + +```bash +cd spring-boot-admin-samples/spring-boot-admin-sample-servlet +mvn spring-boot:run +``` + +Features: + +- Security enabled +- Self-monitoring +- Mail notifications +- Custom UI extensions + +### 2. Eureka Sample (Recommended for Microservices) + +```bash +# Start Eureka Server +docker run -d -p 8761:8761 springcloud/eureka + +# Start Admin Server +cd spring-boot-admin-samples/spring-boot-admin-sample-eureka +mvn spring-boot:run +``` + +Features: + +- Automatic service discovery +- Dynamic registration +- No client library needed + +### 3. Hazelcast Sample (Recommended for Production) + +```bash +# Start multiple instances +cd spring-boot-admin-samples/spring-boot-admin-sample-hazelcast + +# Terminal 1 +SERVER_PORT=8080 mvn spring-boot:run + +# Terminal 2 +SERVER_PORT=8081 mvn spring-boot:run +``` + +Features: + +- High availability +- Shared event store +- Load balancing ready + +## Docker Support + +Some samples include Docker Compose configurations: + +```bash +cd spring-boot-admin-samples/spring-boot-admin-sample-eureka +docker-compose up +``` + +## Customizing Samples + +Use the samples as templates: + +1. **Copy sample directory**: + ```bash + cp -r spring-boot-admin-sample-servlet my-admin-server + ``` + +2. **Update `pom.xml`**: + ```xml + my-admin-server + My Admin Server + ``` + +3. **Customize configuration**: + - Update `application.yml` + - Add security configuration + - Configure notifications + +4. **Build and run**: + ```bash + mvn clean package + java -jar target/my-admin-server.jar + ``` + +## Testing Samples + +Each sample includes tests: + +```bash +cd spring-boot-admin-samples/spring-boot-admin-sample-servlet +mvn test +``` + +## Troubleshooting Samples + +### Port Already in Use + +Change the port: + +```bash +SERVER_PORT=9090 mvn spring-boot:run +``` + +Or in `application.yml`: + +```yaml +server: + port: 9090 +``` + +### Build Failures + +Clean and rebuild: + +```bash +mvn clean install -DskipTests +``` + +### Dependencies Issues + +Update Spring Boot Admin version in parent POM and rebuild. + +## Contributing + +To add a new sample: + +1. Create directory under `spring-boot-admin-samples/` +2. Follow existing sample structure +3. Add `README.md` with specific instructions +4. Include `docker-compose.yml` if applicable +5. Add tests +6. Update samples documentation + +## See Also + +- [Getting Started](../02-getting-started/) - Basic setup guide +- [Server Configuration](../02-server/01-server.mdx) - Server configuration options +- [Integration](../04-integration/) - Service discovery integration +- [Customization](../06-customization/) - UI and server customization diff --git a/spring-boot-admin-docs/src/site/docs/10-reference/10-event-types.md b/spring-boot-admin-docs/src/site/docs/10-reference/10-event-types.md new file mode 100644 index 00000000000..53877804fe7 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/10-reference/10-event-types.md @@ -0,0 +1,607 @@ +--- + +sidebar_position: 10 +sidebar_custom_props: + icon: 'book' +--- + +# Event Types Reference + +Complete reference of all `InstanceEvent` types emitted by Spring Boot Admin Server during instance lifecycle. + +## Event Base Class + +All events extend `InstanceEvent`: + +```java +public abstract class InstanceEvent implements Serializable { + private final InstanceId instance; // Unique instance identifier + private final long version; // Event version (incremental) + private final Instant timestamp; // When event occurred + private final String type; // Event type constant +} +``` + +**Common Properties**: + +- `instance`: Unique ID of the instance (e.g., `"abc123def456"`) +- `version`: Monotonically increasing version number +- `timestamp`: ISO-8601 timestamp when event was created +- `type`: String constant identifying the event type + +## Event Lifecycle + +Typical event sequence for an instance: + +``` +1. REGISTERED → Instance first registers +2. ENDPOINTS_DETECTED → Actuator endpoints discovered +3. STATUS_CHANGED → Health status updated to UP +4. INFO_CHANGED → Info endpoint data loaded +5. STATUS_CHANGED → Status changes during lifecycle +6. REGISTRATION_UPDATED → Registration info changes (optional) +7. DEREGISTERED → Instance unregisters +``` + +## Event Types + +### 1. REGISTERED + +**Class**: `InstanceRegisteredEvent` + +**Type Constant**: `"REGISTERED"` + +**When Emitted**: Instance first registers with Admin Server + +**Payload**: + +```java +public class InstanceRegisteredEvent extends InstanceEvent { + Registration registration; // Complete registration info +} +``` + +**Registration Contents**: + +- `name`: Application name +- `managementUrl`: Actuator base URL +- `healthUrl`: Health endpoint URL +- `serviceUrl`: Application base URL +- `source`: Registration source (e.g., "http-api", "discovery") +- `metadata`: Custom metadata map + +**Example**: + +```json +{ + "instance": "abc123def456", + "version": 0, + "timestamp": "2026-02-07T10:00:00Z", + "type": "REGISTERED", + "registration": { + "name": "my-service", + "managementUrl": "http://localhost:8080/actuator", + "healthUrl": "http://localhost:8080/actuator/health", + "serviceUrl": "http://localhost:8080", + "source": "http-api", + "metadata": { + "startup": "2026-02-07T09:59:55Z" + } + } +} +``` + +**Use Cases**: + +- Send welcome notifications +- Initialize instance-specific monitoring +- Log new instance registrations +- Trigger discovery of endpoints + +**Example Listener**: + +```java + +@Component +public class RegistrationListener { + + @EventListener + public void onInstanceRegistered(InstanceRegisteredEvent event) { + log.info("New instance registered: {} at {}", + event.getRegistration().getName(), + event.getRegistration().getServiceUrl()); + } +} +``` + +--- + +### 2. REGISTRATION_UPDATED + +**Class**: `InstanceRegistrationUpdatedEvent` + +**Type Constant**: `"REGISTRATION_UPDATED"` + +**When Emitted**: Instance updates its registration (URL, metadata, etc.) + +**Payload**: + +```java +public class InstanceRegistrationUpdatedEvent extends InstanceEvent { + Registration registration; // Updated registration info +} +``` + +**Common Triggers**: + +- Instance IP address changes +- Management port changes +- Metadata updates +- Health URL changes + +**Example**: + +```json +{ + "instance": "abc123def456", + "version": 5, + "timestamp": "2026-02-07T11:00:00Z", + "type": "REGISTRATION_UPDATED", + "registration": { + "name": "my-service", + "managementUrl": "http://192.168.1.100:8080/actuator", + "healthUrl": "http://192.168.1.100:8080/actuator/health", + "serviceUrl": "http://192.168.1.100:8080", + "metadata": { + "version": "2.0.0" + } + } +} +``` + +**Use Cases**: + +- Detect instance migrations +- Update monitoring endpoints +- Track configuration changes +- Trigger re-discovery of endpoints + +--- + +### 3. DEREGISTERED + +**Class**: `InstanceDeregisteredEvent` + +**Type Constant**: `"DEREGISTERED"` + +**When Emitted**: Instance unregisters (shutdown, explicit deregistration) + +**Payload**: + +```java +public class InstanceDeregisteredEvent extends InstanceEvent { + // No additional fields - just base InstanceEvent fields +} +``` + +**Example**: + +```json +{ + "instance": "abc123def456", + "version": 10, + "timestamp": "2026-02-07T12:00:00Z", + "type": "DEREGISTERED" +} +``` + +**Use Cases**: + +- Send shutdown notifications +- Clean up instance-specific resources +- Log instance lifecycle +- Trigger alerts for unexpected shutdowns + +**Example Listener**: + +```java + +@EventListener +public void onInstanceDeregistered(InstanceDeregisteredEvent event) { + Instant timestamp = event.getTimestamp(); + long version = event.getVersion(); + + log.info("Instance {} deregistered after {} events", + event.getInstance(), version); + + // Cleanup resources + cleanupResourcesFor(event.getInstance()); +} +``` + +--- + +### 4. STATUS_CHANGED + +**Class**: `InstanceStatusChangedEvent` + +**Type Constant**: `"STATUS_CHANGED"` + +**When Emitted**: Instance health status changes + +**Payload**: + +```java +public class InstanceStatusChangedEvent extends InstanceEvent { + StatusInfo statusInfo; // Current status information +} +``` + +**StatusInfo Contents**: + +- `status`: Current status (`UP`, `DOWN`, `OUT_OF_SERVICE`, `UNKNOWN`, `OFFLINE`) +- `details`: Map of health details from actuator + +**Status Values**: + +- `UP`: Application is healthy +- `DOWN`: Application is unhealthy +- `OUT_OF_SERVICE`: Temporarily unavailable +- `UNKNOWN`: Status cannot be determined +- `OFFLINE`: Instance not responding +- `RESTRICTED`: Custom status + +**Example**: + +```json +{ + "instance": "abc123def456", + "version": 3, + "timestamp": "2026-02-07T10:05:00Z", + "type": "STATUS_CHANGED", + "statusInfo": { + "status": "UP", + "details": { + "diskSpace": { + "status": "UP", + "total": 500000000000, + "free": 250000000000 + }, + "db": { + "status": "UP", + "database": "PostgreSQL", + "validationQuery": "isValid()" + } + } + } +} +``` + +**Use Cases**: + +- Send alerts on status changes (UP → DOWN) +- Track uptime/downtime +- Trigger automated recovery +- Update dashboards + +**Example Listener**: + +```java + +@EventListener +public void onStatusChanged(InstanceStatusChangedEvent event) { + StatusInfo statusInfo = event.getStatusInfo(); + String status = statusInfo.getStatus(); + + if ("DOWN".equals(status)) { + alertService.sendAlert( + "Instance " + event.getInstance() + " is DOWN", + statusInfo.getDetails() + ); + } +} +``` + +--- + +### 5. ENDPOINTS_DETECTED + +**Class**: `InstanceEndpointsDetectedEvent` + +**Type Constant**: `"ENDPOINTS_DETECTED"` + +**When Emitted**: Actuator endpoints are discovered + +**Payload**: + +```java +public class InstanceEndpointsDetectedEvent extends InstanceEvent { + Endpoints endpoints; // Discovered endpoints +} +``` + +**Endpoints Contents**: +List of `Endpoint` objects, each containing: + +- `id`: Endpoint ID (e.g., "health", "metrics", "env") +- `url`: Full endpoint URL + +**Example**: + +```json +{ + "instance": "abc123def456", + "version": 1, + "timestamp": "2026-02-07T10:00:05Z", + "type": "ENDPOINTS_DETECTED", + "endpoints": [ + { + "id": "health", + "url": "http://localhost:8080/actuator/health" + }, + { + "id": "metrics", + "url": "http://localhost:8080/actuator/metrics" + }, + { + "id": "env", + "url": "http://localhost:8080/actuator/env" + }, + { + "id": "loggers", + "url": "http://localhost:8080/actuator/loggers" + } + ] +} +``` + +**Use Cases**: + +- Enable/disable UI views based on available endpoints +- Start monitoring specific endpoints +- Validate expected endpoints are present +- Trigger custom endpoint polling + +**Example Listener**: + +```java + +@EventListener +public void onEndpointsDetected(InstanceEndpointsDetectedEvent event) { + Endpoints endpoints = event.getEndpoints(); + + boolean hasMetrics = endpoints.get("metrics").isPresent(); + boolean hasLoggers = endpoints.get("loggers").isPresent(); + + if (hasMetrics && hasLoggers) { + // Enable advanced monitoring + advancedMonitoring.enable(event.getInstance()); + } +} +``` + +--- + +### 6. INFO_CHANGED + +**Class**: `InstanceInfoChangedEvent` + +**Type Constant**: `"INFO_CHANGED"` + +**When Emitted**: Info endpoint data changes + +**Payload**: + +```java +public class InstanceInfoChangedEvent extends InstanceEvent { + Info info; // Info endpoint data +} +``` + +**Info Contents**: +Map of arbitrary info data from `/actuator/info`, commonly including: + +- `build`: Build information (version, time, artifact) +- `git`: Git information (commit, branch, time) +- Custom application metadata + +**Example**: + +```json +{ + "instance": "abc123def456", + "version": 2, + "timestamp": "2026-02-07T10:00:10Z", + "type": "INFO_CHANGED", + "info": { + "build": { + "version": "1.0.0", + "artifact": "my-service", + "name": "my-service", + "time": "2026-02-07T09:00:00Z" + }, + "git": { + "branch": "main", + "commit": { + "id": "abc123", + "time": "2026-02-06T15:30:00Z" + } + }, + "custom": { + "team": "Platform", + "environment": "production" + } + } +} +``` + +**Use Cases**: + +- Track deployed versions +- Display build information in UI +- Verify correct version deployed +- Trigger version-specific logic + +--- + +## Event Ordering + +Events are ordered by `version` number, which is monotonically increasing per instance: + +``` +version 0: REGISTERED +version 1: ENDPOINTS_DETECTED +version 2: STATUS_CHANGED (to UP) +version 3: INFO_CHANGED +version 4: STATUS_CHANGED (to DOWN) +version 5: STATUS_CHANGED (to UP) +version 6: DEREGISTERED +``` + +**Important**: Version numbers are unique per instance and always increase. + +## Event Persistence + +Events are stored in the `InstanceEventStore`: + +- **InMemoryEventStore**: Non-persistent, lost on restart +- **HazelcastEventStore**: Distributed, persisted across cluster + +**Event Compaction**: Old events are compacted to prevent unlimited growth: + +```yaml +spring: + boot: + admin: + event-store: + max-log-size-per-aggregate: 100 # Keep last 100 events per instance +``` + +## Listening to Events + +### Spring Event Listener + +```java + +@Component +public class MyEventListener { + + @EventListener + public void onAnyInstanceEvent(InstanceEvent event) { + log.info("Event: {} for instance {} at version {}", + event.getType(), event.getInstance(), event.getVersion()); + } + + @EventListener + public void onSpecificEvent(InstanceStatusChangedEvent event) { + // Handle specific event type + } +} +``` + +### Custom Notifier + +```java + +@Component +public class CustomNotifier extends AbstractEventNotifier { + + public CustomNotifier(InstanceRepository repository) { + super(repository); + } + + @Override + protected Mono doNotify(InstanceEvent event, Instance instance) { + return Mono.fromRunnable(() -> { + switch (event.getType()) { + case "STATUS_CHANGED": + handleStatusChange((InstanceStatusChangedEvent) event); + break; + case "REGISTERED": + handleRegistration((InstanceRegisteredEvent) event); + break; + // Handle other events + } + }); + } +} +``` + +### Event Stream (SSE) + +Subscribe to event stream via REST API: + +```bash +curl -N http://localhost:8080/instances/events +``` + +Returns Server-Sent Events (SSE) stream: + +``` +data:{"instance":"abc123","version":0,"type":"REGISTERED",...} + +data:{"instance":"abc123","version":1,"type":"ENDPOINTS_DETECTED",...} + +data:{"instance":"abc123","version":2,"type":"STATUS_CHANGED",...} +``` + +## Event Filtering + +Filter events by type using `FilteringNotifier`: + +```java + +@Bean +public FilteringNotifier filteringNotifier(Notifier delegate, + InstanceRepository repository) { + FilteringNotifier notifier = new FilteringNotifier(delegate, repository); + notifier.setFilterExpression("!(type == 'INFO_CHANGED')"); // Exclude INFO_CHANGED + return notifier; +} +``` + +**Filter Expression Language**: SpEL (Spring Expression Language) + +**Available Variables**: + +- `type`: Event type string +- `instance`: Instance ID +- `version`: Event version +- Event-specific fields (e.g., `statusInfo.status` for STATUS_CHANGED) + +**Examples**: + +```java +// Only DOWN events +"type == 'STATUS_CHANGED' && statusInfo.status == 'DOWN'" + +// Exclude INFO_CHANGED and ENDPOINTS_DETECTED + "!(type == 'INFO_CHANGED' || type == 'ENDPOINTS_DETECTED')" + +// Only production instances (via metadata) + "metadata['environment'] == 'production'" +``` + +## Event Reminders + +Use `RemindingNotifier` to send periodic reminders: + +```java + +@Bean +public RemindingNotifier remindingNotifier(Notifier delegate, + InstanceRepository repository) { + RemindingNotifier notifier = new RemindingNotifier(delegate, repository); + notifier.setReminderPeriod(Duration.ofMinutes(10)); + notifier.setCheckReminderInverval(Duration.ofSeconds(60)); + return notifier; +} +``` + +Sends reminders for instances still in non-UP status after configured period. + +## See Also + +- [Custom Notifiers](../02-server/notifications/90-custom-notifiers.md) +- [Instance Registry](../02-server/40-instance-registry.md) +- [REST API](./20-rest-api.md) diff --git a/spring-boot-admin-docs/src/site/docs/10-reference/20-rest-api.md b/spring-boot-admin-docs/src/site/docs/10-reference/20-rest-api.md new file mode 100644 index 00000000000..14434e5100f --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/10-reference/20-rest-api.md @@ -0,0 +1,683 @@ +--- + +sidebar_position: 20 +sidebar_custom_props: + icon: 'book' +--- + +# REST API Reference + +Complete HTTP API reference for Spring Boot Admin Server. + +## Base URL + +Default: `http://localhost:8080` + +With custom context path: + +```yaml +spring: + boot: + admin: + context-path: /admin +``` + +Base URL becomes: `http://localhost:8080/admin` + +## Content Types + +- **Request**: `application/json` +- **Response**: `application/json` or `application/hal+json` +- **Streaming**: `text/event-stream` (Server-Sent Events) + +## Authentication + +If Spring Security is enabled, all endpoints require authentication: + +```bash +curl -u user:password http://localhost:8080/instances +``` + +Or use token-based authentication as configured in your security setup. + +## Instances API + +### Register Instance + +Register a new instance with the Admin Server. + +**Endpoint**: `POST /instances` + +**Request Body**: + +```json +{ + "name": "my-service", + "managementUrl": "http://localhost:8081/actuator", + "healthUrl": "http://localhost:8081/actuator/health", + "serviceUrl": "http://localhost:8081", + "metadata": { + "startup": "2026-02-07T10:00:00Z", + "tags": { + "environment": "production" + } + } +} +``` + +**Response**: `201 Created` + +```json +{ + "id": "abc123def456" +} +``` + +**Headers**: + +- `Location`: `/instances/abc123def456` + +**Example**: + +```bash +curl -X POST http://localhost:8080/instances \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-service", + "managementUrl": "http://localhost:8081/actuator", + "healthUrl": "http://localhost:8081/actuator/health", + "serviceUrl": "http://localhost:8081" + }' +``` + +--- + +### List All Instances + +Get all registered instances. + +**Endpoint**: `GET /instances` + +**Response**: `200 OK` + +```json +[ + { + "id": "abc123def456", + "version": 5, + "registration": { + "name": "my-service", + "managementUrl": "http://localhost:8081/actuator", + "healthUrl": "http://localhost:8081/actuator/health", + "serviceUrl": "http://localhost:8081", + "source": "http-api", + "metadata": {} + }, + "registered": true, + "statusInfo": { + "status": "UP", + "details": {} + }, + "statusTimestamp": "2026-02-07T10:05:00Z", + "info": {}, + "endpoints": [ + { + "id": "health", + "url": "http://localhost:8081/actuator/health" + }, + { + "id": "metrics", + "url": "http://localhost:8081/actuator/metrics" + } + ], + "buildVersion": "1.0.0", + "tags": { + "environment": "production" + } + } +] +``` + +**Example**: + +```bash +curl http://localhost:8080/instances +``` + +--- + +### List Instances by Name + +Get all instances with a specific name. + +**Endpoint**: `GET /instances?name={name}` + +**Parameters**: + +- `name` (required): Application name + +**Response**: `200 OK` + +Same as List All Instances, but filtered. + +**Example**: + +```bash +curl http://localhost:8080/instances?name=my-service +``` + +--- + +### Get Single Instance + +Get details of a specific instance. + +**Endpoint**: `GET /instances/{id}` + +**Parameters**: + +- `id` (path): Instance ID + +**Response**: `200 OK` + +```json +{ + "id": "abc123def456", + "version": 5, + "registration": { + "name": "my-service", + "managementUrl": "http://localhost:8081/actuator", + "healthUrl": "http://localhost:8081/actuator/health", + "serviceUrl": "http://localhost:8081" + }, + "registered": true, + "statusInfo": { + "status": "UP" + }, + "endpoints": [...] +} +``` + +**Example**: + +```bash +curl http://localhost:8080/instances/abc123def456 +``` + +--- + +### Deregister Instance + +Remove an instance from the registry. + +**Endpoint**: `DELETE /instances/{id}` + +**Parameters**: + +- `id` (path): Instance ID + +**Response**: `204 No Content` + +**Example**: + +```bash +curl -X DELETE http://localhost:8080/instances/abc123def456 +``` + +--- + +### Instance Event Stream + +Subscribe to real-time instance events via Server-Sent Events. + +**Endpoint**: `GET /instances/events` + +**Response**: `200 OK` (streaming) + +**Content-Type**: `text/event-stream` + +**Event Format**: + +``` +data:{"instance":"abc123","version":0,"type":"REGISTERED","timestamp":"2026-02-07T10:00:00Z","registration":{...}} + +data:{"instance":"abc123","version":1,"type":"ENDPOINTS_DETECTED","timestamp":"2026-02-07T10:00:05Z","endpoints":[...]} + +data:{"instance":"abc123","version":2,"type":"STATUS_CHANGED","timestamp":"2026-02-07T10:00:10Z","statusInfo":{"status":"UP"}} +``` + +**Example**: + +```bash +curl -N http://localhost:8080/instances/events +``` + +**JavaScript Client**: + +```javascript +const eventSource = new EventSource('http://localhost:8080/instances/events'); + +eventSource.onmessage = (event) => { + const instanceEvent = JSON.parse(event.data); + console.log('Event:', instanceEvent.type, 'for', instanceEvent.instance); +}; +``` + +**Heartbeat**: Ping comments sent every 10 seconds to keep connection alive. + +--- + +### Instance Event Stream (Single Instance) + +Subscribe to events for a specific instance. + +**Endpoint**: `GET /instances/{id}/events` + +**Parameters**: + +- `id` (path): Instance ID + +**Response**: Same as `/instances/events`, but filtered to single instance. + +**Example**: + +```bash +curl -N http://localhost:8080/instances/abc123def456/events +``` + +--- + +## Applications API + +Applications represent logical groups of instances with the same name. + +### List All Applications + +Get all applications (grouped instances). + +**Endpoint**: `GET /applications` + +**Response**: `200 OK` + +```json +[ + { + "name": "my-service", + "buildVersion": "1.0.0", + "status": "UP", + "instances": [ + { + "id": "abc123", + "healthUrl": "http://instance1:8081/actuator/health", + "statusInfo": {"status": "UP"} + }, + { + "id": "def456", + "healthUrl": "http://instance2:8081/actuator/health", + "statusInfo": {"status": "UP"} + } + ] + } +] +``` + +**Example**: + +```bash +curl http://localhost:8080/applications +``` + +--- + +### Get Single Application + +Get details of a specific application. + +**Endpoint**: `GET /applications/{name}` + +**Parameters**: + +- `name` (path): Application name + +**Response**: `200 OK` + +```json +{ + "name": "my-service", + "buildVersion": "1.0.0", + "status": "UP", + "instances": [...] +} +``` + +**Response**: `404 Not Found` if application doesn't exist. + +**Example**: + +```bash +curl http://localhost:8080/applications/my-service +``` + +--- + +### Application Event Stream + +Subscribe to application-level events. + +**Endpoint**: `GET /applications/events` + +**Response**: `200 OK` (streaming) + +**Content-Type**: `text/event-stream` + +**Example**: + +```bash +curl -N http://localhost:8080/applications/events +``` + +--- + +### Refresh Applications + +Trigger manual refresh of all instances from service discovery. + +**Endpoint**: `POST /applications` + +**Response**: `200 OK` + +**Example**: + +```bash +curl -X POST http://localhost:8080/applications +``` + +**Use Case**: Force refresh when using service discovery (Eureka, Consul, etc.) + +--- + +### Delete Application + +Deregister all instances of an application. + +**Endpoint**: `DELETE /applications/{name}` + +**Parameters**: + +- `name` (path): Application name + +**Response**: `204 No Content` + +**Example**: + +```bash +curl -X DELETE http://localhost:8080/applications/my-service +``` + +--- + +## Instance Actuator Proxy + +Admin Server proxies requests to instance actuator endpoints. + +### General Pattern + +**Endpoint**: `GET /instances/{id}/actuator/{endpoint}` + +**Parameters**: + +- `id` (path): Instance ID +- `endpoint` (path): Actuator endpoint name + +**Response**: Proxied response from instance + +**Examples**: + +```bash +# Get health +curl http://localhost:8080/instances/abc123/actuator/health + +# Get metrics +curl http://localhost:8080/instances/abc123/actuator/metrics + +# Get specific metric +curl http://localhost:8080/instances/abc123/actuator/metrics/jvm.memory.used + +# Get environment +curl http://localhost:8080/instances/abc123/actuator/env + +# Get loggers +curl http://localhost:8080/instances/abc123/actuator/loggers +``` + +### Common Endpoints + +| Endpoint | Description | +|----------------------------|------------------------| +| `/actuator/health` | Health status | +| `/actuator/info` | Build and app info | +| `/actuator/metrics` | Metrics list | +| `/actuator/metrics/{name}` | Specific metric | +| `/actuator/env` | Environment properties | +| `/actuator/loggers` | Logger configuration | +| `/actuator/loggers/{name}` | Specific logger | +| `/actuator/httptrace` | HTTP trace | +| `/actuator/threaddump` | Thread dump | +| `/actuator/heapdump` | Heap dump (binary) | +| `/actuator/jolokia` | JMX via Jolokia | + +### Modify Logger Level + +**Endpoint**: `POST /instances/{id}/actuator/loggers/{name}` + +**Request Body**: + +```json +{ + "configuredLevel": "DEBUG" +} +``` + +**Example**: + +```bash +curl -X POST http://localhost:8080/instances/abc123/actuator/loggers/com.example \ + -H "Content-Type: application/json" \ + -d '{"configuredLevel":"DEBUG"}' +``` + +--- + +## Instance Operations + +### Restart Instance + +Restart a Spring Boot application (requires `spring-boot-starter-actuator` with restart endpoint). + +**Endpoint**: `POST /instances/{id}/actuator/restart` + +**Response**: `200 OK` + +**Example**: + +```bash +curl -X POST http://localhost:8080/instances/abc123/actuator/restart +``` + +:::warning Dangerous Operation +This will restart the application. Ensure the restart endpoint is properly secured. +::: + +--- + +### Shutdown Instance + +Gracefully shutdown a Spring Boot application. + +**Endpoint**: `POST /instances/{id}/actuator/shutdown` + +**Response**: `200 OK` + +```json +{ + "message": "Shutting down, bye..." +} +``` + +**Example**: + +```bash +curl -X POST http://localhost:8080/instances/abc123/actuator/shutdown +``` + +:::warning Dangerous Operation +This will shut down the application. Ensure the shutdown endpoint is properly secured and consider it carefully in +production. +::: + +--- + +## Error Responses + +### 400 Bad Request + +Invalid request body or parameters. + +```json +{ + "error": "Bad Request", + "message": "Invalid registration data", + "status": 400 +} +``` + +### 404 Not Found + +Instance or application not found. + +```json +{ + "error": "Not Found", + "message": "Instance not found: abc123", + "status": 404 +} +``` + +### 500 Internal Server Error + +Server error. + +```json +{ + "error": "Internal Server Error", + "message": "Failed to register instance", + "status": 500 +} +``` + +## CORS Support + +Cross-Origin Resource Sharing (CORS) configuration: + +```yaml +spring: + boot: + admin: + cors: + allowed-origins: "http://localhost:3000" + allowed-methods: "GET,POST,DELETE" + allowed-headers: "*" + exposed-headers: "Location" + allow-credentials: true + max-age: 3600 +``` + +## Rate Limiting + +No built-in rate limiting. Use reverse proxy (nginx, API gateway) for rate limiting if needed. + +## Pagination + +Instance and application lists are not paginated. For large deployments, consider filtering by name or using service +discovery-based filtering. + +## Caching + +Responses are not cached by default. Add caching headers via reverse proxy if needed. + +## WebSocket Support + +Not supported. Use Server-Sent Events (SSE) for real-time updates. + +## API Clients + +### Java + +```java +RestTemplate restTemplate = new RestTemplate(); + +// Register instance +Registration registration = Registration.create("my-service") + .managementUrl("http://localhost:8081/actuator") + .healthUrl("http://localhost:8081/actuator/health") + .serviceUrl("http://localhost:8081") + .build(); + +ResponseEntity response = restTemplate.postForEntity( + "http://localhost:8080/instances", + registration, + Map.class +); + +String instanceId = (String) response.getBody().get("id"); +``` + +### JavaScript/TypeScript + +```javascript +// Register instance +const registration = { + name: 'my-service', + managementUrl: 'http://localhost:8081/actuator', + healthUrl: 'http://localhost:8081/actuator/health', + serviceUrl: 'http://localhost:8081' +}; + +const response = await fetch('http://localhost:8080/instances', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(registration) +}); + +const { id } = await response.json(); +console.log('Instance ID:', id); +``` + +### Python + +```python +import requests + +# Register instance +registration = { + "name": "my-service", + "managementUrl": "http://localhost:8081/actuator", + "healthUrl": "http://localhost:8081/actuator/health", + "serviceUrl": "http://localhost:8081" +} + +response = requests.post( + "http://localhost:8080/instances", + json=registration +) + +instance_id = response.json()["id"] +print(f"Instance ID: {instance_id}") +``` + +## See Also + +- [Event Types](./10-event-types.md) +- [Server Configuration](../02-server/01-server.mdx) +- [Client Registration](../03-client/20-registration.md) +- [Security](../05-security/) diff --git a/spring-boot-admin-docs/src/site/docs/80-actuator-endpoints-overview.mdx b/spring-boot-admin-docs/src/site/docs/10-reference/60-actuator-endpoints.mdx similarity index 99% rename from spring-boot-admin-docs/src/site/docs/80-actuator-endpoints-overview.mdx rename to spring-boot-admin-docs/src/site/docs/10-reference/60-actuator-endpoints.mdx index 50ad1c75346..9d0136a41ce 100644 --- a/spring-boot-admin-docs/src/site/docs/80-actuator-endpoints-overview.mdx +++ b/spring-boot-admin-docs/src/site/docs/10-reference/60-actuator-endpoints.mdx @@ -1,3 +1,7 @@ +--- +sidebar_custom_props: + icon: 'book' +--- # Supported Spring Boot Actuator Endpoints Below is a comprehensive list of actuator endpoints which are supported by Spring Boot Admin. diff --git a/spring-boot-admin-docs/src/site/docs/10-reference/_category_.json b/spring-boot-admin-docs/src/site/docs/10-reference/_category_.json new file mode 100644 index 00000000000..ff391b81726 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/10-reference/_category_.json @@ -0,0 +1,4 @@ +{ + "position": 10, + "label": "Reference" +} diff --git a/spring-boot-admin-docs/src/site/docs/10-reference/index.md b/spring-boot-admin-docs/src/site/docs/10-reference/index.md new file mode 100644 index 00000000000..7c1d1476e7a --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/10-reference/index.md @@ -0,0 +1,134 @@ +--- + +sidebar_position: 95 +sidebar_custom_props: + icon: 'book' +--- + +# Reference Documentation + +Comprehensive reference material for Spring Boot Admin including event types, REST API, and configuration properties. + +## Contents + +### [Event Types](./10-event-types.md) + +Complete catalog of all `InstanceEvent` types emitted by Spring Boot Admin, including: + +- Event lifecycle and ordering +- Event payloads and properties +- Common use cases for each event +- Example event listeners + +### [REST API](./20-rest-api.md) + +HTTP API reference for Spring Boot Admin Server (intended for internal use by the SPA, not for public consumption): + +- Instance registration endpoints +- Application management endpoints +- Event streaming +- Instance operations (restart, shutdown, etc.) + +**Note**: The REST API is primarily designed for use by the built-in Single Page Application (SPA) and should not be +considered a stable public API. Use at your own risk for external integrations. + +## Quick Links + +### Event Types + +All events extend `InstanceEvent` base class: + +| Event Type | Description | +|------------------------|-------------------------------| +| `REGISTERED` | Instance first registered | +| `REGISTRATION_UPDATED` | Instance registration changed | +| `DEREGISTERED` | Instance unregistered | +| `STATUS_CHANGED` | Health status changed | +| `ENDPOINTS_DETECTED` | Actuator endpoints discovered | +| `INFO_CHANGED` | Info endpoint data changed | + +### Common Properties + +#### Server Context Path + +```yaml +spring: + boot: + admin: + context-path: /admin # Default: / +``` + +#### Client Registration + +```yaml +spring: + boot: + admin: + client: + url: http://localhost:8080 + instance: + name: ${spring.application.name} +``` + +### Status Values + +Health status values in order of precedence: + +1. `DOWN` - Application not healthy +2. `OUT_OF_SERVICE` - Temporarily unavailable +3. `OFFLINE` - Instance not responding +4. `UNKNOWN` - Status cannot be determined +5. `UP` - Application healthy +6. `RESTRICTED` - Custom status (application-defined) + +## API Versioning + +Spring Boot Admin does not version its REST API. The API is primarily intended for internal use by the SPA and is not +guaranteed to be stable for external integrations. + +**Base Path**: `{server.context-path}/instances` (default: `/instances`) + +**Content Type**: `application/json` (HAL JSON for collection endpoints) + +**Stability**: The REST API is designed for internal use by the built-in SPA and may change without notice. For stable +integrations, consider using the event notification system instead. + +## Property Prefixes + +All Spring Boot Admin properties use these prefixes: + +### Server Properties + +- `spring.boot.admin.*` - Core server configuration +- `spring.boot.admin.ui.*` - UI customization +- `spring.boot.admin.discovery.*` - Service discovery +- `spring.boot.admin.monitor.*` - Monitoring settings +- `spring.boot.admin.notify.*` - Notification settings + +### Client Properties + +- `spring.boot.admin.client.*` - Client configuration +- `spring.boot.admin.client.instance.*` - Instance metadata + +## Configuration Metadata + +Spring Boot Admin provides complete configuration metadata for IDE autocomplete: + +```xml + + de.codecentric + spring-boot-admin-server + +``` + +Metadata files: + +- `spring-configuration-metadata.json` (server) +- `additional-spring-configuration-metadata.json` (client) + +## See Also + +- [Server Configuration](../02-server/01-server.mdx) +- [Client Configuration](../03-client/20-registration.md) +- [Customization](../06-customization/) +- [Notifications](../02-server/notifications/) diff --git a/spring-boot-admin-docs/src/site/docs/11-upgrading/01-spring-boot-admin-4.md b/spring-boot-admin-docs/src/site/docs/11-upgrading/01-spring-boot-admin-4.md new file mode 100644 index 00000000000..bf3bc67d3a1 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/11-upgrading/01-spring-boot-admin-4.md @@ -0,0 +1,320 @@ +--- +sidebar_custom_props: + icon: 'arrow-up' +--- + +# To Spring Boot Admin 4 + +This guide covers the breaking changes, deprecated features, and migration steps required to upgrade from Spring Boot +Admin 3.x to 4.x. + +## Prerequisites + +Before upgrading to Spring Boot Admin 4, ensure your application meets these requirements: + +- **Spring Boot 4.0+** - Spring Boot Admin 4 requires Spring Boot 4.0 or higher +- **Java 17+** - Minimum Java version is 17 +- **Review dependencies** - Check that all third-party dependencies are compatible with Spring Boot 4 + +:::tip Java Version Compatibility +Spring Boot Admin strives to support the same Java baseline version as the corresponding Spring Boot version. This +means: + +- Spring Boot Admin 4.x supports the same minimum Java version as Spring Boot 4.x (Java 17+) +- Future Spring Boot Admin releases will align with Spring Boot's Java requirements + +Always check the [Spring Boot documentation](https://docs.spring.io/spring-boot/system-requirements.html) for the +supported Java versions of your Spring Boot version. +::: + +--- + +## Breaking Changes + +### 1. Nullable Annotations Change + +**What Changed:** + +Spring Boot Admin 4 replaces Spring's nullable annotations with JSpecify annotations for better null-safety across the +Java ecosystem. + +**Migration:** + +```java +// Before (Spring Boot Admin 3.x) +import org.springframework.lang.Nullable; + +public class MyService { + public void process(@Nullable String value) { + // ... + } +} +``` + +```java +// After (Spring Boot Admin 4.x) +import org.jspecify.annotations.Nullable; + +public class MyService { + public void process(@Nullable String value) { + // ... + } +} +``` + +**Action Required:** + +If you extend Spring Boot Admin classes or implement interfaces using `@Nullable` annotations: + +1. Add JSpecify dependency to your `pom.xml`: + +```xml + + org.jspecify + jspecify + 1.0.0 + +``` + +2. Update your imports from `org.springframework.lang.Nullable` to `org.jspecify.annotations.Nullable` + +--- + +### 2. HTTP Client Configuration Changes + +**What Changed:** + +Spring Boot Admin 4 modernizes HTTP client usage: + +- **Client**: Now uses `RestClient` exclusively (replaces `WebClient` autoconfiguration) +- **Server**: Uses `WebClient` for instance communication and `RestTemplate` for notifiers + +**Migration:** + +#### For Admin Client + +The client autoconfiguration now provides `RestClient` instead of `WebClient`: + +```java +// Before (Spring Boot Admin 3.x) +@Bean +public WebClient.Builder webClientBuilder() { + return WebClient.builder() + .defaultHeader("X-Custom-Header", "value"); +} +``` + +```java +// After (Spring Boot Admin 4.x) +@Bean +public RestClient.Builder restClientBuilder() { + return RestClient.builder() + .defaultHeader("X-Custom-Header", "value"); +} +``` + +#### For Admin Server + +No changes required - the server continues using `WebClient` for instance communication: + +```java +// Server-side customization (unchanged) +@Bean +public InstanceWebClient instanceWebClient(WebClient.Builder builder) { + return InstanceWebClient.builder(builder) + .connectTimeout(Duration.ofSeconds(5)) + .build(); +} +``` + +**Action Required:** + +- If you customize the client's HTTP configuration, migrate from `WebClient.Builder` to `RestClient.Builder` +- Update any custom beans that depend on `WebClient` in client applications + +--- + +### 3. Property Rename: `prefer-ip` Removed + +**What Changed:** + +The property `spring.boot.admin.client.instance.prefer-ip` has been removed in favor of the more flexible +`spring.boot.admin.client.instance.service-host-type`. + +**Migration:** + +```yaml +# Before (Spring Boot Admin 3.x) +spring: + boot: + admin: + client: + instance: + prefer-ip: true +``` + +```yaml +# After (Spring Boot Admin 4.x) +spring: + boot: + admin: + client: + instance: + service-host-type: IP # Options: IP, HOST_NAME, CANONICAL_HOST_NAME +``` + +**Available Options:** + +| Value | Description | +|-----------------------|------------------------------------------------------| +| `IP` | Use IP address (equivalent to old `prefer-ip: true`) | +| `HOST_NAME` | Use hostname (equivalent to old `prefer-ip: false`) | +| `CANONICAL_HOST_NAME` | Use canonical hostname | + +**Action Required:** + +- Search your configuration files for `prefer-ip` +- Replace with `service-host-type: IP` (if `prefer-ip: true`) or `service-host-type: HOST_NAME` (if `prefer-ip: false`) + +--- + +### 4. Jolokia Compatibility + +**What Changed:** + +The current stable Jolokia version (2.4.2) does not yet support Spring Boot 4. Spring Boot Admin 4 temporarily +downgrades to **Jolokia 2.1.0** for basic JMX functionality. + +**Limitations:** + +- Some advanced Jolokia features may not be available +- JMX operations work but with reduced functionality compared to Jolokia 2.4.2 + +**Future Outlook:** + +Spring Boot Admin will upgrade to a newer Jolokia version once Spring Boot 4 support is added. Monitor +the [Jolokia project](https://github.com/jolokia/jolokia) for updates on Spring Boot 4 compatibility. + +**Action Required:** + +- **No immediate action needed** - Jolokia 2.1.0 is included automatically and provides basic JMX functionality +- Test your JMX operations to ensure they work with the limited feature set +- If JMX functionality is critical, consider waiting for full Jolokia support before upgrading + +--- + +## Migration Checklist + +Follow these steps to ensure a smooth upgrade: + +### Step 1: Update Dependencies + +Update your `pom.xml`: + +```xml + + 4.0.0 + 4.0.0 + + + + + + de.codecentric + spring-boot-admin-starter-server + ${spring-boot-admin.version} + + + + + de.codecentric + spring-boot-admin-starter-client + ${spring-boot-admin.version} + + +``` + +### Step 2: Update Configuration + +1. **Replace `prefer-ip` property:** + +```bash +# Find and replace in all configuration files +grep -r "prefer-ip" src/main/resources/ +# Replace with service-host-type +``` + +2. **Review HTTP client customizations:** + +```bash +# Check for WebClient customizations in client apps +grep -r "WebClient.Builder" src/main/java/ +``` + +### Step 3: Update Code + +1. **Update nullable annotations:** + +```bash +# Find all Spring nullable imports +find src -name "*.java" -exec grep -l "org.springframework.lang.Nullable" {} \; + +# Replace with JSpecify +sed -i 's/org.springframework.lang.Nullable/org.jspecify.annotations.Nullable/g' +``` + +2. **Migrate client HTTP configuration:** + +Review and update any beans creating `WebClient.Builder` for the Admin Client. + +### Step 4: Test + +1. **Start the Admin Server:** + +```bash +mvn spring-boot:run +``` + +2. **Register a client application** +3. **Verify functionality:** + - Instance registration works + - Health checks update correctly + - Actuator endpoints are accessible + - Notifications fire properly + - JMX operations work (with Jolokia 2.1.0 limitations) + +### Step 5: Monitor Logs + +Watch for deprecation warnings or errors: + +```bash +tail -f logs/spring-boot-admin.log | grep -i "deprecat\|error\|warn" +``` + +--- + +## Getting Help + +If you encounter issues during the upgrade: + +1. **Check the changelog**: Review detailed changes in + the [release notes](https://github.com/codecentric/spring-boot-admin/releases) +2. **Search existing issues**: [GitHub Issues](https://github.com/codecentric/spring-boot-admin/issues) +3. **Ask the community**: [Stack Overflow](https://stackoverflow.com/questions/tagged/spring-boot-admin) with tag + `spring-boot-admin` +4. **Report bugs**: Create an issue on [GitHub](https://github.com/codecentric/spring-boot-admin/issues/new) + +--- + +## Summary + +**Key Changes:** + +- ✅ Update to Spring Boot 4.0+ +- ✅ Replace `org.springframework.lang.Nullable` with `org.jspecify.annotations.Nullable` +- ✅ Migrate client from `WebClient` to `RestClient` +- ✅ Change `prefer-ip` to `service-host-type` +- ⚠️ Accept Jolokia 2.1.0 limitations temporarily + +Most applications can upgrade with minimal code changes, primarily focused on configuration updates and dependency +management. diff --git a/spring-boot-admin-docs/src/site/docs/11-upgrading/_category_.json b/spring-boot-admin-docs/src/site/docs/11-upgrading/_category_.json new file mode 100644 index 00000000000..578a2cb6fcc --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/11-upgrading/_category_.json @@ -0,0 +1,4 @@ +{ + "position": 11, + "label": "Upgrading" +} diff --git a/spring-boot-admin-docs/src/site/docs/11-upgrading/index.md b/spring-boot-admin-docs/src/site/docs/11-upgrading/index.md new file mode 100644 index 00000000000..1a7c65bc29b --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/11-upgrading/index.md @@ -0,0 +1,10 @@ +--- +sidebar_custom_props: + icon: 'arrow-up' +--- + +import DocCardList from '@theme/DocCardList'; + +# Upgrading + + diff --git a/spring-boot-admin-docs/src/site/docs/client/99-properties.mdx b/spring-boot-admin-docs/src/site/docs/client/99-properties.mdx deleted file mode 100644 index e68d76e43a7..00000000000 --- a/spring-boot-admin-docs/src/site/docs/client/99-properties.mdx +++ /dev/null @@ -1,20 +0,0 @@ ---- -sidebar_custom_props: - icon: 'server' ---- - -# Spring Boot Admin Client - -The Spring Boot Admin Client is a Spring Boot Starter that registers itself with the Spring Boot Admin Server to -enable monitoring and management. By including the Spring Boot Admin Client Starter dependency in your application, the -Spring Boot Admin Server can access health, metrics, and other management endpoints, depending on which -Actuator endpoints are accessible. - -## Properties - -import metadata from "../../../../../spring-boot-admin-client/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../src/components/PropertyTable"; - - diff --git a/spring-boot-admin-docs/src/site/docs/customize/01-customize_ui.mdx b/spring-boot-admin-docs/src/site/docs/customize/01-customize_ui.mdx deleted file mode 100644 index 5b40aac7cb4..00000000000 --- a/spring-boot-admin-docs/src/site/docs/customize/01-customize_ui.mdx +++ /dev/null @@ -1,123 +0,0 @@ ---- -sidebar_custom_props: - icon: 'ui' ---- - -import metadata - from "../../../../../spring-boot-admin-server-ui/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../src/components/PropertyTable"; - -# Look and Feel - -You can set custom information in the header (i.e. displaying staging information or company name) by using following -configuration properties: - -* **spring.boot.admin.ui.brand**: This HTML snippet is rendered in navigation header and defaults to -`Spring Boot Admin`. By default, it shows the SBA logo -followed by its name. If you want to show a custom logo you can set: -`spring.boot.admin.ui.brand=`. Either you just add the image to your jar-file in -`/META-INF/spring-boot-admin-server-ui/` (SBA registers a `ResourceHandler` for this location by default), or you must -ensure yourself that the image gets served correctly (e.g. by registering your own `ResourceHandler`) -* **spring.boot.admin.ui.title**: Use this option to customize the browsers window title. - -## Customizing Colors - -You can provide a custom color theme to the application by overwriting the following properties: - -```yaml title="application.yml" -spring: - boot: - admin: - ui: - theme: - color: "#4A1420" - palette: - 50: "#F8EBE4" - 100: "#F2D7CC" - 200: "#E5AC9C" - 300: "#D87B6C" - 400: "#CB463B" - 500: "#9F2A2A" - 600: "#83232A" - 700: "#661B26" - 800: "#4A1420" - 900: "#2E0C16" -``` - - - -## Customizing Login Logo - -You can set a custom image to be displayed on the login page. - -1. Put the image in a resource location which is served via http (e.g. -`/META-INF/spring-boot-admin-server-ui/assets/img/`). -2. Configure the icons to use using the following property: - * **spring.boot.admin.ui.login-icon**: Used as icon on login page. (e.g `assets/img/custom-login-icon.svg`) - -## Customizing Favicon - -It is possible to use a custom favicon, which is also used for desktop notifications. Spring Boot Admin uses a different -icon when one or more application is down. - -1. Put the favicon (`.png` with at least 192x192 pixels) in a resource location which is served via http (e.g. -`/META-INF/spring-boot-admin-server-ui/assets/img/`). -2. Configure the icons to use using the following properties: - * `spring.boot.admin.ui.favicon`: Used as default icon. (e.g `assets/img/custom-favicon.png` - * `spring.boot.admin.ui.favicon-danger`: Used when one or more service is down. (e.g -`assets/img/custom-favicon-danger.png`) - -## Customizing Available Languages - -To filter languages to a subset of all supported languages: - -* **spring.boot.admin.ui.available-languages**: Used as a filter of existing languages. (e.g. `en,de` out of existing -`de,en,fr,ko,pt-BR,ru,zh`) - -## Show or hide views - -You can very simply hide views in the navbar: - -```yaml title="application.yml" -spring: - boot: - admin: - ui: - view-settings: - - name: "journal" - enabled: false -``` - -## Hide Service URL - -To hide service URLs in Spring Boot Admin UI entirely, set the following property in your Server's configuration: - - - -If you want to hide the URL for specific instances only, you can set the `hide-url` property in the instance metadata -while registering a service. -When using Spring Boot Admin Client you can set the property `spring.boot.admin.client.metadata.hide-url=true` in the -corresponding config file. The value set in `metadata` does not have any effect, when the URLs are disabled in Server. diff --git a/spring-boot-admin-docs/src/site/docs/customize/02-extend_ui.md b/spring-boot-admin-docs/src/site/docs/customize/02-extend_ui.md deleted file mode 100644 index 6b05c3a3dc8..00000000000 --- a/spring-boot-admin-docs/src/site/docs/customize/02-extend_ui.md +++ /dev/null @@ -1,283 +0,0 @@ ---- -sidebar_custom_props: - icon: 'ui' ---- - -# Extend the UI - -## Linking / Embedding External Pages in Navbar - -Links will be opened in a new window/tab (i.e. `target="_blank"`) having no access to opener and referrer (`rel="noopener noreferrer"`). - -### Simple link - -To add a simple link to an external page, use the following snippet. - -```yaml title="application.yml" -spring: - boot: - admin: - ui: - external-views: - - label: "🚀" #(1) - url: "https://codecentric.de" #(2) - order: 2000 #(3) -``` - -1. The label will be shown in the navbar -2. URL to the page you want to link to -3. Order that allows to specify position of item in navbar - -### Dropdown with links - -To aggregate links below a single element, dropdowns can be configured as follows. - -```yaml title="application.yml" -spring: - boot: - admin: - ui: - external-views: - - label: Link w/o children - children: - - label: "📖 Docs" - url: https://codecentric.github.io/spring-boot-admin/current/ - - label: "📦 Maven" - url: https://search.maven.org/search?q=g:de.codecentric%20AND%20a:spring-boot-admin-starter-server - - label: "🐙 GitHub" - url: https://github.com/codecentric/spring-boot-admin -``` - -### Dropdown as link having links as children - -```yaml title="application.yml" -spring: - boot: - admin: - ui: - external-views: - - label: Link w children - url: https://codecentric.de #(1) - children: - - label: "📖 Docs" - url: https://codecentric.github.io/spring-boot-admin/current/ - - label: "📦 Maven" - url: https://search.maven.org/search?q=g:de.codecentric%20AND%20a:spring-boot-admin-starter-server - - label: "🐙 GitHub" - url: https://github.com/codecentric/spring-boot-admin - - label: "🎅 Is it christmas" - url: https://isitchristmas.com - iframe: true -``` - -## Custom Views - -It is possible to add custom views to the ui. The views must be implemented as [Vue.js](https://vuejs.org/) components. - -The JavaScript-Bundle and CSS-Stylesheet must be placed on the classpath at `/META-INF/spring-boot-admin-server-ui/extensions/{name}/` so the server can pick them up. The [spring-boot-admin-sample-custom-ui](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/) module contains a sample which has the necessary maven setup to build such a module. - -The custom extension registers itself by calling: - -```javascript title="custom-ui.js" -SBA.use({ - install({ viewRegistry, i18n }) { - viewRegistry.addView({ - name: "custom", //(1) - path: "/custom", //(2) - component: custom, //(3) - group: "custom", //(4) - handle, //(5) - order: 1000, //(6) - }); - i18n.mergeLocaleMessage("en", { - custom: { - label: "My Extensions", //(7) - }, - }); - i18n.mergeLocaleMessage("de", { - custom: { - label: "Meine Erweiterung", - }, - }); - }, -}); -``` - -1. Name of the view and the route. -2. Path in Vue router. -3. The imported custom component, which will be rendered on the route. -4. An optional group name that allows to bind views to a logical group (defaults to "none") -5. The handle for the custom view to be shown in the top navigation bar. -6. Order for the view. -7. Using `i18n.mergeLocaleMessage` allows to add custom translations. - -Views in the top navigation bar are sorted by ascending order. - -If new top level routes are added to the frontend, they also must be known to the backend. Add a `/META-INF/spring-boot-admin-server-ui/extensions/{name}/routes.txt` with all your new toplevel routes (one route per line). - -Groups are used in instance sidebar to aggregate multiple views into a collapsible menu entry showing the group’s name. When a group contains just a single element, the label of the view is shown instead of the group’s name. - -#### Override/Set custom group icons - -In order to override or set icons for (custom) groups you can use the `SBA.viewRegistry.setGroupIcon` function as follows: - -```javascript title="custom-ui.js" -SBA.viewRegistry.setGroupIcon( - "custom", //(1) - ` - ` //(2) -); -``` - -1. Name of the group to set icon for -2. Arbitrary HTML code (e.g. SVG image) that is inserted and parsed as icon. - -#### Adding a Top-Level View - -Here is a simple top level view just listing all registered applications: - -```html title="custom-ui.vue" - - - -``` - -1. By destructuring `applications` of `SBA.useApplicationStore()`, you have reactive access to registered applications. - -:::tip -There are some helpful methods on the application and instances object available. Have a look at [application.ts](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-server-ui/src/main/frontend/services/application.ts) and [instance.ts](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-server-ui/src/main/frontend/services/instance.ts) -::: - -And this is how you register the top-level view. - -##### Example - -```javascript title="custom-ui.js" -SBA.viewRegistry.addView({ - name: "customSub", - parent: "custom", // (1) - path: "/customSub", // (2) - component: customSubitem, - label: "Custom Sub", - order: 1000, -}); -``` - -1. References the name of the parent view. -2. Router path used to navigate to. -3. Define whether the path should be registered as child route in parent’s route. When set to `true`, the parent component has to implement `` - -The `routes.txt` config with the added route: - -```text title="routes.txt" -/custom/** -/customSub/** -``` - -### Visualizing a Custom Endpoint - -Here is a view to show a custom endpoint: - -```html title="custom-ui.vue" - - - - - -``` - -1. If you define a `instance` prop the component will receive the instance the view should be rendered for. -2. Each instance has a preconfigured [axios](https://github.com/axios/axios) instance to access the endpoints with the correct path and headers. - -Registering the instance view works like for the top-level view with some additional properties: - -```javascript title="custom-ui.js" -SBA.viewRegistry.addView({ - name: "instances/custom", - parent: "instances", // (1) - path: "custom", - component: customEndpoint, - label: "Custom", - group: "custom", // (2) - order: 1000, - isEnabled: ({ instance }) => { - return instance.hasEndpoint("custom"); - }, // (3) -}); -``` - -1. The parent must be 'instances' in order to render the new custom view for a single instance. -2. You can group views by assigning them to a group. -3. If you add a `isEnabled` callback you can figure out dynamically if the view should be shown for the particular instance. - -:::note -You can override default views by putting the same group and name as the one you want to override. -::: - diff --git a/spring-boot-admin-docs/src/site/docs/customize/03-customize_http-headers.md b/spring-boot-admin-docs/src/site/docs/customize/03-customize_http-headers.md deleted file mode 100644 index 1b8a59edfaa..00000000000 --- a/spring-boot-admin-docs/src/site/docs/customize/03-customize_http-headers.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -sidebar_custom_props: - icon: 'http' ---- - -# HTTP Headers - -In case you need to inject custom HTTP headers into the requests made to the monitored application’s actuator endpoints you can easily add a `HttpHeadersProvider`: - -```java title="CustomHttpHeadersProvider.java" -@Bean -public HttpHeadersProvider customHttpHeadersProvider() { - return (instance) -> { - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add("X-CUSTOM", "My Custom Value"); - return httpHeaders; - }; -} -``` diff --git a/spring-boot-admin-docs/src/site/docs/customize/04-customize_interceptors.md b/spring-boot-admin-docs/src/site/docs/customize/04-customize_interceptors.md deleted file mode 100644 index 6ba336f4b43..00000000000 --- a/spring-boot-admin-docs/src/site/docs/customize/04-customize_interceptors.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -sidebar_custom_props: - icon: 'http' ---- - -# HTTP Interceptors - -You can intercept and modify requests and responses made to the monitored application’s actuator endpoints by implementing the `InstanceExchangeFilterFunction` interface. This can be useful for auditing or adding some extra security checks. - -```java title="CustomHttpInterceptor.java" -@Bean -public InstanceExchangeFilterFunction auditLog() { - return (instance, request, next) -> next.exchange(request).doOnSubscribe((s) -> { - if (HttpMethod.DELETE.equals(request.method()) || HttpMethod.POST.equals(request.method())) { - log.info("{} for {} on {}", request.method(), instance.getId(), request.url()); - } - }); -} -``` diff --git a/spring-boot-admin-docs/src/site/docs/customize/99-ui-properties.mdx b/spring-boot-admin-docs/src/site/docs/customize/99-ui-properties.mdx deleted file mode 100644 index 62ce534313c..00000000000 --- a/spring-boot-admin-docs/src/site/docs/customize/99-ui-properties.mdx +++ /dev/null @@ -1,12 +0,0 @@ ---- -sidebar_custom_props: - icon: 'properties' ---- -# Properties - -import metadata from "../../../../../spring-boot-admin-server-ui/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../src/components/PropertyTable"; - - diff --git a/spring-boot-admin-docs/src/site/docs/customize/_category_.json b/spring-boot-admin-docs/src/site/docs/customize/_category_.json deleted file mode 100644 index 5e842889c09..00000000000 --- a/spring-boot-admin-docs/src/site/docs/customize/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Customizing", - "position": 4 -} diff --git a/spring-boot-admin-docs/src/site/docs/customize/index.md b/spring-boot-admin-docs/src/site/docs/customize/index.md deleted file mode 100644 index c87557136f7..00000000000 --- a/spring-boot-admin-docs/src/site/docs/customize/index.md +++ /dev/null @@ -1,16 +0,0 @@ -import DocCardList from '@theme/DocCardList'; - -# Customizing - -Spring Boot Admin provides flexibility for customizing the appearance and branding of the Admin Server’s user interface. This allows you to tailor the UI to match your organization's branding, improve usability, or add custom functionality. - -You can easily modify key elements like logos, themes, and even extend the UI with custom views or components. The customization options include: - - * **Custom Logos and Branding**: Replace the default Spring Boot Admin logo with your own branding assets. - * **Theming**: Customize colors, fonts, and layout styles using custom CSS or by modifying the theme properties. - * **Custom Views**: Add new pages or extend existing ones by integrating custom JavaScript and HTML components. - * **Localization**: Support multiple languages by adding custom translations for the UI. - -By leveraging these customization options, you can create a tailored experience that aligns with your project or organization's needs, while still retaining the powerful monitoring and management capabilities of Spring Boot Admin. - - diff --git a/spring-boot-admin-docs/src/site/docs/index.mdx b/spring-boot-admin-docs/src/site/docs/index.mdx new file mode 100644 index 00000000000..cc87aece006 --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/index.mdx @@ -0,0 +1,36 @@ +--- +sidebar_position: 0 +slug: /index +id: index +title: Start +hide_title: true +hide_table_of_contents: true +sidebar_custom_props: + icon: 'home' +--- + +import { HexMesh } from "@sba/spring-boot-admin-docs/src/site/src/components/HexMesh"; +import Link from "@docusaurus/Link"; + +
+ ( + +
{item.title}
+
{item.description}
+ + )} + /> +
+ diff --git a/spring-boot-admin-docs/src/site/docs/index.module.css b/spring-boot-admin-docs/src/site/docs/index.module.css new file mode 100644 index 00000000000..a6466c5857c --- /dev/null +++ b/spring-boot-admin-docs/src/site/docs/index.module.css @@ -0,0 +1,38 @@ +.hexMeshContainer { + height: 600px; + margin: 2rem 0; +} + +.hexItem { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 1rem; + color: var(--ifm-font-color-base); + text-decoration: none; + height: 100%; + width: 100%; +} + +.hexItem:hover { + color: var(--ifm-color-primary); + text-decoration: none; +} + +.hexIcon { + font-size: 2.5rem; + margin-bottom: 0.5rem; +} + +.hexTitle { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.25rem; +} + +.hexDescription { + font-size: 0.85rem; + opacity: 0.8; +} diff --git a/spring-boot-admin-docs/src/site/docs/installation-and-setup/index.md b/spring-boot-admin-docs/src/site/docs/installation-and-setup/index.md deleted file mode 100644 index a4bb77de72a..00000000000 --- a/spring-boot-admin-docs/src/site/docs/installation-and-setup/index.md +++ /dev/null @@ -1,227 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Installation and Setup - -## Overview - -Spring Boot Admin works by registering Spring Boot applications that expose Actuator endpoints. Each application's -health and metrics data is polled by Spring Boot Admin Server, which aggregates and displays this information in a web -dashboard. The registered applications can either self-register or be discovered using service discovery tools like -Eureka or Consul. Through the dashboard, users can monitor the health, memory usage, logs, and more for each -application, and even interact with them via management endpoints for tasks like restarting or updating configurations. - -## Motivation - -In modern microservices architecture, monitoring and managing distributed systems is complex and challenging. Spring -Boot Admin provides a powerful solution for visualizing, monitoring, and managing Spring Boot applications in real-time. -By offering a web interface that aggregates the health and metrics of all attached services, Spring Boot Admin -simplifies the process of ensuring system stability and performance. Whether you need insights into application health, -memory usage, or log output, Spring Boot Admin offers a centralized tool that streamlines operational management, -helping developers and DevOps teams maintain robust and efficient applications. - -While Spring Boot Admin offers a user-friendly and centralized interface for monitoring Spring Boot applications, it is -not designed to replace sophisticated, full-scale monitoring and observability tools like Grafana, Datadog, or Instana. -These tools provide advanced capabilities such as real-time alerting, history data, complex metric analysis, distributed -tracing, and customizable dashboards across diverse environments. - -Spring Boot Admin excels at providing a lightweight, application-centric view with essential health checks, metrics, and -management endpoints. For production-grade observability in larger, more complex systems, integrating Spring Boot Admin -alongside these advanced platforms ensures comprehensive system monitoring and deep insights. - -## Quick Start - -Since Spring Boot Admin is built on top of Spring Boot, you'll need to set up a Spring Boot application first. We -recommend using [http://start.spring.io](http://start.spring.io) for easy project setup. The Spring Boot Admin Server -can run as either in a Servlet or WebFlux application, so you'll need to choose one and add the corresponding Spring -Boot Starter. In this example, we'll use the Servlet Web Starter. - -### Setting up the Spring Boot Admin Server - -To set up Spring Boot Admin Server, you need to add the dependency `spring-boot-admin-starter-server` as well as -`spring-boot-starter-web` to your project (either in your `pom.xml` or `build.gradle(.kts)`). - -```xml title="pom.xml" - - - - de.codecentric - spring-boot-admin-starter-server - @VERSION@ - - - org.springframework.boot - spring-boot-starter-web - - -``` - -After that, you need to annotate your main class with `@EnableAdminServer` to enable the Spring Boot Admin Server. -This will load all required configuration at runtime by leveraging Springs' autodiscovery feature. - -```java title="SpringBootAdminApplication.java" - -@SpringBootApplication -@EnableAdminServer -public class SpringBootAdminApplication { - public static void main(String[] args) { - SpringApplication.run(SpringBootAdminApplication.class, args); - } -} -``` - -After starting your application, you can now access the Spring Boot Admin Server web interface at -`http://localhost:8080`. - -:::note -If you want to set up Spring Boot Admin Server via war-deployment in a servlet-container, please have a look at -the [spring-boot-admin-sample-war](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-war/). -::: - -:::note -See also -the [spring-boot-admin-sample-servlet](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-servlet/) -project, which adds security. -::: - -### Registering Applications - -To register your application at the server, you can either include the Spring Boot Admin Client or -use [Spring Cloud Discovery](https://spring.io/projects/spring-cloud) (e.g. Eureka, Consul, …​). There is also -an [option to use static configuration on server side](../server/01-server.mdx#static-configuration-using-simplediscoveryclient). - -#### Using Spring Boot Admin Client - -Each application that is not using Spring Cloud features but wants to register at the server has to include the Spring -Boot Admin Client as dependency. - -```xml title="pom.xml" - - - - de.codecentric - spring-boot-admin-starter-client - @VERSION@ - - - org.springframework.boot - spring-boot-starter-security - - -``` - -After adding the dependency, you need to configure the URL of the Spring Boot Admin Server in your -`application.properties` or `application.yml` file as follows: - -```properties title="application.properties" -spring.boot.admin.client.url=http://localhost:8080 #1 -management.endpoints.web.exposure.include=* #2 -management.info.env.enabled=true #3 -``` - -1. This property defines the URL of the Spring Boot Admin Server. -2. As with Spring Boot 2 most of the endpoints aren’t exposed via http by default, but we want to expose all of them. - For production, you should carefully choose which endpoints to expose and keep security in mind. It is also possible - to use a different port for the actuator endpoints by setting `management.port` property. -3. Since Spring Boot 2.6, the info actuator endpoint is disabled by default. In our case, we enable it to - provide additional information to the Spring Boot Admin Server. - -When you start your monitored application now, it will register itself at the Spring Boot Admin Server. You can see your -app in the web interface of Spring Boot Admin. - -:::info -It is possible to add `spring-boot-admin-client` as well as `spring-boot-admin-server` to the same application. This -allows you to monitor the Spring Boot Admin Server itself. To get a more realistic setup, you should run the Spring Boot -Admin Server and clients in separate applications. -::: - -#### Using Spring Cloud Discovery - -If you already use Spring Cloud Discovery in your application architecture you don’t need to add Spring Boot Admin -Client. In this case you can leverage the Spring Cloud features by adding a DiscoveryClient to Spring Boot Admin Server. - -The following steps uses Eureka, but other Spring Cloud Discovery implementations are supported as well. There are -examples -for [Consul](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-consul/) -and [Zookeeper](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/). - -Since Spring Boot Admin Server is fully build on top of Spring Cloud features and uses its discovery mechanism, please -refer to the [Spring Cloud documentation](http://projects.spring.io/spring-cloud) for more information. - -To start using Eureka, you need to add the following dependencies to your project: - -```xml title="pom.xml" - - - org.springframework.cloud - spring-cloud-starter-netflix-eureka-client - -``` - -After that, you have to enable discovery by adding `@EnableDiscoveryClient` to your configuration: - -```java title="SpringBootAdminApplication.java" - -@EnableDiscoveryClient -@SpringBootApplication -@EnableAdminServer -public class SpringBootAdminApplication { - public static void main(String[] args) { - SpringApplication.run(SpringBootAdminApplication.class, args); - } -} -``` - -The next step is to configure the Eureka client in your `application.yml` file and define the URL of Eureka's service -registry. - -```yml title="application.yml" -spring: - application: - name: spring-boot-admin-sample-eureka - profiles: - active: - - secure -eureka: - instance: - leaseRenewalIntervalInSeconds: 10 - health-check-url-path: /actuator/health - metadata-map: - startup: ${random.int} # needed to trigger info and endpoint update after restart - client: - registryFetchIntervalSeconds: 5 - serviceUrl: - defaultZone: http://localhost:8761/eureka/ -management: - endpoints: - web: - exposure: - include: "*" - endpoint: - health: - show-details: ALWAYS -``` - -:::info -There is also a [basic example](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-eureka/) in Spring Boot Admin's GitHub repository using Eureka. -::: - -:::tip -You can include the Spring Boot Admin Server to your Eureka server as well. Setup everything as described above and set -`spring.boot.admin.context-path` to something different from `/` so that the Spring Boot Admin Server UI won’t clash -with -Eureka’s one. -::: - -### Docker Images -Since Spring Boot Admin can be run in a vast variety of environments, we neither provide nor maintain any Docker images. -However, you can easily create your own Docker image by adding a Dockerfile to your project and add the configuration that fits your needs. -Even though we don't provide Docker images, we have a some example in our Docker Hub repository at [https://hub.docker.com/r/codecentric/spring-boot-admin](https://hub.docker.com/r/codecentric/spring-boot-admin). - -:::info -We do not offer any support for these images. -They are provided as-is and are not maintained by the Spring Boot Admin team. -Neither do we guarantee that they are up-to-date nor secure. -As stated in the preambles, we recommend to create your own Docker image. -::: diff --git a/spring-boot-admin-docs/src/site/docs/server/02-security.md b/spring-boot-admin-docs/src/site/docs/server/02-security.md deleted file mode 100644 index 993f880ae64..00000000000 --- a/spring-boot-admin-docs/src/site/docs/server/02-security.md +++ /dev/null @@ -1,205 +0,0 @@ -# Foster Security - -Since there are several approaches on solving authentication and authorization in distributed web applications Spring Boot Admin doesn’t ship a default one. By default `spring-boot-admin-server-ui` provides a login page and a logout button. - -A Spring Security configuration for your server could look like this: - -```java title="SecuritySecureConfig.java" -@Configuration(proxyBeanMethods = false) -public class SecuritySecureConfig { - - private final AdminServerProperties adminServer; - - private final SecurityProperties security; - - public SecuritySecureConfig(AdminServerProperties adminServer, SecurityProperties security) { - this.adminServer = adminServer; - this.security = security; - } - - @Bean - protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); - successHandler.setTargetUrlParameter("redirectTo"); - successHandler.setDefaultTargetUrl(this.adminServer.path("/")); - - http.authorizeHttpRequests((authorizeRequests) -> authorizeRequests // - .requestMatchers(new AntPathRequestMatcher(this.adminServer.path("/assets/**"))) - .permitAll() // (1) - .requestMatchers(new AntPathRequestMatcher(this.adminServer.path("/actuator/info"))) - .permitAll() - .requestMatchers(new AntPathRequestMatcher(adminServer.path("/actuator/health"))) - .permitAll() - .requestMatchers(new AntPathRequestMatcher(this.adminServer.path("/login"))) - .permitAll() - .dispatcherTypeMatchers(DispatcherType.ASYNC) - .permitAll() // https://github.com/spring-projects/spring-security/issues/11027 - .anyRequest() - .authenticated()) // (2) - .formLogin( - (formLogin) -> formLogin.loginPage(this.adminServer.path("/login")).successHandler(successHandler)) // (3) - .logout((logout) -> logout.logoutUrl(this.adminServer.path("/logout"))) - .httpBasic(Customizer.withDefaults()); // (4) - - http.addFilterAfter(new CustomCsrfFilter(), BasicAuthenticationFilter.class) // (5) - .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) - .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) - .ignoringRequestMatchers( - new AntPathRequestMatcher(this.adminServer.path("/instances"), POST.toString()), // (6) - new AntPathRequestMatcher(this.adminServer.path("/instances/*"), DELETE.toString()), // (6) - new AntPathRequestMatcher(this.adminServer.path("/actuator/**")) // (7) - )); - - http.rememberMe((rememberMe) -> rememberMe.key(UUID.randomUUID().toString()).tokenValiditySeconds(1209600)); - - return http.build(); - - } - - // Required to provide UserDetailsService for "remember functionality" - @Bean - public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) { - UserDetails user = User.withUsername("user").password(passwordEncoder.encode("password")).roles("USER").build(); - return new InMemoryUserDetailsManager(user); - } - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - -} -``` - -1. Grants public access to all static assets and the login page. -2. Every other request must be authenticated. -3. Configures login and logout. -4. Enables HTTP-Basic support. This is needed for the Spring Boot Admin Client to register. -5. Enables CSRF-Protection using Cookies -6. Disables CSRF-Protection for the endpoint the Spring Boot Admin Client uses to (de-)register. -7. Disables CSRF-Protection for the actuator endpoints. - -In case you use the Spring Boot Admin Client, it needs the credentials for accessing the server: - -```yaml title="application.yml" -spring.boot.admin.client: - username: sba-client - password: s3cret -``` - -For a complete sample look at [spring-boot-admin-sample-servlet](https://github.com/codecentric/spring-boot-admin/tree/master/spring-boot-admin-samples/spring-boot-admin-sample-servlet/). - -:::note -If you protect the /instances endpoint don’t forget to configure the username and password on your SBA-Client using spring.boot.admin.client.username and spring.boot.admin.client.password. -::: - -## Securing Client Actuator Endpoints - -When the actuator endpoints are secured using HTTP Basic authentication the SBA Server needs credentials to access them. You can submit the credentials in the metadata when registering the application. The `BasicAuthHttpHeaderProvider` then uses this metadata to add the `Authorization` header to access your application’s actuator endpoints. You can provide your own `HttpHeadersProvider` to alter the behaviour (e.g. add some decryption) or add extra headers. - -:::note -The SBA Server masks certain metadata in the HTTP interface to prevent leaking of sensitive information. -::: - -:::warning -You should configure HTTPS for your SBA Server or (service registry) when transferring credentials via the metadata. -::: - -:::warning -When using Spring Cloud Discovery, you must be aware that anybody who can query your service registry can obtain the credentials. -::: - -:::tip -When using this approach the SBA Server decides whether the user can access the registered applications. There are more complex solutions possible (using OAuth2) to let the clients decide if the user can access the endpoints. For that please have a look at the samples in [joshiste/spring-boot-admin-samples](https://github.com/joshiste/spring-boot-admin-samples). -::: - -### SBA Client - -```yaml title="application.yml" -spring.boot.admin.client: - url: http://localhost:8080 - instance: - metadata: - user.name: ${spring.security.user.name} - user.password: ${spring.security.user.password} -``` - -### SBA Server - -You can specify credentials via configuration properties in your admin server. - -:::tip -You can use this in conjunction with [spring-cloud-kubernetes](https://cloud.spring.io/spring-cloud-kubernetes/1.1.x/reference/html/#secrets-propertysource) to pull credentials from [secrets](https://kubernetes.io/docs/concepts/configuration/secret/). -::: - -To enable pulling credentials from properties the `spring.boot.admin.instance-auth.enabled` property must be `true` (default). - -:::note -If your clients provide credentials via metadata (i.e., via service annotations), that metadata will be used instead of the properties. -::: - -You can provide a default username and password by setting `spring.boot.admin.instance-auth.default-user-name` and `spring.boot.admin.instance-auth.default-user-password`. Optionally you can provide credentials for specific services (by name) using the `spring.boot.admin.instance-auth.service-map.*.user-name` pattern, replacing `*` with the service name. - -```yaml title="application.yml" -spring.boot.admin: - instance-auth: - enabled: true - default-user-name: "${some.user.name.from.secret}" - default-password: "${some.user.password.from.secret}" - service-map: - my-first-service-to-monitor: - user-name: "${some.user.name.from.secret}" - user-password: "${some.user.password.from.secret}" - my-second-service-to-monitor: - user-name: "${some.user.name.from.secret}" - user-password: "${some.user.password.from.secret}" -``` - -### Eureka -```yaml title="application.yml" -eureka: - instance: - metadata-map: - user.name: ${spring.security.user.name} - user.password: ${spring.security.user.password} -``` - -### Consul -```yaml title="application.yml" -spring.cloud.consul: - discovery: - metadata: - user-name: ${spring.security.user.name} - user-password: ${spring.security.user.password} -``` - -:::warning -Consul does not allow dots (".") in metadata keys, use dashes instead. -::: - -## CSRF on Actuator Endpoints - -Some of the actuator endpoints (e.g. `/loggers`) support POST requests. When using Spring Security you need to ignore the actuator endpoints for CSRF-Protection as the Spring Boot Admin Server currently lacks support. - -```java title="SecuritySecureConfig.java" -@Bean -protected SecurityFilterChain filterChain(HttpSecurity http) { - return http.csrf(c -> c.ignoringRequestMatchers("/actuator/**")).build(); -} -``` - -## Using Mutual TLS - -SBA Server can also use client certificates to authenticate when accessing the actuator endpoints. If a custom configured `ClientHttpConnector` bean is present, Spring Boot will automatically configure a `WebClient.Builder` using it, which will be used by Spring Boot Admin. - -```java title="CustomHttpClientConfig.java" -@Bean -public ClientHttpConnector customHttpClient() { - SslContextBuilder sslContext = SslContextBuilder.forClient(); - //Your sslContext customizations go here - HttpClient httpClient = HttpClient.create().secure( - ssl -> ssl.sslContext(sslContext) - ); - return new ReactorClientHttpConnector(httpClient); -} -``` diff --git a/spring-boot-admin-docs/src/site/docs/server/99-server-properties.mdx b/spring-boot-admin-docs/src/site/docs/server/99-server-properties.mdx deleted file mode 100644 index b104b425d4e..00000000000 --- a/spring-boot-admin-docs/src/site/docs/server/99-server-properties.mdx +++ /dev/null @@ -1,12 +0,0 @@ ---- -sidebar_custom_props: - icon: 'properties' ---- -# Properties - -import metadata from "../../../../../spring-boot-admin-server/target/classes/META-INF/spring-configuration-metadata.json"; -import { PropertyTable } from "../../src/components/PropertyTable"; - - diff --git a/spring-boot-admin-docs/src/site/docs/third-party/pyctuator.md b/spring-boot-admin-docs/src/site/docs/third-party/pyctuator.md deleted file mode 100644 index 3e170f41ba6..00000000000 --- a/spring-boot-admin-docs/src/site/docs/third-party/pyctuator.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -sidebar_custom_props: - icon: 'python' ---- - -# Pyctuator - -You can easily integrate Spring Boot Admin with [Flask](https://flask.palletsprojects.com) or [FastAPI](https://fastapi.tiangolo.com/) Python applications using the [Pyctuator](https://github.com/SolarEdgeTech/pyctuator) project. - -The following steps uses Flask, but other web frameworks are supported as well. See Pyctuator’s documentation for an updated list of supported frameworks and features. - -1. Install the pyctuator package: -```bash -pip install pyctuator -``` -2. Enable pyctuator by pointing it to your Flask app and letting it know where Spring Boot Admin is running: -```python title="app.py" -import os -from flask import Flask -from pyctuator.pyctuator import Pyctuator -app_name = "Flask App with Pyctuator" -app = Flask(app_name) -@app.route("/") -def hello(): - return "Hello World!" -Pyctuator( - app, - app_name, - app_url="http://example-app.com", - pyctuator_endpoint_url="http://example-app.com/pyctuator", - registration_url=os.getenv("SPRING_BOOT_ADMIN_URL") -) -app.run() -``` - -For further details and examples, see Pyctuator’s [documentation](https://github.com/SolarEdgeTech/pyctuator/blob/master/README.md) and [examples](https://github.com/SolarEdgeTech/pyctuator/tree/master/examples). diff --git a/spring-boot-admin-docs/src/site/docusaurus.config.ts b/spring-boot-admin-docs/src/site/docusaurus.config.ts index b7ff9161d49..864ca4e99c7 100644 --- a/spring-boot-admin-docs/src/site/docusaurus.config.ts +++ b/spring-boot-admin-docs/src/site/docusaurus.config.ts @@ -2,6 +2,7 @@ import 'dotenv/config'; import { themes as prismThemes } from "prism-react-renderer"; import type { Config } from "@docusaurus/types"; import type * as Preset from "@docusaurus/preset-classic"; +import path from "path"; const globalVariables = { VERSION: process.env.VERSION, @@ -15,7 +16,6 @@ const config: Config = { organizationName: 'codecentric', projectName: 'spring-boot-admin', onBrokenLinks: 'warn', - onBrokenMarkdownLinks: 'warn', onBrokenAnchors: 'warn', i18n: { defaultLocale: "en", @@ -44,7 +44,29 @@ const config: Config = { } satisfies Preset.Options ] ], + plugins: [ + '@signalwire/docusaurus-plugin-llms-txt', + function () { + return { + name: 'custom-webpack-config', + configureWebpack() { + return { + resolve: { + alias: { + '@sba': path.resolve(__dirname, '../../..') + } + } + }; + }, + }; + }, + ], markdown: { + hooks: { + onBrokenMarkdownLinks: "throw", + onBrokenMarkdownImages: "throw" + }, + mermaid: true, preprocessor: ({fileContent}) => { let content = fileContent; for (const variable in globalVariables) { @@ -54,6 +76,7 @@ const config: Config = { return content }, }, + themes: ['@docusaurus/theme-mermaid'], themeConfig: { image: "img/social-card.jpg", tableOfContents: { @@ -78,13 +101,13 @@ const config: Config = { items: [ { type: "docSidebar", - sidebarId: "tutorialSidebar", + sidebarId: "sidebar", position: "left", label: "Documentation" }, { type: "docSidebar", - sidebarId: "tutorialSidebar", + sidebarId: "sidebar", position: "left", label: "FAQ", href: "/faq" @@ -104,7 +127,7 @@ const config: Config = { items: [ { label: "Overview", - to: "/docs/index" + to: "/docs/getting-started/" }, { label: "FAQ", @@ -143,8 +166,8 @@ const config: Config = { }, prism: { theme: prismThemes.github, - darkTheme: prismThemes.dracula, - additionalLanguages: ["java"] + darkTheme: prismThemes.vsDark, + additionalLanguages: ["java", "bash", "javascript", "typescript", "docker", "gradle", "groovy", "yaml"] } } satisfies Preset.ThemeConfig }; diff --git a/spring-boot-admin-docs/src/site/package-lock.json b/spring-boot-admin-docs/src/site/package-lock.json index b5aec28105f..fe9330b790d 100644 --- a/spring-boot-admin-docs/src/site/package-lock.json +++ b/spring-boot-admin-docs/src/site/package-lock.json @@ -11,10 +11,12 @@ "@docusaurus/core": "^3.6.3", "@docusaurus/module-type-aliases": "^3.6.3", "@docusaurus/preset-classic": "^3.6.3", + "@docusaurus/theme-mermaid": "^3.9.2", "@docusaurus/tsconfig": "^3.6.3", "@docusaurus/types": "^3.6.3", "@iconify/react": "^6.0.0", "@mdx-js/react": "^3.1.0", + "@signalwire/docusaurus-plugin-llms-txt": "^1.2.2", "asciidoctor": "^3.0.4", "clsx": "^2.1.1", "dotenv": "^17.0.0", @@ -288,6 +290,19 @@ "node": ">=6.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@asciidoctor/cli": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@asciidoctor/cli/-/cli-4.0.0.tgz", @@ -2045,6 +2060,63 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0" + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -3776,6 +3848,34 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, + "node_modules/@docusaurus/theme-mermaid": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.9.2.tgz", + "integrity": "sha512-5vhShRDq/ntLzdInsQkTdoKWSzw8d1jB17sNPYhA/KvYYFXfuVEGHLM6nrf8MFbV8TruAHDG21Fn3W4lO8GaDw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.9.2", + "@docusaurus/module-type-aliases": "3.9.2", + "@docusaurus/theme-common": "3.9.2", + "@docusaurus/types": "3.9.2", + "@docusaurus/utils-validation": "3.9.2", + "mermaid": ">=11.6.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@mermaid-js/layout-elk": "^0.1.9", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@mermaid-js/layout-elk": { + "optional": true + } + } + }, "node_modules/@docusaurus/theme-search-algolia": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.9.2.tgz", @@ -3961,6 +4061,17 @@ "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==" }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -4234,6 +4345,15 @@ "react": ">=16" } }, + "node_modules/@mermaid-js/parser": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz", + "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "license": "MIT", + "dependencies": { + "langium": "3.3.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4329,6 +4449,44 @@ "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "license": "BSD-3-Clause" }, + "node_modules/@signalwire/docusaurus-plugin-llms-txt": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@signalwire/docusaurus-plugin-llms-txt/-/docusaurus-plugin-llms-txt-1.2.2.tgz", + "integrity": "sha512-Qo5ZBDZpyXFlcrWwise77vs6B0R3m3/qjIIm1IHf4VzW6sVYgaR/y46BJwoAxMrV3WJkn0CVGpyYC+pMASFBZw==", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.0.0", + "hast-util-select": "^6.0.4", + "hast-util-to-html": "^9.0.5", + "hast-util-to-string": "^3.0.1", + "p-map": "^7.0.2", + "rehype-parse": "^9", + "rehype-remark": "^10", + "remark-gfm": "^4", + "remark-stringify": "^11", + "string-width": "^5.0.0", + "unified": "^11", + "unist-util-visit": "^5" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@docusaurus/core": "^3.0.0" + } + }, + "node_modules/@signalwire/docusaurus-plugin-llms-txt/node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -4678,6 +4836,259 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -4741,6 +5152,12 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/gtag.js": { "version": "0.0.12", "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", @@ -4962,6 +5379,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -5192,9 +5616,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -5631,6 +6055,16 @@ "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -5899,9 +6333,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001721", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", - "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", "funding": [ { "type": "opencollective", @@ -6037,6 +6471,38 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/chevrotain/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6347,6 +6813,12 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -6544,6 +7016,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -6837,6 +7318,22 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-selector-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", + "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", @@ -7024,30 +7521,556 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" + "cose-base": "^1.0.0" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "peerDependencies": { + "cytoscape": "^3.2.0" } }, - "node_modules/decode-named-character-reference": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", + "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", "dependencies": { "character-entities": "^2.0.0" @@ -7175,6 +8198,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -7255,6 +8287,19 @@ "node": ">=8" } }, + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -7318,6 +8363,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", @@ -8560,6 +9614,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -8670,6 +9730,38 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-from-parse5": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.2.tgz", @@ -8690,6 +9782,62 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-body-ok-link": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", + "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-minify-whitespace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz", + "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-parse-selector": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", @@ -8703,6 +9851,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-raw": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", @@ -8719,8 +9884,35 @@ "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0", - "web-namespaces": "^2.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "nth-check": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" }, "funding": { @@ -8728,6 +9920,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-select/node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hast-util-to-estree": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.0.tgz", @@ -8771,6 +9973,39 @@ "inline-style-parser": "0.1.1" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html/node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz", @@ -8798,6 +10033,32 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-mdast": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-10.1.2.tgz", + "integrity": "sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "hast-util-to-text": "^4.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-minify-whitespace": "^6.0.0", + "trim-trailing-lines": "^2.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", @@ -8817,6 +10078,35 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", @@ -9289,6 +10579,15 @@ "node": ">= 0.10" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -9829,6 +11128,31 @@ "promise": "^7.0.1" } }, + "node_modules/katex": { + "version": "0.16.28", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.28.tgz", + "integrity": "sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9837,6 +11161,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -9853,6 +11182,22 @@ "node": ">=6" } }, + "node_modules/langium": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", + "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/latest-version": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", @@ -9876,6 +11221,12 @@ "shell-quote": "^1.8.1" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -9943,6 +11294,12 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -10036,6 +11393,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/mdast-util-directive": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz", @@ -10492,6 +11861,47 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.12.2", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz", + "integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.1", + "@mermaid-js/parser": "^0.6.3", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.13", + "dayjs": "^1.11.18", + "dompurify": "^3.2.5", + "katex": "^0.16.22", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^16.2.1", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -12397,6 +13807,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -15455,6 +16877,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "license": "MIT" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -15566,6 +16994,12 @@ "tslib": "^2.0.3" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", @@ -15636,6 +17070,12 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -15668,6 +17108,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.4", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", @@ -17801,6 +19268,35 @@ "regjsparser": "bin/parser" } }, + "node_modules/rehype-minify-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-6.0.2.tgz", + "integrity": "sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rehype-raw": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", @@ -17831,6 +19327,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-remark": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-10.0.1.tgz", + "integrity": "sha512-EmDndlb5NVwXGfUa4c9GPK+lXeItTilLhE6ADSaQuHr4JUlKw9MidzGzx4HpqZrNCt6vnHmEifXQiiA+CEnjYQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "hast-util-to-mdast": "^10.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -18675,6 +20188,24 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/rtlcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", @@ -18727,6 +20258,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -19467,6 +21004,12 @@ "postcss": "^8.4.31" } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -19695,6 +21238,15 @@ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinypool": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", @@ -19764,6 +21316,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/trim-trailing-lines": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-2.1.0.tgz", + "integrity": "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -19774,6 +21336,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -19845,6 +21416,12 @@ "node": ">=14.17" } }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -19944,6 +21521,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -20387,6 +21978,55 @@ "node": ">=0.10.0" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", diff --git a/spring-boot-admin-docs/src/site/package.json b/spring-boot-admin-docs/src/site/package.json index 040fbdfde94..25c7a4fd34d 100644 --- a/spring-boot-admin-docs/src/site/package.json +++ b/spring-boot-admin-docs/src/site/package.json @@ -5,6 +5,7 @@ "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start", + "swizzle": "docusaurus swizzle", "build:current-version-redirect": "mkdir -p build/current/ && sed \"s/@@VERSION@@/$VERSION/g\" current/index.template.html > build/current/index.html && sed \"s/@@VERSION@@/$VERSION/g\" current/404.template.html > build/current/404.html", "build": "docusaurus build", "build:prod": "npm run build && npm run build:current-version-redirect && rsync -a --delete-before ./build ../../target/generated-docs" @@ -13,10 +14,12 @@ "@docusaurus/core": "^3.6.3", "@docusaurus/module-type-aliases": "^3.6.3", "@docusaurus/preset-classic": "^3.6.3", + "@docusaurus/theme-mermaid": "^3.9.2", "@docusaurus/tsconfig": "^3.6.3", "@docusaurus/types": "^3.6.3", "@iconify/react": "^6.0.0", "@mdx-js/react": "^3.1.0", + "@signalwire/docusaurus-plugin-llms-txt": "^1.2.2", "asciidoctor": "^3.0.4", "clsx": "^2.1.1", "dotenv": "^17.0.0", diff --git a/spring-boot-admin-docs/src/site/sidebars.ts b/spring-boot-admin-docs/src/site/sidebars.ts index 1496b49bf6a..a471dfbdfc8 100644 --- a/spring-boot-admin-docs/src/site/sidebars.ts +++ b/spring-boot-admin-docs/src/site/sidebars.ts @@ -1,7 +1,9 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; const sidebars: SidebarsConfig = { - tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], + sidebar: [ + {type: 'autogenerated', dirName: '.'} + ], }; export default sidebars; diff --git a/spring-boot-admin-docs/src/site/src/components/HexMesh.module.css b/spring-boot-admin-docs/src/site/src/components/HexMesh.module.css new file mode 100644 index 00000000000..a56b78f8a43 --- /dev/null +++ b/spring-boot-admin-docs/src/site/src/components/HexMesh.module.css @@ -0,0 +1,81 @@ +.hexMesh { + width: 100%; + height: 75vh; + display: flex; + justify-content: space-around; + align-items: center; +} + +.hex { + --fill-color: #4a4a4a; + --stroke-color: transparent; + + fill: var(--fill-color); + fill-opacity: 0.05; + stroke: var(--stroke-color); + stroke-width: 0.5; + stroke-opacity: 0.8; + pointer-events: none; + + cursor: pointer; + + &.hasContent { + cursor: pointer; + --fill-color: var(--ifm-color-primary); + --stroke-color: var(--ifm-color-primary); + + &:hover { + fill-opacity: 0.25; + stroke-opacity: 1; + stroke-width: 2; + } + } + + :hover path { + fill-opacity: 0.25; + stroke-opacity: 1; + stroke-width: 2; + } + + :global(.hex__body) { + pointer-events: auto; + position: fixed; + z-index: 10; + font-size: 100%; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: var(--ifm-font-color-base); + + &:hover { + text-decoration: none; + } + } + + :global(.hex__body::after) { + display: flex; + justify-content: center; + align-content: center; + font-size: 15em; + position: absolute; + z-index: -1; + width: 100%; + } + + :global(.hex__body__title) { + width: 85%; + font-size: 1.75em; + font-weight: bold; + text-align: center; + line-height: 1em; + } + + :global(.hex__body__description) { + font-style: italic; + font-size: 1em; + } +} + diff --git a/spring-boot-admin-docs/src/site/src/components/HexMesh.tsx b/spring-boot-admin-docs/src/site/src/components/HexMesh.tsx new file mode 100644 index 00000000000..72331541582 --- /dev/null +++ b/spring-boot-admin-docs/src/site/src/components/HexMesh.tsx @@ -0,0 +1,255 @@ +/* + * Copyright 2014-2018 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect, useRef, useState, useMemo } from 'react'; +import styles from './HexMesh.module.css'; + +interface HexMeshProps { + items: T[]; + classForItem?: (item: T | undefined) => string | undefined; + renderItem?: (item: T) => React.ReactNode; + onClick?: (item: T, event: React.MouseEvent) => void; +} + +interface Layout { + cols: number; + rows: number; + sideLength: number; +} + +const tileCount = (cols: number, rows: number): number => { + const shorterRows = Math.floor(rows / 2); + return rows * cols - shorterRows; +}; + +const calcSideLength = (width: number, height: number, cols: number, rows: number): number => { + const fitToWidth = width / cols / Math.sqrt(3); + const fitToHeight = (height * 2) / (3 * rows + 1); + return Math.min(fitToWidth, fitToHeight); +}; + +const calcLayout = (minTileCount: number, width: number, height: number): Layout => { + let cols = 1; + let rows = 1; + let sideLength = calcSideLength(width, height, cols, rows); + + while (minTileCount > tileCount(cols, rows)) { + const sidelengthExtraCol = calcSideLength(width, height, cols + 1, rows); + const sidelengthExtraRow = calcSideLength(width, height, cols, rows + 1); + if (sidelengthExtraCol > sidelengthExtraRow) { + sideLength = sidelengthExtraCol; + cols++; + } else { + sideLength = sidelengthExtraRow; + rows++; + } + } + return { + cols, + rows, + sideLength, + }; +}; + +export function HexMesh({ items, classForItem, renderItem, onClick }: HexMeshProps) { + const rootRef = useRef(null); + const [layout, setLayout] = useState({ cols: 1, rows: 1, sideLength: 1 }); + + const { cols, rows, sideLength } = layout; + + const hexHeight = useMemo(() => sideLength * 2, [sideLength]); + const hexWidth = useMemo(() => sideLength * Math.sqrt(3), [sideLength]); + const meshWidth = useMemo(() => hexWidth * cols, [hexWidth, cols]); + const meshHeight = useMemo(() => sideLength * (2 + (rows - 1) * 1.5), [sideLength, rows]); + + const point = (i: number): string => { + const innerSideLength = sideLength * 0.95; + const marginTop = hexHeight / 2; + const marginLeft = hexWidth / 2; + const x = marginLeft + innerSideLength * Math.cos(((1 + i * 2) * Math.PI) / 6); + const y = marginTop + innerSideLength * Math.sin(((1 + i * 2) * Math.PI) / 6); + return `${x},${y}`; + }; + + const hexPath = useMemo(() => { + const points = [point(0), point(1), point(2), point(3), point(4), point(5)]; + + // Radius for the rounded corners + const cornerRadius = sideLength * 0.05; + + // Parse points into coordinate pairs + const coords = points.map((p) => { + const [x, y] = p.split(',').map(Number); + return { x, y }; + }); + + // Helper function to calculate distance between two points + const distance = (p1: { x: number; y: number }, p2: { x: number; y: number }) => + Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2)); + + // Helper function to move along a line from p1 towards p2 by a given distance + const moveAlong = (p1: { x: number; y: number }, p2: { x: number; y: number }, dist: number) => { + const d = distance(p1, p2); + const ratio = dist / d; + return { + x: p1.x + (p2.x - p1.x) * ratio, + y: p1.y + (p2.y - p1.y) * ratio, + }; + }; + + // Build the path + let path = ''; + + for (let i = 0; i < coords.length; i++) { + const current = coords[i]; + const prev = coords[(i - 1 + coords.length) % coords.length]; + const next = coords[(i + 1) % coords.length]; + + // Point after current corner (moving from current towards next) + const nextEdgeLength = distance(current, next); + const afterCorner = moveAlong(current, next, Math.min(cornerRadius, nextEdgeLength / 2)); + + // Point before current corner (moving from current towards prev) + const prevEdgeLength = distance(current, prev); + const beforeCorner = moveAlong(current, prev, Math.min(cornerRadius, prevEdgeLength / 2)); + + if (i === 0) { + // Start at the point after the first corner + path += `M ${afterCorner.x},${afterCorner.y} `; + } else { + // Draw line to the point before this corner + path += `L ${beforeCorner.x},${beforeCorner.y} `; + // Draw quadratic bezier curve around this corner using the corner as control point + path += `Q ${current.x},${current.y} ${afterCorner.x},${afterCorner.y} `; + } + } + + // Close the path (draws line back to start and bezier around first corner) + const firstCorner = coords[0]; + const lastCorner = coords[coords.length - 1]; + const firstEdgeLength = distance(firstCorner, lastCorner); + const beforeFirstCorner = moveAlong(firstCorner, lastCorner, Math.min(cornerRadius, firstEdgeLength / 2)); + path += `L ${beforeFirstCorner.x},${beforeFirstCorner.y} `; + + const afterFirstCorner = moveAlong( + firstCorner, + coords[1], + Math.min(cornerRadius, distance(firstCorner, coords[1]) / 2) + ); + path += `Q ${firstCorner.x},${firstCorner.y} ${afterFirstCorner.x},${afterFirstCorner.y} `; + + path += 'Z'; + return path; + }, [sideLength, hexHeight, hexWidth]); + + const translate = (col: number, row: number): string => { + const x = (col - 1) * hexWidth + (row % 2 ? 0 : hexWidth / 2); + const y = (row - 1) * sideLength * 1.5; + return `translate(${x},${y})`; + }; + + const getItem = (col: number, row: number): T | undefined => { + const rowOffset = (row - 1) * cols - Math.max(Math.floor((row - 1) / 2), 0); + const index = rowOffset + col - 1; + return items[index]; + }; + + const handleClick = (event: React.MouseEvent, col: number, row: number) => { + const item = getItem(col, row); + if (item && onClick) { + onClick(item, event); + } + }; + + const updateLayout = () => { + if (rootRef.current) { + const boundingClientRect = rootRef.current.getBoundingClientRect(); + const newLayout = calcLayout(items.length, boundingClientRect.width, boundingClientRect.height); + setLayout(newLayout); + } + }; + + useEffect(() => { + updateLayout(); + }, [items.length]); + + useEffect(() => { + if (rootRef.current) { + rootRef.current.style.fontSize = `${sideLength / 9.5}px`; + } + }, [sideLength]); + + useEffect(() => { + const resizeObserver = new ResizeObserver(() => { + updateLayout(); + }); + + if (rootRef.current) { + resizeObserver.observe(rootRef.current); + } + + return () => { + resizeObserver.disconnect(); + }; + }, [items.length]); + + const hexes = useMemo(() => { + const result: React.ReactNode[] = []; + for (let row = 1; row <= rows; row++) { + const colCount = cols + (row % 2 ? 0 : -1); + for (let col = 1; col <= colCount; col++) { + const item = getItem(col, row); + const className = classForItem ? classForItem(item) : undefined; + + result.push( + handleClick(e, col, row)} + > + + {item && renderItem && ( + + {renderItem(item)} + + )} + + ); + } + } + return result; + }, [rows, cols, hexPath, hexHeight, hexWidth, items, classForItem, renderItem]); + + return ( +
+ + + + + + + {hexes} + +
+ ); +} diff --git a/spring-boot-admin-docs/src/site/src/components/PropertyTable.module.css b/spring-boot-admin-docs/src/site/src/components/PropertyTable.module.css index f63cd1f1a95..8e0ce1083c5 100644 --- a/spring-boot-admin-docs/src/site/src/components/PropertyTable.module.css +++ b/spring-boot-admin-docs/src/site/src/components/PropertyTable.module.css @@ -1,20 +1,51 @@ .propertyTable { + display: table; + width: 100%; + caption { text-align: left; font-weight: bold; padding: 0 0 1rem 0; } + + tr { + --description-background: var(--ifm-table-stripe-background); + } + tr:nth-child(2n) { + --description-background: var(--ifm-background-color); + } + + code { + word-break: break-all; + } } -.propertyCell { - div { - display: inline-flex; - gap: .5rem; +.propertyBlock { + display: flex; + align-items: center; + gap: .5rem; + margin-bottom: .5rem; + + code { + border: none; + background: none; + text-wrap: nowrap; + } +} + +.descriptionBlock { + background: var(--description-background); + border-radius: 5px; + border: var(--ifm-table-border-width) solid var(--ifm-table-border-color); + padding: .5rem; + overflow: hidden; + + p { + margin-bottom: .25rem; + } - code { - border: none; - background: none; - text-wrap: wrap; - } + dl { + margin-top: 0; + margin-bottom: 0; } } diff --git a/spring-boot-admin-docs/src/site/src/components/PropertyTable.tsx b/spring-boot-admin-docs/src/site/src/components/PropertyTable.tsx index 1a32f768883..8ff9f9555cd 100644 --- a/spring-boot-admin-docs/src/site/src/components/PropertyTable.tsx +++ b/spring-boot-admin-docs/src/site/src/components/PropertyTable.tsx @@ -4,26 +4,36 @@ import { CopyButton } from "@site/src/components/CopyButton"; type Props = { title?: string; - properties: Array - filter?: Array - exclusive?: boolean, - additionalProperties: Array + properties: Array; + filter?: Array; + includeOnly?: boolean; + additionalProperties?: Array; } -export function PropertyTable({ - title, - properties, - filter = [], - exclusive = true, - additionalProperties = [] as Array, - }: Readonly) { - const filteredProperties = filterPropertiesByName(properties, filter, exclusive) +function getFilteredProperties(properties: Array, filter: Array, includeOnly: boolean) { + if (filter.length === 0) { + return properties; + } + + return filterPropertiesByName(properties, filter, includeOnly) .filter((property, index, self) => index === self.findIndex((p) => p.name === property.name) ) .sort((a, b) => { return a.name.length - b.name.length || a.name.localeCompare(b.name); }); +} + +export function PropertyTable({ + title, + properties, + filter = [], + includeOnly = true, + additionalProperties = [] as Array + }: Readonly) { + + + const filteredProperties = getFilteredProperties(properties, filter, includeOnly); const propertiesToShow = [ ...filteredProperties, @@ -31,10 +41,8 @@ export function PropertyTable({ ]; const hasDefaultValueOrType = (property: SpringPropertyDefinition) => { - console.log(property.defaultValue, typeof property.defaultValue); - console.log(property.type, typeof property.type); - return property.defaultValue || property.type - } + return property.defaultValue || property.type; + }; return ( @@ -42,7 +50,6 @@ export function PropertyTable({ - @@ -50,31 +57,31 @@ export function PropertyTable({ <> - diff --git a/spring-boot-admin-docs/src/site/src/css/custom.css b/spring-boot-admin-docs/src/site/src/css/custom.css index 6c904def862..05c54804659 100644 --- a/spring-boot-admin-docs/src/site/src/css/custom.css +++ b/spring-boot-admin-docs/src/site/src/css/custom.css @@ -6,6 +6,7 @@ --ifm-color-primary-light: #1a7156; --ifm-color-primary-lighter: #1c765a; --ifm-color-primary-lightest: #1f8665; + --ifm-background-color: #fff; } [data-theme='dark'] { @@ -22,6 +23,10 @@ main[class^='docMainContainer'] { position: relative; } +main > .container { + max-width: initial !important; +} + .dl-horizontal { * { margin: 0; diff --git a/spring-boot-admin-docs/src/site/src/pages/faq.md b/spring-boot-admin-docs/src/site/src/pages/faq.md index f03ba7323f3..ea9873396e8 100644 --- a/spring-boot-admin-docs/src/site/src/pages/faq.md +++ b/spring-boot-admin-docs/src/site/src/pages/faq.md @@ -1,10 +1,583 @@ +--- +toc_min_heading_level: 2 +toc_max_heading_level: 2 +--- # FAQ -## Can I include spring-boot-admin into my business application? -**tl;dr** You can, but you shouldn't. + +This FAQ covers common questions and troubleshooting scenarios encountered when using Spring Boot Admin. + +--- + +## General Questions + +### Can I include spring-boot-admin into my business application? + +**tl;dr** You can, but you shouldn't. + You can set `spring.boot.admin.context-path` to alter the path where the UI and REST-API is served, but depending on the complexity of your application you might get in trouble. On the other hand in my opinion it makes no sense for an application to monitor itself. In case your application goes down your monitoring tool also does. -## Can I change or reload Spring Boot properties at runtime? +### Can I change or reload Spring Boot properties at runtime? + Yes, you can refresh the entire environment or set/update individual properties for both single instances as well as for the entire application. + Note, however, that the Spring Boot application needs to have [Spring Cloud Commons](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#endpoints) and `management.endpoint.env.post.enabled=true` in place. + Also check the details of `@RefreshScope` https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#refresh-scope. + +### Which Spring Boot Admin version should I use? + +Spring Boot Admin's version matches the major and minor versions of Spring Boot: + +- **Spring Boot Admin 2.x** → **Spring Boot 2.x** +- **Spring Boot Admin 3.x** → **Spring Boot 3.x** +- **Spring Boot Admin 4.x** → **Spring Boot 4.x** + +Always match the major and minor version numbers. For example, if you're using Spring Boot 3.2.x, use Spring Boot Admin 3.2.x. + +--- + +## Client Registration Issues + +### My client application is not registering with the Admin Server + +> **Related Issues:** [#918](https://github.com/codecentric/spring-boot-admin/issues/918), [#2039](https://github.com/codecentric/spring-boot-admin/issues/2039), [#797](https://github.com/codecentric/spring-boot-admin/issues/797) +> **Stack Overflow:** [spring-boot-admin](https://stackoverflow.com/questions/tagged/spring-boot-admin+registration) + +**Common causes:** + +1. **Incorrect Admin Server URL** + +Verify your client's `application.properties`: + +```properties +spring.boot.admin.client.url=http://localhost:8080 +``` + +Make sure the URL points to the running Admin Server. + +2. **Missing dependency** + +Ensure you have the client starter in your `pom.xml`: + +```xml + + de.codecentric + spring-boot-admin-starter-client + ${spring-boot-admin.version} + +``` + +3. **Network connectivity** + +Test if the client can reach the admin server: + +```bash +curl http://localhost:8080/actuator/health +``` + +### I get "401 Unauthorized" errors during registration + +> **Related Issues:** [#803](https://github.com/codecentric/spring-boot-admin/issues/803), [#1190](https://github.com/codecentric/spring-boot-admin/issues/1190), [#470](https://github.com/codecentric/spring-boot-admin/issues/470) +> **Stack Overflow:** [spring-boot-admin+security](https://stackoverflow.com/questions/tagged/spring-boot-admin+spring-security) + +This occurs when the Admin Server has security enabled but the client doesn't provide credentials. + +**Solution:** Add credentials to your client configuration: + +```properties +spring.boot.admin.client.username=admin +spring.boot.admin.client.password=secret +``` + +### Registration works but client shows as "OFFLINE" immediately + +> **Related Issues:** [#319](https://github.com/codecentric/spring-boot-admin/issues/319), [#136](https://github.com/codecentric/spring-boot-admin/issues/136) +> **Stack Overflow:** [spring-boot-actuator](https://stackoverflow.com/questions/tagged/spring-boot-actuator+spring-boot-admin) + +This typically happens when: + +1. **Health endpoint is not accessible** + +Ensure the health endpoint is exposed: + +```properties +management.endpoints.web.exposure.include=health,info +``` + +2. **Client has security but Admin Server can't access it** + +Provide credentials via metadata: + +```properties +spring.boot.admin.client.instance.metadata.user.name=actuator-user +spring.boot.admin.client.instance.metadata.user.password=actuator-password +``` + +### Client registration works in local development but fails in Docker/Kubernetes + +> **Related Issues:** [#1537](https://github.com/codecentric/spring-boot-admin/issues/1537), [#1665](https://github.com/codecentric/spring-boot-admin/issues/1665) +> **Stack Overflow:** [spring-boot+docker](https://stackoverflow.com/questions/tagged/spring-boot+docker), [spring-boot+kubernetes](https://stackoverflow.com/questions/tagged/spring-boot+kubernetes) + +This is often due to hostname resolution issues. + +**Solution:** Use IP addresses instead of hostnames: + +```properties +spring.boot.admin.client.instance.service-host-type=IP +``` + +Or specify the service URL explicitly: + +```properties +spring.boot.admin.client.instance.service-base-url=http://my-service:8080 +``` + +--- + +## Actuator Endpoints + +### Only "Health" and "Info" endpoints are visible in the UI + +> **Related Issues:** [#1102](https://github.com/codecentric/spring-boot-admin/issues/1102) +> **Stack Overflow:** [spring-boot-actuator+endpoints](https://stackoverflow.com/questions/tagged/spring-boot-actuator+endpoints) + +Starting with Spring Boot 2.x, most actuator endpoints are not exposed by default. + +**Solution:** Expose all endpoints in your client's `application.properties`: + +```properties +management.endpoints.web.exposure.include=* +``` + +For production, be more selective: + +```properties +management.endpoints.web.exposure.include=health,info,metrics,env,loggers +``` + +### How do I verify endpoints are accessible? + +Visit the actuator discovery endpoint directly on your client application: + +``` +http://localhost:8080/actuator +``` + +You should see a JSON response with links to all available endpoints. + +### Endpoints work locally but not through Spring Boot Admin + +Check if security is blocking the Admin Server from accessing client endpoints: + +1. **Verify the Admin Server can access endpoints directly:** + +```bash +curl -u user:password http://client-host:8080/actuator/metrics +``` + +2. **Configure instance authentication:** + +```properties +# Client application +spring.boot.admin.client.instance.metadata.user.name=actuator +spring.boot.admin.client.instance.metadata.user.password=secret +``` + +--- + +## Service Discovery (Eureka, Consul, Kubernetes) + +### Applications registered in Eureka don't appear in Spring Boot Admin + +> **Related Issues:** [#1327](https://github.com/codecentric/spring-boot-admin/issues/1327), [#152](https://github.com/codecentric/spring-boot-admin/issues/152) +> **Stack Overflow:** [spring-cloud-eureka](https://stackoverflow.com/questions/tagged/spring-cloud-eureka+spring-boot-admin) + +**Solution:** Enable registry fetching in your Admin Server: + +```properties +eureka.client.fetch-registry=true +eureka.client.registry-fetch-interval-seconds=5 +``` + +Also ensure your Admin Server has `@EnableDiscoveryClient`: + +```java +@SpringBootApplication +@EnableAdminServer +@EnableDiscoveryClient +public class AdminServerApplication { + static void main(String[] args) { + SpringApplication.run(AdminServerApplication.class, args); + } +} +``` + +### Service discovery takes too long (1.5+ minutes) + +> **Related Issues:** [#1327](https://github.com/codecentric/spring-boot-admin/issues/1327) + +This is due to default registry fetch intervals. + +**Solution:** Speed up discovery: + +```properties +eureka.client.registry-fetch-interval-seconds=5 +eureka.instance.lease-renewal-interval-in-seconds=10 +``` + +### Services disappear from Admin Server when they go DOWN + +> **Related Issues:** [#1472](https://github.com/codecentric/spring-boot-admin/issues/1472) +> **Stack Overflow:** [spring-cloud-discovery](https://stackoverflow.com/questions/tagged/spring-cloud+service-discovery) + +This is a known issue with Eureka's `DiscoveryClient` implementation - it filters out non-UP services. + +**Workaround:** Use client registration instead of service discovery for critical monitoring, or implement a custom `ServiceInstanceConverter`. + +### Multiple instances of the same application only show one in Admin Server + +> **Related Issues:** [#856](https://github.com/codecentric/spring-boot-admin/issues/856), [#552](https://github.com/codecentric/spring-boot-admin/issues/552) +> **Stack Overflow:** [spring-cloud+multiple-instances](https://stackoverflow.com/questions/tagged/spring-cloud) + +This can happen with certain cloud platforms (PCF, Kubernetes) when instances share the same hostname. + +**Solution:** Ensure each instance has a unique instance ID: + +```properties +spring.boot.admin.client.instance.metadata.instanceId=${spring.application.name}:${random.value} +``` + +--- + +## Security & Authentication + +### How do I secure the Admin Server UI? + +Add Spring Security dependency and configure authentication: + +```xml + + org.springframework.boot + spring-boot-starter-security + +``` + +```java +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .requestMatchers("/assets/**", "/login").permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form.loginPage("/login")) + .logout(logout -> logout.logoutUrl("/logout")) + .httpBasic(withDefaults()); + return http.build(); + } +} +``` + +### CORS errors when accessing client applications + +> **Related Issues:** [#1362](https://github.com/codecentric/spring-boot-admin/issues/1362), [#1691](https://github.com/codecentric/spring-boot-admin/issues/1691) +> **Stack Overflow:** [spring-boot+cors](https://stackoverflow.com/questions/tagged/spring-boot+cors) + +When client applications run on different domains, browsers make preflight requests that can fail. + +**Solution:** Configure CORS on the Admin Server: + +```java +@Bean +public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:3000") + .allowedMethods("GET", "POST", "PUT", "DELETE") + .allowCredentials(true); + } + }; +} +``` + +### CSRF protection is blocking client registration + +By default, Spring Security's CSRF protection can block registration requests. + +**Solution:** Exempt the registration endpoint: + +```java +http.csrf(csrf -> csrf + .ignoringRequestMatchers("/instances", "/actuator/**") +); +``` + +--- + +## Notifications + +### Mail notifications are not working + +> **Related Issues:** [#507](https://github.com/codecentric/spring-boot-admin/issues/507) +> **Stack Overflow:** [spring-boot+email](https://stackoverflow.com/questions/tagged/spring-boot+email) + +**Checklist:** + +1. **Add mail dependency:** + +```xml + + org.springframework.boot + spring-boot-starter-mail + +``` + +2. **Configure mail properties:** + +```properties +spring.boot.admin.notify.mail.enabled=true +spring.boot.admin.notify.mail.from=admin@example.com +spring.boot.admin.notify.mail.to=alerts@example.com + +spring.mail.host=smtp.example.com +spring.mail.port=587 +spring.mail.username=user +spring.mail.password=secret +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true +``` + +3. **Test mail configuration separately** to ensure SMTP settings are correct. + +### Slack notifications are not sending + +> **Related Issues:** [#202](https://github.com/codecentric/spring-boot-admin/issues/202), [#356](https://github.com/codecentric/spring-boot-admin/issues/356) +> **Stack Overflow:** [spring-boot+slack](https://stackoverflow.com/questions/tagged/spring-boot+slack) + +**Solution:** Configure Slack webhook: + +```properties +spring.boot.admin.notify.slack.enabled=true +spring.boot.admin.notify.slack.webhook-url=https://hooks.slack.com/services/YOUR/WEBHOOK/URL +spring.boot.admin.notify.slack.channel=monitoring +``` + +Note: The channel name should not include the `#` prefix. + +### I'm receiving too many notifications + +> **Related Issues:** [#402](https://github.com/codecentric/spring-boot-admin/issues/402) + +**Solution:** Filter notifications by status changes: + +```properties +# Ignore specific status transitions +spring.boot.admin.notify.mail.ignore-changes=UNKNOWN:UP,UNKNOWN:OFFLINE +``` + +Or create a custom filtered notifier: + +```java +@Bean +@Primary +public FilteringNotifier filteringNotifier(Notifier delegate, InstanceRepository repository) { + return new FilteringNotifier(delegate, repository); +} +``` + +--- + +## Kubernetes & Cloud Deployments + +### Health checks fail with 401 errors in Kubernetes + +> **Related Issues:** [#1325](https://github.com/codecentric/spring-boot-admin/issues/1325) +> **Stack Overflow:** [kubernetes+spring-boot](https://stackoverflow.com/questions/tagged/kubernetes+spring-boot+health-check) + +When health endpoints are secured in Kubernetes, the Admin Server cannot access them. + +**Solution:** Either: + +1. **Make health endpoint public** (for Kubernetes probes): + +```properties +management.endpoint.health.show-details=when-authorized +management.endpoints.web.exposure.include=health,info +``` + +2. **Configure separate ports** for management endpoints: + +```properties +management.server.port=8081 +``` + +Then configure Kubernetes probes to use the management port. + +### Spring Boot Admin creates wrong health URL in Kubernetes + +> **Related Issues:** [#1522](https://github.com/codecentric/spring-boot-admin/issues/1522), [#437](https://github.com/codecentric/spring-boot-admin/issues/437) +> **Stack Overflow:** [kubernetes+spring-boot-admin](https://stackoverflow.com/questions/tagged/kubernetes+spring-boot) + +This happens with multi-port services (e.g., HTTP + gRPC). + +**Solution:** Explicitly configure the management base URL: + +```properties +spring.boot.admin.client.instance.management-base-url=http://my-service:8081/actuator +``` + +### Liveness probe failures causing cascading restarts + +**Important:** Never configure liveness probes to depend on external system health checks. + +```yaml +# Bad - includes external dependencies +livenessProbe: + httpGet: + path: /actuator/health + +# Good - only internal application health +livenessProbe: + httpGet: + path: /actuator/health/liveness +``` + +Configure Spring Boot to separate liveness and readiness: + +```properties +management.health.probes.enabled=true +management.endpoint.health.group.liveness.include=ping +management.endpoint.health.group.readiness.include=db,redis +``` + +--- + +## UI Customization + +### How do I add custom views to the Admin UI? + +> **Related Issues:** [#683](https://github.com/codecentric/spring-boot-admin/issues/683), [#867](https://github.com/codecentric/spring-boot-admin/issues/867) +> **Stack Overflow:** [spring-boot-admin+customization](https://stackoverflow.com/questions/tagged/spring-boot-admin) + +Custom views must be implemented as Vue.js components and placed at: + +``` +/META-INF/spring-boot-admin-server-ui/extensions/{name}/ +``` + +**Example registration:** + +```javascript +SBA.use({ + install({viewRegistry}) { + viewRegistry.addView({ + name: 'custom-view', + path: '/custom', + component: CustomComponent, + label: 'Custom View', + order: 1000, + }); + } +}); +``` + +For development, configure the extension location: + +```properties +spring.boot.admin.ui.extension-resource-locations=file:///path/to/custom-ui/target/dist/ +``` + +### Can I conditionally show custom views based on instance metadata? + +> **Related Issues:** [#1385](https://github.com/codecentric/spring-boot-admin/issues/1385) + +Yes, use the `isEnabled` function in view registration: + +```javascript +viewRegistry.addView({ + name: 'custom-view', + path: '/custom', + component: CustomComponent, + isEnabled: ({instance}) => instance.hasTag('custom-enabled') +}); +``` + +--- + +## Performance & Troubleshooting + +### Admin Server is slow or uses too much memory + +**Common causes:** + +1. **Too many instances being monitored** +2. **Aggressive monitoring intervals** +3. **Event store growing too large** + +**Solutions:** + +1. **Adjust monitoring intervals:** + +```properties +spring.boot.admin.monitor.status-interval=30s +spring.boot.admin.monitor.info-interval=1m +``` + +2. **Use Hazelcast for clustered deployments:** + +```xml + + de.codecentric + spring-boot-admin-server-cloud + + + com.hazelcast + hazelcast + +``` + +3. **Increase JVM memory:** + +```bash +java -Xmx1g -Xms512m -jar admin-server.jar +``` + +### How do I enable DEBUG logging for troubleshooting? + +Add to `application.properties`: + +```properties +# General Admin Server logging +logging.level.de.codecentric.boot.admin=DEBUG + +# Client registration logging +logging.level.de.codecentric.boot.admin.server.services.InstanceRegistry=DEBUG + +# HTTP client logging +logging.level.org.springframework.web.reactive.function.client=DEBUG +``` + +### Where can I get help? + +1. **Check the changelog:** [GitHub Releases](https://github.com/codecentric/spring-boot-admin/releases) +2. **Search existing issues:** [GitHub Issues](https://github.com/codecentric/spring-boot-admin/issues) +3. **Ask the community:** + - [Stack Overflow](https://stackoverflow.com/questions/tagged/spring-boot-admin) - Questions tagged `spring-boot-admin` + - [Stack Overflow Search](https://stackoverflow.com/search?q=spring-boot-admin) - Search all Spring Boot Admin discussions +4. **Report bugs:** [Create an issue](https://github.com/codecentric/spring-boot-admin/issues/new) + +:::note Community Resources +**For questions and troubleshooting:** Use [Stack Overflow](https://stackoverflow.com/questions/tagged/spring-boot-admin) with the `spring-boot-admin` tag. The FAQ entries above reference related Stack Overflow tags for each topic. + +**For bug reports and feature requests:** Use [GitHub Issues](https://github.com/codecentric/spring-boot-admin/issues). The FAQ entries reference specific GitHub issues where bugs were reported and resolved. + +For broader Spring ecosystem questions, also check: +- [Spring Boot on Stack Overflow](https://stackoverflow.com/questions/tagged/spring-boot) +- [Spring Security on Stack Overflow](https://stackoverflow.com/questions/tagged/spring-security) (for security-related questions) +- [Spring Cloud on Stack Overflow](https://stackoverflow.com/questions/tagged/spring-cloud) (for Eureka/Discovery questions) +::: diff --git a/spring-boot-admin-docs/src/site/src/pages/index.tsx b/spring-boot-admin-docs/src/site/src/pages/index.tsx index ef763a85537..4877354862d 100644 --- a/spring-boot-admin-docs/src/site/src/pages/index.tsx +++ b/spring-boot-admin-docs/src/site/src/pages/index.tsx @@ -3,3 +3,4 @@ import { Redirect } from "@docusaurus/router"; export default function Home() { return ; } + diff --git a/spring-boot-admin-docs/src/site/src/propertiesUtil.ts b/spring-boot-admin-docs/src/site/src/propertiesUtil.ts index 181dc373730..7eddac487d9 100644 --- a/spring-boot-admin-docs/src/site/src/propertiesUtil.ts +++ b/spring-boot-admin-docs/src/site/src/propertiesUtil.ts @@ -1,9 +1,17 @@ +/** + * Filters an array of Spring property definitions by their names based on provided keywords. + * + * @param properties - The array of Spring property definitions to filter + * @param keywords - The array of keywords to search for in property names + * @param includeOnly - If true, includes only properties matching keywords; if false, excludes matching properties (default: false) + * @returns A filtered array of property definitions + */ export const filterPropertiesByName = ( properties: Array, keywords: string[], - exclusive: boolean = true + includeOnly: boolean = false ) => { - if (exclusive) { + if (!includeOnly) { return properties.filter(property => !containsKeywordIgnoreCase(property.name, keywords)); } @@ -11,6 +19,10 @@ export const filterPropertiesByName = ( }; function containsKeywordIgnoreCase(str: string, keywords: string[]): boolean { - const lowerStr = str.toLowerCase(); - return keywords.some(keyword => lowerStr.includes(keyword.toLowerCase())); + const searchContext = str.toLowerCase(); + return keywords.some(keyword => { + const searchTerm = keyword.toLowerCase(); + const isIncluded = searchContext.includes(searchTerm); + return isIncluded; + }); } diff --git a/spring-boot-admin-docs/src/site/src/theme/DocCard/index.js b/spring-boot-admin-docs/src/site/src/theme/DocCard/index.js index 1c249d6c647..1c466c71479 100644 --- a/spring-boot-admin-docs/src/site/src/theme/DocCard/index.js +++ b/spring-boot-admin-docs/src/site/src/theme/DocCard/index.js @@ -7,18 +7,33 @@ import isInternalUrl from "@docusaurus/isInternalUrl"; import { translate } from "@docusaurus/Translate"; import Heading from "@theme/Heading"; import styles from "./styles.module.css"; -import { Icon } from '@iconify/react'; +import { Icon } from "@iconify/react"; const ICON_MAP = { - ui: , - http: , - properties: , - server: , - notifications: , - python: , - features: , - configuration: -} + apps: , + "arrow-up": , + bell: , + book: , + category: , + cloud: , + configuration: , + database: , + features: , + "file-code": , + home: , + http: , + link: , + notifications: , + package: , + properties: , + puzzle: , + python: , + rocket: , + server: , + shield: , + ui: , + wrench: +}; function useCategoryItemsPlural() { const { selectMessage } = usePluralForm(); @@ -70,6 +85,17 @@ function CardLayout({ href, icon, title, description }) { ); } +export default function DocCard({ item }) { + switch (item.type) { + case "link": + return ; + case "category": + return ; + default: + throw new Error(`unknown item type ${JSON.stringify(item)}`); + } +} + function CardCategory({ item }) { const href = findFirstSidebarItemLink(item); const categoryItemsPlural = useCategoryItemsPlural(); @@ -80,7 +106,7 @@ function CardCategory({ item }) { return ( @@ -91,22 +117,13 @@ function CardLink({ item }) { const doc = useDocById(item.docId ?? undefined); return ( - + <> + + ); } - -export default function DocCard({ item }) { - switch (item.type) { - case "link": - return ; - case "category": - return ; - default: - throw new Error(`unknown item type ${JSON.stringify(item)}`); - } -} diff --git a/spring-boot-admin-docs/src/site/tsconfig.json b/spring-boot-admin-docs/src/site/tsconfig.json index 94f6ea8e22c..08e8217fab8 100644 --- a/spring-boot-admin-docs/src/site/tsconfig.json +++ b/spring-boot-admin-docs/src/site/tsconfig.json @@ -3,7 +3,18 @@ "extends": "@docusaurus/tsconfig", "compilerOptions": { "target": "es2023", - "lib": ["es2023", "dom"], - "baseUrl": "." - } + "lib": [ + "es2023", + "dom" + ], + "baseUrl": ".", + "paths": { + "@sba": [ + "/../../.." + ] + } + }, + "include": [ + "**/*.mdx" + ] } diff --git a/spring-boot-admin-samples/spring-boot-admin-sample-consul/pom.xml b/spring-boot-admin-samples/spring-boot-admin-sample-consul/pom.xml index ba4cfae5da7..172ce9b43f4 100644 --- a/spring-boot-admin-samples/spring-boot-admin-sample-consul/pom.xml +++ b/spring-boot-admin-samples/spring-boot-admin-sample-consul/pom.xml @@ -34,7 +34,7 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webmvc org.springframework.boot diff --git a/spring-boot-admin-samples/spring-boot-admin-sample-eureka/pom.xml b/spring-boot-admin-samples/spring-boot-admin-sample-eureka/pom.xml index 579660dc349..08110f422a4 100644 --- a/spring-boot-admin-samples/spring-boot-admin-sample-eureka/pom.xml +++ b/spring-boot-admin-samples/spring-boot-admin-sample-eureka/pom.xml @@ -38,7 +38,7 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webmvc org.springframework.boot diff --git a/spring-boot-admin-samples/spring-boot-admin-sample-hazelcast/pom.xml b/spring-boot-admin-samples/spring-boot-admin-sample-hazelcast/pom.xml index 146b0af2b5d..36f37079390 100644 --- a/spring-boot-admin-samples/spring-boot-admin-sample-hazelcast/pom.xml +++ b/spring-boot-admin-samples/spring-boot-admin-sample-hazelcast/pom.xml @@ -34,7 +34,7 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webmvc org.springframework.boot diff --git a/spring-boot-admin-samples/spring-boot-admin-sample-servlet/pom.xml b/spring-boot-admin-samples/spring-boot-admin-sample-servlet/pom.xml index c2e1a0cc144..56b4e1cd4c6 100644 --- a/spring-boot-admin-samples/spring-boot-admin-sample-servlet/pom.xml +++ b/spring-boot-admin-samples/spring-boot-admin-sample-servlet/pom.xml @@ -46,7 +46,7 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webmvc org.springframework.cloud @@ -78,7 +78,7 @@ org.jolokia - jolokia-support-spring + jolokia-support-springboot diff --git a/spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/SecuritySecureConfig.java b/spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/SecuritySecureConfig.java index 46bd0144932..5d8da5436b8 100644 --- a/spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/SecuritySecureConfig.java +++ b/spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/java/de/codecentric/boot/admin/sample/SecuritySecureConfig.java @@ -19,7 +19,7 @@ import java.util.UUID; import jakarta.servlet.DispatcherType; -import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.security.autoconfigure.SecurityProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; diff --git a/spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/resources/application.yml b/spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/resources/application.yml index e060d8f6a8f..7dc1ef091d7 100644 --- a/spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/resources/application.yml +++ b/spring-boot-admin-samples/spring-boot-admin-sample-servlet/src/main/resources/application.yml @@ -137,4 +137,4 @@ management: endpoint: sbom: application: - location: classpath:bom.json + location: optional:classpath:bom.json diff --git a/spring-boot-admin-samples/spring-boot-admin-sample-war/pom.xml b/spring-boot-admin-samples/spring-boot-admin-sample-war/pom.xml index b35c6bd3093..ee60b5eab9e 100644 --- a/spring-boot-admin-samples/spring-boot-admin-sample-war/pom.xml +++ b/spring-boot-admin-samples/spring-boot-admin-sample-war/pom.xml @@ -41,7 +41,7 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webmvc org.springframework.boot diff --git a/spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/pom.xml b/spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/pom.xml index 5eb66e910f5..4234d4f7aa7 100644 --- a/spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/pom.xml +++ b/spring-boot-admin-samples/spring-boot-admin-sample-zookeeper/pom.xml @@ -33,7 +33,7 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webmvc org.springframework.boot diff --git a/spring-boot-admin-server-cloud/pom.xml b/spring-boot-admin-server-cloud/pom.xml index a2beab77024..9aa6b12f1e4 100644 --- a/spring-boot-admin-server-cloud/pom.xml +++ b/spring-boot-admin-server-cloud/pom.xml @@ -50,7 +50,7 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webmvc @@ -62,7 +62,7 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webmvc @@ -74,7 +74,7 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webmvc @@ -105,7 +105,7 @@ test - com.fasterxml.jackson.datatype + tools.jackson.datatype jackson-datatype-json-org test diff --git a/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/config/AdminServerDiscoveryAutoConfiguration.java b/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/config/AdminServerDiscoveryAutoConfiguration.java index 03bd1911a41..f4c4ba623a0 100644 --- a/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/config/AdminServerDiscoveryAutoConfiguration.java +++ b/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/config/AdminServerDiscoveryAutoConfiguration.java @@ -18,18 +18,16 @@ import com.netflix.discovery.EurekaClient; import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform; 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.cloud.CloudPlatform; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.client.discovery.DiscoveryClient; -import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; -import org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClient; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import de.codecentric.boot.admin.server.cloud.discovery.DefaultServiceInstanceConverter; @@ -83,7 +81,7 @@ public EurekaServiceInstanceConverter serviceInstanceConverter() { @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean({ ServiceInstanceConverter.class }) - @Conditional(KubernetesDiscoveryClientCondition.class) + @ConditionalOnCloudPlatform(CloudPlatform.KUBERNETES) public static class KubernetesConverterConfiguration { @Bean @@ -95,22 +93,4 @@ public KubernetesServiceInstanceConverter serviceInstanceConverter( } - private static class KubernetesDiscoveryClientCondition extends AnyNestedCondition { - - KubernetesDiscoveryClientCondition() { - super(ConfigurationPhase.REGISTER_BEAN); - } - - @ConditionalOnBean(KubernetesInformerDiscoveryClient.class) - static class OfficialKubernetesCondition { - - } - - @ConditionalOnBean(KubernetesDiscoveryClient.class) - static class Fabric8KubernetesCondition { - - } - - } - } diff --git a/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/config/package-info.java b/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/config/package-info.java index 08605802853..ecc8fdc08e5 100644 --- a/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/config/package-info.java +++ b/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/config/package-info.java @@ -14,9 +14,7 @@ * limitations under the License. */ -@NonNullApi -@NonNullFields +@NullMarked package de.codecentric.boot.admin.server.cloud.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/DefaultServiceInstanceConverter.java b/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/DefaultServiceInstanceConverter.java index b2c31be6749..2c520903a75 100644 --- a/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/DefaultServiceInstanceConverter.java +++ b/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/DefaultServiceInstanceConverter.java @@ -20,10 +20,10 @@ import java.util.Map; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.client.ServiceInstance; -import org.springframework.lang.Nullable; import org.springframework.web.util.UriComponentsBuilder; import de.codecentric.boot.admin.server.domain.entities.Instance; diff --git a/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/package-info.java b/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/package-info.java index a7ecc8d8de5..2355b7c0cad 100644 --- a/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/package-info.java +++ b/spring-boot-admin-server-cloud/src/main/java/de/codecentric/boot/admin/server/cloud/discovery/package-info.java @@ -14,9 +14,7 @@ * limitations under the License. */ -@NonNullApi -@NonNullFields +@NullMarked package de.codecentric.boot.admin.server.cloud.discovery; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/AdminApplicationDiscoveryTest.java b/spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/AdminApplicationDiscoveryTest.java index 12f4de1d796..16c9d36616c 100644 --- a/spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/AdminApplicationDiscoveryTest.java +++ b/spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/AdminApplicationDiscoveryTest.java @@ -18,11 +18,8 @@ import java.net.URI; import java.time.Duration; -import java.util.List; import java.util.concurrent.atomic.AtomicReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule; import org.json.JSONObject; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -31,21 +28,24 @@ import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; +import org.springframework.cloud.client.discovery.simple.InstanceProperties; import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryProperties; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.http.MediaType; -import org.springframework.http.codec.json.Jackson2JsonDecoder; -import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.json.JacksonJsonDecoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.function.client.ExchangeStrategies; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.datatype.jsonorg.JsonOrgModule; import de.codecentric.boot.admin.server.config.EnableAdminServer; @@ -82,50 +82,51 @@ void lifecycle() { AtomicReference location = new AtomicReference<>(); StepVerifier.create(getEventStream().log()).expectSubscription().then(() -> { - listEmptyInstances(); - location.set(registerInstance()); + StepVerifier.create(listEmptyInstances()).expectNext(true).verifyComplete(); + StepVerifier.create(registerInstance()).consumeNextWith(location::set).verifyComplete(); }) .assertNext((event) -> assertThat(event.opt("type")).isEqualTo("REGISTERED")) .assertNext((event) -> assertThat(event.opt("type")).isEqualTo("STATUS_CHANGED")) .assertNext((event) -> assertThat(event.opt("type")).isEqualTo("ENDPOINTS_DETECTED")) .assertNext((event) -> assertThat(event.opt("type")).isEqualTo("INFO_CHANGED")) .then(() -> { - getInstance(location.get()); - listInstances(); + StepVerifier.create(getInstance(location.get())).expectNext(true).verifyComplete(); + StepVerifier.create(listInstances()).expectNext(true).verifyComplete(); deregisterInstance(); }) .assertNext((event) -> assertThat(event.opt("type")).isEqualTo("DEREGISTERED")) - .then(this::listEmptyInstances) + .then(() -> StepVerifier.create(listEmptyInstances()).expectNext(true).verifyComplete()) .thenCancel() .verify(Duration.ofSeconds(60)); } - private URI registerInstance() { + private Mono registerInstance() { // We register the instance by setting static values for the SimpleDiscoveryClient // and issuing a // InstanceRegisteredEvent that makes sure the instance gets registered. - DefaultServiceInstance serviceInstance = new DefaultServiceInstance(); - serviceInstance.setServiceId("Test-Instance"); - serviceInstance.setUri(URI.create("http://localhost:" + this.port)); - serviceInstance.getMetadata().put("management.context-path", "/mgmt"); - this.simpleDiscovery.getInstances().put("Test-Application", singletonList(serviceInstance)); + InstanceProperties instanceProps = new InstanceProperties(); + instanceProps.setServiceId("Test-Instance"); + instanceProps.setUri(URI.create("http://localhost:" + this.port)); + instanceProps.getMetadata().put("management.context-path", "/mgmt"); + this.simpleDiscovery.getInstances().put("Test-Instance", singletonList(instanceProps)); this.instance.publishEvent(new InstanceRegisteredEvent<>(new Object(), null)); // To get the location of the registered instances we fetch the instance with the // name. - List applications = this.webClient.get() + //@formatter:off + return this.webClient.get() .uri("/instances?name=Test-Instance") .accept(MediaType.APPLICATION_JSON) .exchange() - .expectStatus() - .isOk() - .returnResult(JSONObject.class) - .getResponseBody() + .returnResult(JSONObject.class).getResponseBody() .collectList() - .block(); - assertThat(applications).hasSize(1); - return URI.create("http://localhost:" + this.port + "/instances/" + applications.get(0).optString("id")); + .map((applications) -> { + assertThat(applications).hasSize(1); + return URI + .create("http://localhost:" + this.port + "/instances/" + applications.get(0).optString("id")); + }); + //@formatter:on } private void deregisterInstance() { @@ -143,46 +144,55 @@ private Flux getEventStream() { //@formatter:on } - private void getInstance(URI uri) { + private Mono getInstance(URI uri) { //@formatter:off - this.webClient.get().uri(uri).accept(MediaType.APPLICATION_JSON) + return this.webClient.get().uri(uri).accept(MediaType.APPLICATION_JSON) .exchange() - .expectStatus().isOk() - .expectBody() - .jsonPath("$.registration.name").isEqualTo("Test-Instance") - .jsonPath("$.statusInfo.status").isEqualTo("UP") - .jsonPath("$.info.test").isEqualTo("foobar"); + .returnResult(String.class).getResponseBody().single() + .map((body) -> { + assertThat(body).contains("\"name\":\"Test-Instance\""); + assertThat(body).contains("\"status\":\"UP\""); + assertThat(body).contains("\"test\":\"foobar\""); + return true; + }); //@formatter:on } - private void listInstances() { + private Mono listInstances() { //@formatter:off - this.webClient.get().uri("/instances").accept(MediaType.APPLICATION_JSON) + return this.webClient.get().uri("/instances").accept(MediaType.APPLICATION_JSON) .exchange() - .expectStatus().isOk() - .expectBody() - .jsonPath("$[0].registration.name").isEqualTo("Test-Instance") - .jsonPath("$[0].statusInfo.status").isEqualTo("UP") - .jsonPath("$[0].info.test").isEqualTo("foobar"); + .returnResult(String.class).getResponseBody().single() + .map((body) -> { + assertThat(body).contains("\"name\":\"Test-Instance\""); + assertThat(body).contains("\"status\":\"UP\""); + assertThat(body).contains("\"test\":\"foobar\""); + return true; + }); //@formatter:on } - private void listEmptyInstances() { + private Mono listEmptyInstances() { //@formatter:off - this.webClient.get().uri("/instances").accept(MediaType.APPLICATION_JSON) + return this.webClient.get().uri("/instances").accept(MediaType.APPLICATION_JSON) .exchange() - .expectStatus().isOk() - .expectBody().json("[]"); + .returnResult(String.class).getResponseBody() + .collectList() + .map((list) -> { + assertThat(list).hasSize(1); + assertThat(list.get(0)).isEqualTo("[]"); + return true; + }); //@formatter:on } private WebTestClient createWebClient(int port) { - ObjectMapper mapper = new ObjectMapper().registerModule(new JsonOrgModule()); + JsonMapper mapper = JsonMapper.builder().addModule(new JsonOrgModule()).build(); return WebTestClient.bindToServer() .baseUrl("http://localhost:" + port) .exchangeStrategies(ExchangeStrategies.builder().codecs((configurer) -> { - configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper)); - configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper)); + configurer.defaultCodecs().jacksonJsonDecoder(new JacksonJsonDecoder(mapper)); + configurer.defaultCodecs().jacksonJsonEncoder(new JacksonJsonEncoder(mapper)); }).build()) .build(); } diff --git a/spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/config/AdminServerDiscoveryAutoConfigurationTest.java b/spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/config/AdminServerDiscoveryAutoConfigurationTest.java index 34ab4fb8001..33c91522122 100644 --- a/spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/config/AdminServerDiscoveryAutoConfigurationTest.java +++ b/spring-boot-admin-server-cloud/src/test/java/de/codecentric/boot/admin/server/cloud/config/AdminServerDiscoveryAutoConfigurationTest.java @@ -17,20 +17,17 @@ package de.codecentric.boot.admin.server.cloud.config; import com.netflix.discovery.EurekaClient; -import io.kubernetes.client.openapi.apis.CoreV1Api; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.http.client.reactive.ClientHttpConnectorAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.autoconfigure.reactive.ReactiveHttpClientAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration; import org.springframework.cloud.commons.util.UtilAutoConfiguration; -import org.springframework.cloud.kubernetes.client.discovery.KubernetesInformerDiscoveryClient; import org.springframework.cloud.kubernetes.commons.discovery.KubernetesDiscoveryProperties; -import org.springframework.cloud.kubernetes.fabric8.discovery.KubernetesDiscoveryClient; import de.codecentric.boot.admin.server.cloud.discovery.DefaultServiceInstanceConverter; import de.codecentric.boot.admin.server.cloud.discovery.EurekaServiceInstanceConverter; @@ -46,9 +43,9 @@ class AdminServerDiscoveryAutoConfigurationTest { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(UtilAutoConfiguration.class, - ClientHttpConnectorAutoConfiguration.class, WebClientAutoConfiguration.class, - AdminServerAutoConfiguration.class, AdminServerDiscoveryAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(UtilAutoConfiguration.class, ReactiveHttpClientAutoConfiguration.class, + WebClientAutoConfiguration.class, AdminServerAutoConfiguration.class, + AdminServerDiscoveryAutoConfiguration.class)) .withUserConfiguration(AdminServerMarkerConfiguration.class); @Test @@ -62,24 +59,16 @@ void defaultServiceInstanceConverter() { void eurekaServiceInstanceConverter() { this.contextRunner.withBean(EurekaClient.class, () -> mock(EurekaClient.class)) .withBean(DiscoveryClient.class, () -> mock(DiscoveryClient.class)) - .run((context) -> assertThat(context).getBean(ServiceInstanceConverter.class) + .run((context) -> assertThat(context.getBean(ServiceInstanceConverter.class)) .isInstanceOf(EurekaServiceInstanceConverter.class)); } @Test - void officialKubernetesServiceInstanceConverter() { + void kubernetesServiceInstanceConverter() { this.contextRunner.withUserConfiguration(KubernetesDiscoveryPropertiesConfiguration.class) - .withBean(CoreV1Api.class, () -> mock(CoreV1Api.class)) - .withBean(KubernetesInformerDiscoveryClient.class, () -> mock(KubernetesInformerDiscoveryClient.class)) - .run((context) -> assertThat(context).getBean(ServiceInstanceConverter.class) - .isInstanceOf(KubernetesServiceInstanceConverter.class)); - } - - @Test - void fabric8KubernetesServiceInstanceConverter() { - this.contextRunner.withUserConfiguration(KubernetesDiscoveryPropertiesConfiguration.class) - .withBean(KubernetesDiscoveryClient.class, () -> mock(KubernetesDiscoveryClient.class)) - .run((context) -> assertThat(context).getBean(ServiceInstanceConverter.class) + .withBean(DiscoveryClient.class, () -> mock(DiscoveryClient.class)) + .withPropertyValues("spring.main.cloud-platform=KUBERNETES") + .run((context) -> assertThat(context.getBean(ServiceInstanceConverter.class)) .isInstanceOf(KubernetesServiceInstanceConverter.class)); } @@ -87,7 +76,7 @@ void fabric8KubernetesServiceInstanceConverter() { void customServiceInstanceConverter() { this.contextRunner.withUserConfiguration(SimpleDiscoveryClientAutoConfiguration.class) .withBean(CustomServiceInstanceConverter.class) - .run((context) -> assertThat(context).getBean(ServiceInstanceConverter.class) + .run((context) -> assertThat(context.getBean(ServiceInstanceConverter.class)) .isInstanceOf(CustomServiceInstanceConverter.class)); } diff --git a/spring-boot-admin-server-ui/pom.xml b/spring-boot-admin-server-ui/pom.xml index 265c02476fd..f17a4aa6f96 100644 --- a/spring-boot-admin-server-ui/pom.xml +++ b/spring-boot-admin-server-ui/pom.xml @@ -38,7 +38,7 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webmvc true diff --git a/spring-boot-admin-server-ui/src/main/frontend/components.d.ts b/spring-boot-admin-server-ui/src/main/frontend/components.d.ts index 604df6ad55e..f79ecaefebb 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/components.d.ts +++ b/spring-boot-admin-server-ui/src/main/frontend/components.d.ts @@ -3,7 +3,7 @@ // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 // biome-ignore lint: disable -export {} +export {}; /* prettier-ignore */ declare module 'vue' { diff --git a/spring-boot-admin-server-ui/src/main/frontend/components/sba-accordion.stories.ts b/spring-boot-admin-server-ui/src/main/frontend/components/sba-accordion.stories.ts new file mode 100644 index 00000000000..4e7f8b76a28 --- /dev/null +++ b/spring-boot-admin-server-ui/src/main/frontend/components/sba-accordion.stories.ts @@ -0,0 +1,239 @@ +/* + * Copyright 2014-2025 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ref } from 'vue'; + +import SbaAccordion from './sba-accordion.vue'; + +export default { + component: SbaAccordion, + title: 'Components/Accordion', +}; + +const Template = (args) => { + return { + components: { SbaAccordion }, + setup() { + const isOpen = ref(args.modelValue ?? true); + return { args, isOpen }; + }, + template: ` + + + + + +
+ Current state: {{ isOpen ? 'Open' : 'Closed' }} +
+ `, + }; +}; + +export const DefaultOpen = { + render: Template, + + args: { + modelValue: true, + title: 'Application Information', + slot: `
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vitae dolor ac ante ornare pharetra. + Proin laoreet ex et lacinia hendrerit. Fusce sed justo at nulla pellentesque maximus sed at diam.

+

Suspendisse sem lorem, lobortis vel orci quis, efficitur porta massa. In vel neque justo. + Maecenas dapibus quam ut nisl porta, molestie egestas felis maximus.

+
`, + }, +}; + +export const DefaultClosed = { + render: Template, + + args: { + modelValue: false, + title: 'System Properties', + slot: `
+

This content is initially hidden. Click the title to expand the accordion.

+
`, + }, +}; + +export const WithKeyValueTable = { + render: Template, + + args: { + modelValue: true, + title: 'Health Details', + slot: ` +
+
+
Status
+
UP
+
+
+
Disk Space
+
10.5 GB free
+
+
+
Database
+
Connected
+
+
+
Memory
+
512 MB / 2 GB
+
+
`, + }, +}; + +export const WithActions = { + render: Template, + + args: { + modelValue: true, + title: 'Configuration Settings', + actions: + '', + slot: `
+

Configuration content with action buttons in the header.

+
`, + }, +}; + +export const WithId = { + render: (args) => { + return { + components: { SbaAccordion }, + setup() { + const isOpen = ref(args.modelValue ?? true); + return { args, isOpen }; + }, + template: ` +
+

+ This accordion has an ID and will persist its state in localStorage. + Try toggling it and refreshing the page. +

+ + + + +
+ Current state: {{ isOpen ? 'Open' : 'Closed' }} +
+ LocalStorage key: de.codecentric.spring-boot-admin.accordion.${args.id}.open +
+
+ `, + }; + }, + + args: { + id: 'storybook-example', + modelValue: true, + title: 'Persisted State Example', + slot: `
+

This accordion's open/closed state is stored in localStorage using the ID "storybook-example".

+

Try toggling it and refreshing the browser to see the state persist.

+
`, + }, +}; + +export const MultipleAccordions = { + render: (args) => { + return { + components: { SbaAccordion }, + setup() { + const accordion1Open = ref(true); + const accordion2Open = ref(false); + const accordion3Open = ref(true); + return { args, accordion1Open, accordion2Open, accordion3Open }; + }, + template: ` +
+ + + + + + + + + + + + + + + + +
+ States: + 1: {{ accordion1Open ? 'Open' : 'Closed' }} | + 2: {{ accordion2Open ? 'Open' : 'Closed' }} | + 3: {{ accordion3Open ? 'Open' : 'Closed' }} +
+
+ `, + }; + }, + + args: {}, +}; + +export const NestedContent = { + render: Template, + + args: { + modelValue: true, + title: 'Advanced Configuration', + slot: ` +
+
+

Server Settings

+
    +
  • Port: 8080
  • +
  • Context Path: /admin
  • +
  • SSL Enabled: false
  • +
+
+
+

Monitoring

+
    +
  • Interval: 10000ms
  • +
  • Timeout: 5000ms
  • +
  • Retries: 3
  • +
+
+
`, + }, +}; diff --git a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/sba-accordion.vue b/spring-boot-admin-server-ui/src/main/frontend/components/sba-accordion.vue similarity index 84% rename from spring-boot-admin-server-ui/src/main/frontend/views/instances/details/sba-accordion.vue rename to spring-boot-admin-server-ui/src/main/frontend/components/sba-accordion.vue index 28c06c41c0c..905692e9b23 100644 --- a/spring-boot-admin-server-ui/src/main/frontend/views/instances/details/sba-accordion.vue +++ b/spring-boot-admin-server-ui/src/main/frontend/components/sba-accordion.vue @@ -18,10 +18,14 @@
PropertyDescription
-
+
{a.name}
-
-

- {hasDefaultValueOrType(a) && ( -

- {a.type && ( -
-
Type: 
-
{a.type}
-
- )} - {a.defaultValue && ( -
-
Default: 
-
{JSON.stringify(a.defaultValue)}
-
- )} -
- )} +
+

+ {hasDefaultValueOrType(a) && ( +

+ {a.type && ( +
+
Type: 
+
{a.type}
+
+ )} + {a.defaultValue && ( +
+
Default: 
+
{JSON.stringify(a.defaultValue)}
+
+ )} +
+ )} +